util: add sourcemap support to getCallSites

PR-URL: https://github.com/nodejs/node/pull/55589
Fixes: https://github.com/nodejs/node/issues/55109
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Marco Ippolito 2024-11-04 16:06:47 +00:00 committed by GitHub
parent bdc266269c
commit d35cde624f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 194 additions and 6 deletions

View File

@ -3899,7 +3899,7 @@ The `util.getCallSite` API has been removed. Please use [`util.getCallSites()`][
[`url.parse()`]: url.md#urlparseurlstring-parsequerystring-slashesdenotehost [`url.parse()`]: url.md#urlparseurlstring-parsequerystring-slashesdenotehost
[`url.resolve()`]: url.md#urlresolvefrom-to [`url.resolve()`]: url.md#urlresolvefrom-to
[`util._extend()`]: util.md#util_extendtarget-source [`util._extend()`]: util.md#util_extendtarget-source
[`util.getCallSites()`]: util.md#utilgetcallsitesframecount [`util.getCallSites()`]: util.md#utilgetcallsitesframecountoroptions-options
[`util.getSystemErrorName()`]: util.md#utilgetsystemerrornameerr [`util.getSystemErrorName()`]: util.md#utilgetsystemerrornameerr
[`util.inspect()`]: util.md#utilinspectobject-options [`util.inspect()`]: util.md#utilinspectobject-options
[`util.inspect.custom`]: util.md#utilinspectcustom [`util.inspect.custom`]: util.md#utilinspectcustom

View File

@ -364,7 +364,7 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
// when printed to a terminal. // when printed to a terminal.
``` ```
## `util.getCallSites(frameCount)` ## `util.getCallSites(frameCountOrOptions, [options])`
> Stability: 1.1 - Active development > Stability: 1.1 - Active development
@ -372,8 +372,11 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
added: v22.9.0 added: v22.9.0
--> -->
* `frameCount` {number} Number of frames to capture as call site objects. * `frameCount` {number} Optional number of frames to capture as call site objects.
**Default:** `10`. Allowable range is between 1 and 200. **Default:** `10`. Allowable range is between 1 and 200.
* `options` {Object} Optional
* `sourceMap` {boolean} Reconstruct the original location in the stacktrace from the source-map.
Enabled by default with the flag `--enable-source-maps`.
* Returns: {Object\[]} An array of call site objects * Returns: {Object\[]} An array of call site objects
* `functionName` {string} Returns the name of the function associated with this call site. * `functionName` {string} Returns the name of the function associated with this call site.
* `scriptName` {string} Returns the name of the resource that contains the script for the * `scriptName` {string} Returns the name of the resource that contains the script for the
@ -421,6 +424,33 @@ function anotherFunction() {
anotherFunction(); anotherFunction();
``` ```
It is possible to reconstruct the original locations by setting the option `sourceMap` to `true`.
If the source map is not available, the original location will be the same as the current location.
When the `--enable-source-maps` flag is enabled, for example when using `--experimental-transform-types`,
`sourceMap` will be true by default.
```ts
import util from 'node:util';
interface Foo {
foo: string;
}
const callSites = util.getCallSites({ sourceMap: true });
// With sourceMap:
// Function Name: ''
// Script Name: example.js
// Line Number: 7
// Column Number: 26
// Without sourceMap:
// Function Name: ''
// Script Name: example.js
// Line Number: 2
// Column Number: 26
```
## `util.getSystemErrorName(err)` ## `util.getSystemErrorName(err)`
<!-- YAML <!-- YAML

View File

@ -24,6 +24,7 @@
const { const {
ArrayIsArray, ArrayIsArray,
ArrayPrototypePop, ArrayPrototypePop,
ArrayPrototypePush,
Error, Error,
ErrorCaptureStackTrace, ErrorCaptureStackTrace,
FunctionPrototypeBind, FunctionPrototypeBind,
@ -61,6 +62,7 @@ const {
validateNumber, validateNumber,
validateString, validateString,
validateOneOf, validateOneOf,
validateObject,
} = require('internal/validators'); } = require('internal/validators');
const { const {
isReadableStream, isReadableStream,
@ -74,11 +76,13 @@ function lazyUtilColors() {
utilColors ??= require('internal/util/colors'); utilColors ??= require('internal/util/colors');
return utilColors; return utilColors;
} }
const { getOptionValue } = require('internal/options');
const binding = internalBinding('util'); const binding = internalBinding('util');
const { const {
deprecate, deprecate,
getLazy,
getSystemErrorMap, getSystemErrorMap,
getSystemErrorName: internalErrorName, getSystemErrorName: internalErrorName,
getSystemErrorMessage: internalErrorMessage, getSystemErrorMessage: internalErrorMessage,
@ -328,14 +332,90 @@ function parseEnv(content) {
return binding.parseEnv(content); return binding.parseEnv(content);
} }
const lazySourceMap = getLazy(() => require('internal/source_map/source_map_cache'));
/**
* @typedef {object} CallSite // The call site
* @property {string} scriptName // The name of the resource that contains the
* script for the function for this StackFrame
* @property {string} functionName // The name of the function associated with this stack frame
* @property {number} lineNumber // The number, 1-based, of the line for the associate function call
* @property {number} columnNumber // The 1-based column offset on the line for the associated function call
*/
/**
* @param {CallSite} callSite // The call site object to reconstruct from source map
* @returns {CallSite | undefined} // The reconstructed call site object
*/
function reconstructCallSite(callSite) {
const { scriptName, lineNumber, column } = callSite;
const sourceMap = lazySourceMap().findSourceMap(scriptName);
if (!sourceMap) return;
const entry = sourceMap.findEntry(lineNumber - 1, column - 1);
if (!entry?.originalSource) return;
return {
__proto__: null,
// If the name is not found, it is an empty string to match the behavior of `util.getCallSite()`
functionName: entry.name ?? '',
scriptName: entry.originalSource,
lineNumber: entry.originalLine + 1,
column: entry.originalColumn + 1,
};
}
/**
*
* The call site array to map
* @param {CallSite[]} callSites
* Array of objects with the reconstructed call site
* @returns {CallSite[]}
*/
function mapCallSite(callSites) {
const result = [];
for (let i = 0; i < callSites.length; ++i) {
const callSite = callSites[i];
const found = reconstructCallSite(callSite);
ArrayPrototypePush(result, found ?? callSite);
}
return result;
}
/**
* @typedef {object} CallSiteOptions // The call site options
* @property {boolean} sourceMap // Enable source map support
*/
/** /**
* Returns the callSite * Returns the callSite
* @param {number} frameCount * @param {number} frameCount
* @returns {object} * @param {CallSiteOptions} options
* @returns {CallSite[]}
*/ */
function getCallSites(frameCount = 10) { function getCallSites(frameCount = 10, options) {
// If options is not provided check if frameCount is an object
if (options === undefined) {
if (typeof frameCount === 'object') {
// If frameCount is an object, it is the options object
options = frameCount;
validateObject(options, 'options');
validateBoolean(options.sourceMap, 'options.sourceMap');
frameCount = 10;
} else {
// If options is not provided, set it to an empty object
options = {};
};
} else {
// If options is provided, validate it
validateObject(options, 'options');
validateBoolean(options.sourceMap, 'options.sourceMap');
}
// Using kDefaultMaxCallStackSizeToCapture as reference // Using kDefaultMaxCallStackSizeToCapture as reference
validateNumber(frameCount, 'frameCount', 1, 200); validateNumber(frameCount, 'frameCount', 1, 200);
// If options.sourceMaps is true or if sourceMaps are enabled but the option.sourceMaps is not set explictly to false
if (options.sourceMap === true || (getOptionValue('--enable-source-maps') && options.sourceMap !== false)) {
return mapCallSite(binding.getCallSites(frameCount));
}
return binding.getCallSites(frameCount); return binding.getCallSites(frameCount);
}; };

View File

@ -0,0 +1,10 @@
const { getCallSites } = require('node:util');
interface CallSite {
A;
B;
}
const callSite = getCallSites({ sourceMap: false })[0];
console.log('mapCallSite: ', callSite);

View File

@ -0,0 +1,10 @@
const { getCallSites } = require('node:util');
interface CallSite {
A;
B;
}
const callSite = getCallSites()[0];
console.log('getCallSite: ', callSite);

View File

@ -53,7 +53,17 @@ const assert = require('node:assert');
code: 'ERR_OUT_OF_RANGE' code: 'ERR_OUT_OF_RANGE'
})); }));
assert.throws(() => { assert.throws(() => {
getCallSites({}); getCallSites([]);
}, common.expectsError({
code: 'ERR_INVALID_ARG_TYPE'
}));
assert.throws(() => {
getCallSites({}, {});
}, common.expectsError({
code: 'ERR_INVALID_ARG_TYPE'
}));
assert.throws(() => {
getCallSites(10, 10);
}, common.expectsError({ }, common.expectsError({
code: 'ERR_INVALID_ARG_TYPE' code: 'ERR_INVALID_ARG_TYPE'
})); }));
@ -104,3 +114,51 @@ const assert = require('node:assert');
assert.notStrictEqual(callSites.length, 0); assert.notStrictEqual(callSites.length, 0);
Error.stackTraceLimit = originalStackTraceLimit; Error.stackTraceLimit = originalStackTraceLimit;
} }
{
const { status, stderr, stdout } = spawnSync(process.execPath, [
'--no-warnings',
'--experimental-transform-types',
fixtures.path('typescript/ts/test-get-callsite.ts'),
]);
const output = stdout.toString();
assert.strictEqual(stderr.toString(), '');
assert.match(output, /lineNumber: 8/);
assert.match(output, /column: 18/);
assert.match(output, /test-get-callsite\.ts/);
assert.strictEqual(status, 0);
}
{
const { status, stderr, stdout } = spawnSync(process.execPath, [
'--no-warnings',
'--experimental-transform-types',
'--no-enable-source-maps',
fixtures.path('typescript/ts/test-get-callsite.ts'),
]);
const output = stdout.toString();
assert.strictEqual(stderr.toString(), '');
// Line should be wrong when sourcemaps are disable
assert.match(output, /lineNumber: 2/);
assert.match(output, /column: 18/);
assert.match(output, /test-get-callsite\.ts/);
assert.strictEqual(status, 0);
}
{
// Source maps should be disabled when options.sourceMap is false
const { status, stderr, stdout } = spawnSync(process.execPath, [
'--no-warnings',
'--experimental-transform-types',
fixtures.path('typescript/ts/test-get-callsite-explicit.ts'),
]);
const output = stdout.toString();
assert.strictEqual(stderr.toString(), '');
assert.match(output, /lineNumber: 2/);
assert.match(output, /column: 18/);
assert.match(output, /test-get-callsite-explicit\.ts/);
assert.strictEqual(status, 0);
}