Skip to content

Commit bcfe9c8

Browse files
marco-ippolitoruyadorno
authored andcommitted
util: add sourcemap support to getCallSites
PR-URL: #55589 Backport-PR-URL: #56209 Fixes: #55109 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent 359fff1 commit bcfe9c8

File tree

5 files changed

+193
-5
lines changed

5 files changed

+193
-5
lines changed

doc/api/util.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
364364
// when printed to a terminal.
365365
```
366366

367-
## `util.getCallSites(frameCount)`
367+
## `util.getCallSites(frameCountOrOptions, [options])`
368368

369369
> Stability: 1.1 - Active development
370370
@@ -376,8 +376,11 @@ changes:
376376
description: The API is renamed from `util.getCallSite` to `util.getCallSites()`.
377377
-->
378378

379-
* `frameCount` {number} Number of frames to capture as call site objects.
379+
* `frameCount` {number} Optional number of frames to capture as call site objects.
380380
**Default:** `10`. Allowable range is between 1 and 200.
381+
* `options` {Object} Optional
382+
* `sourceMap` {boolean} Reconstruct the original location in the stacktrace from the source-map.
383+
Enabled by default with the flag `--enable-source-maps`.
381384
* Returns: {Object\[]} An array of call site objects
382385
* `functionName` {string} Returns the name of the function associated with this call site.
383386
* `scriptName` {string} Returns the name of the resource that contains the script for the
@@ -425,6 +428,33 @@ function anotherFunction() {
425428
anotherFunction();
426429
```
427430

431+
It is possible to reconstruct the original locations by setting the option `sourceMap` to `true`.
432+
If the source map is not available, the original location will be the same as the current location.
433+
When the `--enable-source-maps` flag is enabled, for example when using `--experimental-transform-types`,
434+
`sourceMap` will be true by default.
435+
436+
```ts
437+
import util from 'node:util';
438+
439+
interface Foo {
440+
foo: string;
441+
}
442+
443+
const callSites = util.getCallSites({ sourceMap: true });
444+
445+
// With sourceMap:
446+
// Function Name: ''
447+
// Script Name: example.js
448+
// Line Number: 7
449+
// Column Number: 26
450+
451+
// Without sourceMap:
452+
// Function Name: ''
453+
// Script Name: example.js
454+
// Line Number: 2
455+
// Column Number: 26
456+
```
457+
428458
## `util.getSystemErrorName(err)`
429459

430460
<!-- YAML

lib/util.js

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
ArrayIsArray,
2626
ArrayPrototypeJoin,
2727
ArrayPrototypePop,
28+
ArrayPrototypePush,
2829
Date,
2930
DatePrototypeGetDate,
3031
DatePrototypeGetHours,
@@ -70,6 +71,7 @@ const {
7071
validateNumber,
7172
validateString,
7273
validateOneOf,
74+
validateObject,
7375
} = require('internal/validators');
7476
const { isBuffer } = require('buffer').Buffer;
7577
const {
@@ -84,11 +86,13 @@ function lazyUtilColors() {
8486
utilColors ??= require('internal/util/colors');
8587
return utilColors;
8688
}
89+
const { getOptionValue } = require('internal/options');
8790

8891
const binding = internalBinding('util');
8992

9093
const {
9194
deprecate,
95+
getLazy,
9296
getSystemErrorMap,
9397
getSystemErrorName: internalErrorName,
9498
getSystemErrorMessage: internalErrorMessage,
@@ -472,14 +476,90 @@ function parseEnv(content) {
472476
return binding.parseEnv(content);
473477
}
474478

479+
const lazySourceMap = getLazy(() => require('internal/source_map/source_map_cache'));
480+
481+
/**
482+
* @typedef {object} CallSite // The call site
483+
* @property {string} scriptName // The name of the resource that contains the
484+
* script for the function for this StackFrame
485+
* @property {string} functionName // The name of the function associated with this stack frame
486+
* @property {number} lineNumber // The number, 1-based, of the line for the associate function call
487+
* @property {number} columnNumber // The 1-based column offset on the line for the associated function call
488+
*/
489+
490+
/**
491+
* @param {CallSite} callSite // The call site object to reconstruct from source map
492+
* @returns {CallSite | undefined} // The reconstructed call site object
493+
*/
494+
function reconstructCallSite(callSite) {
495+
const { scriptName, lineNumber, column } = callSite;
496+
const sourceMap = lazySourceMap().findSourceMap(scriptName);
497+
if (!sourceMap) return;
498+
const entry = sourceMap.findEntry(lineNumber - 1, column - 1);
499+
if (!entry?.originalSource) return;
500+
return {
501+
__proto__: null,
502+
// If the name is not found, it is an empty string to match the behavior of `util.getCallSite()`
503+
functionName: entry.name ?? '',
504+
scriptName: entry.originalSource,
505+
lineNumber: entry.originalLine + 1,
506+
column: entry.originalColumn + 1,
507+
};
508+
}
509+
510+
/**
511+
*
512+
* The call site array to map
513+
* @param {CallSite[]} callSites
514+
* Array of objects with the reconstructed call site
515+
* @returns {CallSite[]}
516+
*/
517+
function mapCallSite(callSites) {
518+
const result = [];
519+
for (let i = 0; i < callSites.length; ++i) {
520+
const callSite = callSites[i];
521+
const found = reconstructCallSite(callSite);
522+
ArrayPrototypePush(result, found ?? callSite);
523+
}
524+
return result;
525+
}
526+
527+
/**
528+
* @typedef {object} CallSiteOptions // The call site options
529+
* @property {boolean} sourceMap // Enable source map support
530+
*/
531+
475532
/**
476533
* Returns the callSite
477534
* @param {number} frameCount
478-
* @returns {object}
535+
* @param {CallSiteOptions} options
536+
* @returns {CallSite[]}
479537
*/
480-
function getCallSites(frameCount = 10) {
538+
function getCallSites(frameCount = 10, options) {
539+
// If options is not provided check if frameCount is an object
540+
if (options === undefined) {
541+
if (typeof frameCount === 'object') {
542+
// If frameCount is an object, it is the options object
543+
options = frameCount;
544+
validateObject(options, 'options');
545+
validateBoolean(options.sourceMap, 'options.sourceMap');
546+
frameCount = 10;
547+
} else {
548+
// If options is not provided, set it to an empty object
549+
options = {};
550+
};
551+
} else {
552+
// If options is provided, validate it
553+
validateObject(options, 'options');
554+
validateBoolean(options.sourceMap, 'options.sourceMap');
555+
}
556+
481557
// Using kDefaultMaxCallStackSizeToCapture as reference
482558
validateNumber(frameCount, 'frameCount', 1, 200);
559+
// If options.sourceMaps is true or if sourceMaps are enabled but the option.sourceMaps is not set explictly to false
560+
if (options.sourceMap === true || (getOptionValue('--enable-source-maps') && options.sourceMap !== false)) {
561+
return mapCallSite(binding.getCallSites(frameCount));
562+
}
483563
return binding.getCallSites(frameCount);
484564
};
485565

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { getCallSites } = require('node:util');
2+
3+
interface CallSite {
4+
A;
5+
B;
6+
}
7+
8+
const callSite = getCallSites({ sourceMap: false })[0];
9+
10+
console.log('mapCallSite: ', callSite);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { getCallSites } = require('node:util');
2+
3+
interface CallSite {
4+
A;
5+
B;
6+
}
7+
8+
const callSite = getCallSites()[0];
9+
10+
console.log('getCallSite: ', callSite);

test/parallel/test-util-getcallsites.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ const assert = require('node:assert');
5353
code: 'ERR_OUT_OF_RANGE'
5454
}));
5555
assert.throws(() => {
56-
getCallSites({});
56+
getCallSites([]);
57+
}, common.expectsError({
58+
code: 'ERR_INVALID_ARG_TYPE'
59+
}));
60+
assert.throws(() => {
61+
getCallSites({}, {});
62+
}, common.expectsError({
63+
code: 'ERR_INVALID_ARG_TYPE'
64+
}));
65+
assert.throws(() => {
66+
getCallSites(10, 10);
5767
}, common.expectsError({
5868
code: 'ERR_INVALID_ARG_TYPE'
5969
}));
@@ -104,3 +114,51 @@ const assert = require('node:assert');
104114
assert.notStrictEqual(callSites.length, 0);
105115
Error.stackTraceLimit = originalStackTraceLimit;
106116
}
117+
118+
{
119+
const { status, stderr, stdout } = spawnSync(process.execPath, [
120+
'--no-warnings',
121+
'--experimental-transform-types',
122+
fixtures.path('typescript/ts/test-get-callsite.ts'),
123+
]);
124+
125+
const output = stdout.toString();
126+
assert.strictEqual(stderr.toString(), '');
127+
assert.match(output, /lineNumber: 8/);
128+
assert.match(output, /column: 18/);
129+
assert.match(output, /test-get-callsite\.ts/);
130+
assert.strictEqual(status, 0);
131+
}
132+
133+
{
134+
const { status, stderr, stdout } = spawnSync(process.execPath, [
135+
'--no-warnings',
136+
'--experimental-transform-types',
137+
'--no-enable-source-maps',
138+
fixtures.path('typescript/ts/test-get-callsite.ts'),
139+
]);
140+
141+
const output = stdout.toString();
142+
assert.strictEqual(stderr.toString(), '');
143+
// Line should be wrong when sourcemaps are disable
144+
assert.match(output, /lineNumber: 2/);
145+
assert.match(output, /column: 18/);
146+
assert.match(output, /test-get-callsite\.ts/);
147+
assert.strictEqual(status, 0);
148+
}
149+
150+
{
151+
// Source maps should be disabled when options.sourceMap is false
152+
const { status, stderr, stdout } = spawnSync(process.execPath, [
153+
'--no-warnings',
154+
'--experimental-transform-types',
155+
fixtures.path('typescript/ts/test-get-callsite-explicit.ts'),
156+
]);
157+
158+
const output = stdout.toString();
159+
assert.strictEqual(stderr.toString(), '');
160+
assert.match(output, /lineNumber: 2/);
161+
assert.match(output, /column: 18/);
162+
assert.match(output, /test-get-callsite-explicit\.ts/);
163+
assert.strictEqual(status, 0);
164+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy