Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v22.x backport] util: add sourcemap support to getCallSites #56209

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
// when printed to a terminal.
```

## `util.getCallSites(frameCount)`
## `util.getCallSites(frameCountOrOptions, [options])`

> Stability: 1.1 - Active development

Expand All @@ -376,8 +376,11 @@ changes:
description: The API is renamed from `util.getCallSite` to `util.getCallSites()`.
-->

* `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.
* `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
* `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
Expand Down Expand Up @@ -425,6 +428,33 @@ function 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)`

<!-- YAML
Expand Down
84 changes: 82 additions & 2 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
ArrayIsArray,
ArrayPrototypeJoin,
ArrayPrototypePop,
ArrayPrototypePush,
Date,
DatePrototypeGetDate,
DatePrototypeGetHours,
Expand Down Expand Up @@ -70,6 +71,7 @@ const {
validateNumber,
validateString,
validateOneOf,
validateObject,
} = require('internal/validators');
const { isBuffer } = require('buffer').Buffer;
const {
Expand All @@ -84,11 +86,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,
Expand Down Expand Up @@ -472,14 +476,90 @@ 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 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
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);
};

Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/typescript/ts/test-get-callsite-explicit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { getCallSites } = require('node:util');

interface CallSite {
A;
B;
}

const callSite = getCallSites({ sourceMap: false })[0];

console.log('mapCallSite: ', callSite);
10 changes: 10 additions & 0 deletions test/fixtures/typescript/ts/test-get-callsite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { getCallSites } = require('node:util');

interface CallSite {
A;
B;
}

const callSite = getCallSites()[0];

console.log('getCallSite: ', callSite);
60 changes: 59 additions & 1 deletion test/parallel/test-util-getcallsites.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}));
Expand Down Expand Up @@ -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, /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);
}
Loading