Skip to content

Commit a9977d8

Browse files
util: add sourcemap support to getCallSite
1 parent 4354a1d commit a9977d8

File tree

5 files changed

+152
-3
lines changed

5 files changed

+152
-3
lines changed

doc/api/util.md

Lines changed: 4 additions & 1 deletion
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.getCallSite(frames)`
367+
## `util.getCallSite(frames, options)`
368368

369369
> Stability: 1.1 - Active development
370370
@@ -374,6 +374,9 @@ added: v22.9.0
374374

375375
* `frames` {number} Number of frames returned in the stacktrace.
376376
**Default:** `10`. Allowable range is between 1 and 200.
377+
* `options` {Object}
378+
* `sourceMap` {boolean} Reconstruct the original location in the stacktrace from the source-map.
379+
Enabled by default with the flag `--enable-source-maps`.
377380
* Returns: {Object\[]} An array of stacktrace objects
378381
* `functionName` {string} Returns the name of the function associated with this stack frame.
379382
* `scriptName` {string} Returns the name of the resource that contains the script for the

lib/util.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
const {
2525
ArrayIsArray,
2626
ArrayPrototypePop,
27+
ArrayPrototypePush,
2728
Error,
2829
ErrorCaptureStackTrace,
2930
FunctionPrototypeBind,
@@ -61,6 +62,7 @@ const {
6162
validateNumber,
6263
validateString,
6364
validateOneOf,
65+
validateObject,
6466
} = require('internal/validators');
6567
const {
6668
isReadableStream,
@@ -74,11 +76,13 @@ function lazyUtilColors() {
7476
utilColors ??= require('internal/util/colors');
7577
return utilColors;
7678
}
79+
const { getOptionValue } = require('internal/options');
7780

7881
const binding = internalBinding('util');
7982

8083
const {
8184
deprecate,
85+
getLazy,
8286
getSystemErrorMap,
8387
getSystemErrorName: internalErrorName,
8488
getSystemErrorMessage: internalErrorMessage,
@@ -328,14 +332,78 @@ function parseEnv(content) {
328332
return binding.parseEnv(content);
329333
}
330334

335+
const lazySourceMap = getLazy(() => require('internal/source_map/source_map_cache'));
336+
337+
/**
338+
* @typedef {object} CallSite // The call site
339+
* @property {string} scriptName // The name of the resource that contains the
340+
* script for the function for this StackFrame
341+
* @property {string} functionName // The name of the function associated with this stack frame
342+
* @property {number} lineNumber // The number, 1-based, of the line for the associate function call
343+
* @property {number} columnNumber // The 1-based column offset on the line for the associated function call
344+
*/
345+
346+
/**
347+
* @param {CallSite} callSite // The call site object to reconstruct from source map
348+
* @returns {CallSite | undefined} // The reconstructed call site object
349+
*/
350+
function reconstructCallSite(callSite) {
351+
const { scriptName, lineNumber, column } = callSite;
352+
const sourceMap = lazySourceMap().findSourceMap(scriptName);
353+
if (!sourceMap) return;
354+
const entry = sourceMap.findEntry(lineNumber - 1, column - 1);
355+
if (!entry?.originalSource) return;
356+
return {
357+
__proto__: null,
358+
// If the name is not found, it is an empty string to match the behavior of `util.getCallSite()`
359+
functionName: entry.name ?? '',
360+
scriptName: entry.originalSource,
361+
lineNumber: entry.originalLine + 1,
362+
column: entry.originalColumn + 1,
363+
};
364+
}
365+
366+
/**
367+
*
368+
* The call site object or array of object to map (ex `util.getCallSite()`)
369+
* @param {CallSite | CallSite[]} callSites
370+
* An object or array of objects with the reconstructed call site
371+
* @returns {CallSite | CallSite[]}
372+
*/
373+
function mapCallSite(callSites) {
374+
if (ArrayIsArray(callSites)) {
375+
const result = [];
376+
for (let i = 0; i < callSites.length; ++i) {
377+
const callSite = callSites[i];
378+
const found = reconstructCallSite(callSite);
379+
ArrayPrototypePush(result, found ?? callSite);
380+
}
381+
return result;
382+
}
383+
return reconstructCallSite(callSites) ?? callSites;
384+
}
385+
331386
/**
332387
* Returns the callSite
333388
* @param {number} frames
334389
* @returns {object}
335390
*/
336-
function getCallSite(frames = 10) {
391+
function getCallSite(frames = 10, options) {
392+
if (options === undefined) {
393+
if (typeof frames === 'object') {
394+
options = frames;
395+
frames = 10;
396+
} else {
397+
options = {};
398+
};
399+
}
337400
// Using kDefaultMaxCallStackSizeToCapture as reference
338401
validateNumber(frames, 'frames', 1, 200);
402+
validateObject(options, 'options');
403+
// If options.sourceMaps is true or if sourceMaps are enabled but the option.sourceMaps is not set explictly to false
404+
if (options.sourceMap === true || (getOptionValue('--enable-source-maps') && options.sourceMap !== false)) {
405+
return mapCallSite(binding.getCallSite(frames));
406+
}
339407
return binding.getCallSite(frames);
340408
};
341409

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

test/parallel/test-util-getCallSite.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-
getCallSite({});
56+
getCallSite([]);
57+
}, common.expectsError({
58+
code: 'ERR_INVALID_ARG_TYPE'
59+
}));
60+
assert.throws(() => {
61+
getCallSite({}, {});
62+
}, common.expectsError({
63+
code: 'ERR_INVALID_ARG_TYPE'
64+
}));
65+
assert.throws(() => {
66+
getCallSite(10, 10);
5767
}, common.expectsError({
5868
code: 'ERR_INVALID_ARG_TYPE'
5969
}));
@@ -104,3 +114,51 @@ const assert = require('node:assert');
104114
assert.notStrictEqual(callsite.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, /typescript\/ts\/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, /typescript\/ts\/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, /typescript\/ts\/test-get-callsite-explicit\.ts/);
163+
assert.strictEqual(status, 0);
164+
}

0 commit comments

Comments
 (0)