Skip to content

Commit bcfe9c8

Browse files
marco-ippolitoruyadorno
authored andcommitted
util: add sourcemap support to getCallSites
PR-URL: #55589 Backport-PR-URL: #56209 Fixes: #55109 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent 359fff1 commit bcfe9c8

5 files changed

Lines changed: 193 additions & 5 deletions

File tree

doc/api/util.md

Lines changed: 32 additions & 2 deletions
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.getCallSites(frameCount)`
367+
## `util.getCallSites(frameCountOrOptions, [options])`
368368

369369
> Stability: 1.1 - Active development
370370
@@ -376,8 +376,11 @@ changes:
376376
description: The API is renamed from `util.getCallSite` to `util.getCallSites()`.
377377
-->
378378

379-
* `frameCount` {number} Number of frames to capture as call site objects.
379+
* `frameCount` {number} Optional number of frames to capture as call site objects.
380380
**Default:** `10`. Allowable range is between 1 and 200.
381+
* `options` {Object} Optional
382+
* `sourceMap` {boolean} Reconstruct the original location in the stacktrace from the source-map.
383+
Enabled by default with the flag `--enable-source-maps`.
381384
* Returns: {Object\[]} An array of call site objects
382385
* `functionName` {string} Returns the name of the function associated with this call site.
383386
* `scriptName` {string} Returns the name of the resource that contains the script for the
@@ -425,6 +428,33 @@ function anotherFunction() {
425428
anotherFunction();
426429
```
427430

431+
It is possible to reconstruct the original locations by setting the option `sourceMap` to `true`.
432+
If the source map is not available, the original location will be the same as the current location.
433+
When the `--enable-source-maps` flag is enabled, for example when using `--experimental-transform-types`,
434+
`sourceMap` will be true by default.
435+
436+
```ts
437+
import util from 'node:util';
438+
439+
interface Foo {
440+
foo: string;
441+
}
442+
443+
const callSites = util.getCallSites({ sourceMap: true });
444+
445+
// With sourceMap:
446+
// Function Name: ''
447+
// Script Name: example.js
448+
// Line Number: 7
449+
// Column Number: 26
450+
451+
// Without sourceMap:
452+
// Function Name: ''
453+
// Script Name: example.js
454+
// Line Number: 2
455+
// Column Number: 26
456+
```
457+
428458
## `util.getSystemErrorName(err)`
429459

430460
<!-- YAML

lib/util.js

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
ArrayIsArray,
2626
ArrayPrototypeJoin,
2727
ArrayPrototypePop,
28+
ArrayPrototypePush,
2829
Date,
2930
DatePrototypeGetDate,
3031
DatePrototypeGetHours,
@@ -70,6 +71,7 @@ const {
7071
validateNumber,
7172
validateString,
7273
validateOneOf,
74+
validateObject,
7375
} = require('internal/validators');
7476
const { isBuffer } = require('buffer').Buffer;
7577
const {
@@ -84,11 +86,13 @@ function lazyUtilColors() {
8486
utilColors ??= require('internal/util/colors');
8587
return utilColors;
8688
}
89+
const { getOptionValue } = require('internal/options');
8790

8891
const binding = internalBinding('util');
8992

9093
const {
9194
deprecate,
95+
getLazy,
9296
getSystemErrorMap,
9397
getSystemErrorName: internalErrorName,
9498
getSystemErrorMessage: internalErrorMessage,
@@ -472,14 +476,90 @@ function parseEnv(content) {
472476
return binding.parseEnv(content);
473477
}
474478

479+
const lazySourceMap = getLazy(() => require('internal/source_map/source_map_cache'));
480+
481+
/**
482+
* @typedef {object} CallSite // The call site
483+
* @property {string} scriptName // The name of the resource that contains the
484+
* script for the function for this StackFrame
485+
* @property {string} functionName // The name of the function associated with this stack frame
486+
* @property {number} lineNumber // The number, 1-based, of the line for the associate function call
487+
* @property {number} columnNumber // The 1-based column offset on the line for the associated function call
488+
*/
489+
490+
/**
491+
* @param {CallSite} callSite // The call site object to reconstruct from source map
492+
* @returns {CallSite | undefined} // The reconstructed call site object
493+
*/
494+
function reconstructCallSite(callSite) {
495+
const { scriptName, lineNumber, column } = callSite;
496+
const sourceMap = lazySourceMap().findSourceMap(scriptName);
497+
if (!sourceMap) return;
498+
const entry = sourceMap.findEntry(lineNumber - 1, column - 1);
499+
if (!entry?.originalSource) return;
500+
return {
501+
__proto__: null,
502+
// If the name is not found, it is an empty string to match the behavior of `util.getCallSite()`
503+
functionName: entry.name ?? '',
504+
scriptName: entry.originalSource,
505+
lineNumber: entry.originalLine + 1,
506+
column: entry.originalColumn + 1,
507+
};
508+
}
509+
510+
/**
511+
*
512+
* The call site array to map
513+
* @param {CallSite[]} callSites
514+
* Array of objects with the reconstructed call site
515+
* @returns {CallSite[]}
516+
*/
517+
function mapCallSite(callSites) {
518+
const result = [];
519+
for (let i = 0; i < callSites.length; ++i) {
520+
const callSite = callSites[i];
521+
const found = reconstructCallSite(callSite);
522+
ArrayPrototypePush(result, found ?? callSite);
523+
}
524+
return result;
525+
}
526+
527+
/**
528+
* @typedef {object} CallSiteOptions // The call site options
529+
* @property {boolean} sourceMap // Enable source map support
530+
*/
531+
475532
/**
476533
* Returns the callSite
477534
* @param {number} frameCount
478-
* @returns {object}
535+
* @param {CallSiteOptions} options
536+
* @returns {CallSite[]}
479537
*/
480-
function getCallSites(frameCount = 10) {
538+
function getCallSites(frameCount = 10, options) {
539+
// If options is not provided check if frameCount is an object
540+
if (options === undefined) {
541+
if (typeof frameCount === 'object') {
542+
// If frameCount is an object, it is the options object
543+
options = frameCount;
544+
validateObject(options, 'options');
545+
validateBoolean(options.sourceMap, 'options.sourceMap');
546+
frameCount = 10;
547+
} else {
548+
// If options is not provided, set it to an empty object
549+
options = {};
550+
};
551+
} else {
552+
// If options is provided, validate it
553+
validateObject(options, 'options');
554+
validateBoolean(options.sourceMap, 'options.sourceMap');
555+
}
556+
481557
// Using kDefaultMaxCallStackSizeToCapture as reference
482558
validateNumber(frameCount, 'frameCount', 1, 200);
559+
// If options.sourceMaps is true or if sourceMaps are enabled but the option.sourceMaps is not set explictly to false
560+
if (options.sourceMap === true || (getOptionValue('--enable-source-maps') && options.sourceMap !== false)) {
561+
return mapCallSite(binding.getCallSites(frameCount));
562+
}
483563
return binding.getCallSites(frameCount);
484564
};
485565