From a9977d85e6b18a1fc3ae38eb65df8d67209c4cfb 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/util.md | 5 +- lib/util.js | 70 ++++++++++++++++++- .../ts/test-get-callsite-explicit.ts | 10 +++ .../typescript/ts/test-get-callsite.ts | 10 +++ test/parallel/test-util-getCallSite.js | 60 +++++++++++++++- 5 files changed, 152 insertions(+), 3 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/util.md b/doc/api/util.md index 12527b794d0948..6e40919871c764 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.getCallSite(frames)` +## `util.getCallSite(frames, options)` > Stability: 1.1 - Active development @@ -374,6 +374,9 @@ added: v22.9.0 * `frames` {number} Number of frames returned in the stacktrace. **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 stacktrace objects * `functionName` {string} Returns the name of the function associated with this stack frame. * `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 6034e1af3e56d1..05480dada47ff3 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,78 @@ 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 object or array of object to map (ex `util.getCallSite()`) + * @param {CallSite | CallSite[]} callSites + * An object or array of objects with the reconstructed call site + * @returns {CallSite | CallSite[]} + */ +function mapCallSite(callSites) { + if (ArrayIsArray(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; + } + return reconstructCallSite(callSites) ?? callSites; +} + /** * Returns the callSite * @param {number} frames * @returns {object} */ -function getCallSite(frames = 10) { +function getCallSite(frames = 10, options) { + if (options === undefined) { + if (typeof frames === 'object') { + options = frames; + frames = 10; + } else { + options = {}; + }; + } // Using kDefaultMaxCallStackSizeToCapture as reference validateNumber(frames, 'frames', 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.getCallSite(frames)); + } return binding.getCallSite(frames); }; 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..e0e0f6383d5453 --- /dev/null +++ b/test/fixtures/typescript/ts/test-get-callsite-explicit.ts @@ -0,0 +1,10 @@ +const { getCallSite } = require('node:util'); + +interface CallSite { + A; + B; +} + +const callSite = getCallSite({ 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..7ac04eb0575299 --- /dev/null +++ b/test/fixtures/typescript/ts/test-get-callsite.ts @@ -0,0 +1,10 @@ +const { getCallSite } = require('node:util'); + +interface CallSite { + A; + B; +} + +const callSite = getCallSite()[0]; + +console.log('getCallSite: ', callSite); diff --git a/test/parallel/test-util-getCallSite.js b/test/parallel/test-util-getCallSite.js index ae862e2b278401..e66f5c43d59ad3 100644 --- a/test/parallel/test-util-getCallSite.js +++ b/test/parallel/test-util-getCallSite.js @@ -53,7 +53,17 @@ const assert = require('node:assert'); code: 'ERR_OUT_OF_RANGE' })); assert.throws(() => { - getCallSite({}); + getCallSite([]); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE' + })); + assert.throws(() => { + getCallSite({}, {}); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE' + })); + assert.throws(() => { + getCallSite(10, 10); }, common.expectsError({ code: 'ERR_INVALID_ARG_TYPE' })); @@ -104,3 +114,51 @@ const assert = require('node:assert'); assert.notStrictEqual(callsite.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); +}