From 20bd8b79a5de4720f1e95b22691c22d4dc07e2b2 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Mon, 28 Oct 2024 10:52:45 -0500 Subject: [PATCH] util: add sourcemap support to getCallSite --- doc/api/deprecations.md | 2 +- doc/api/util.md | 5 +- lib/util.js | 75 ++++++++++++++++++- .../ts/test-get-callsite-explicit.ts | 10 +++ .../typescript/ts/test-get-callsite.ts | 10 +++ test/parallel/test-util-getcallsites.js | 60 ++++++++++++++- 6 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 test/fixtures/typescript/ts/test-get-callsite-explicit.ts create mode 100644 test/fixtures/typescript/ts/test-get-callsite.ts diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 38e1b6efc9aafd..7d2dc63b28b902 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -3899,7 +3899,7 @@ The `util.getCallSite` API has been removed. Please use [`util.getCallSites()`][ [`url.parse()`]: url.md#urlparseurlstring-parsequerystring-slashesdenotehost [`url.resolve()`]: url.md#urlresolvefrom-to [`util._extend()`]: util.md#util_extendtarget-source -[`util.getCallSites()`]: util.md#utilgetcallsitesframecount +[`util.getCallSites()`]: util.md#utilgetcallsitesframecount-options [`util.getSystemErrorName()`]: util.md#utilgetsystemerrornameerr [`util.inspect()`]: util.md#utilinspectobject-options [`util.inspect.custom`]: util.md#utilinspectcustom diff --git a/doc/api/util.md b/doc/api/util.md index 0b3326e87e209c..127133df2cee55 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -364,7 +364,7 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 }); // when printed to a terminal. ``` -## `util.getCallSites(frameCount)` +## `util.getCallSites(frameCount, options)` > Stability: 1.1 - Active development @@ -374,6 +374,9 @@ added: v22.9.0 * `frameCount` {number} Number of frames to capture as call site objects. **Default:** `10`. Allowable range is between 1 and 200. +* `options` {Object} + * `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 * `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 diff --git a/lib/util.js b/lib/util.js index b2b280f6a567cd..d8f2b3ac582864 100644 --- a/lib/util.js +++ b/lib/util.js @@ -24,6 +24,7 @@ const { ArrayIsArray, ArrayPrototypePop, + ArrayPrototypePush, Error, ErrorCaptureStackTrace, FunctionPrototypeBind, @@ -61,6 +62,7 @@ const { validateNumber, validateString, validateOneOf, + validateObject, } = require('internal/validators'); const { isReadableStream, @@ -74,11 +76,13 @@ function lazyUtilColors() { utilColors ??= require('internal/util/colors'); return utilColors; } +const { getOptionValue } = require('internal/options'); const binding = internalBinding('util'); const { deprecate, + getLazy, getSystemErrorMap, getSystemErrorName: internalErrorName, getSystemErrorMessage: internalErrorMessage, @@ -328,14 +332,81 @@ function 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 * @param {number} frameCount - * @returns {object} + * @param {CallSiteOptions} options + * @returns {CallSite[]} */ -function getCallSites(frameCount = 10) { +function getCallSites(frameCount = 10, options) { + if (options === undefined) { + if (typeof frameCount === 'object') { + options = frameCount; + frameCount = 10; + } else { + options = {}; + }; + } // Using kDefaultMaxCallStackSizeToCapture as reference validateNumber(frameCount, 'frameCount', 1, 200); + validateObject(options, 'options'); + // 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); }; diff --git a/test/fixtures/typescript/ts/test-get-callsite-explicit.ts b/test/fixtures/typescript/ts/test-get-callsite-explicit.ts new file mode 100644 index 00000000000000..def07baca23659 --- /dev/null +++ b/test/fixtures/typescript/ts/test-get-callsite-explicit.ts @@ -0,0 +1,10 @@ +const { getCallSites } = require('node:util'); + +interface CallSite { + A; + B; +} + +const callSite = getCallSites({ sourceMap: false })[0]; + +console.log('mapCallSite: ', callSite); diff --git a/test/fixtures/typescript/ts/test-get-callsite.ts b/test/fixtures/typescript/ts/test-get-callsite.ts new file mode 100644 index 00000000000000..959fdd1e6d5216 --- /dev/null +++ b/test/fixtures/typescript/ts/test-get-callsite.ts @@ -0,0 +1,10 @@ +const { getCallSites } = require('node:util'); + +interface CallSite { + A; + B; +} + +const callSite = getCallSites()[0]; + +console.log('getCallSite: ', callSite); diff --git a/test/parallel/test-util-getcallsites.js b/test/parallel/test-util-getcallsites.js index 47f21b7e73b909..1f6dc238369b08 100644 --- a/test/parallel/test-util-getcallsites.js +++ b/test/parallel/test-util-getcallsites.js @@ -53,7 +53,17 @@ const assert = require('node:assert'); code: 'ERR_OUT_OF_RANGE' })); 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({ code: 'ERR_INVALID_ARG_TYPE' })); @@ -104,3 +114,51 @@ const assert = require('node:assert'); assert.notStrictEqual(callSites.length, 0); 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, /typescript\/ts\/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, /typescript\/ts\/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, /typescript\/ts\/test-get-callsite-explicit\.ts/); + assert.strictEqual(status, 0); +}