Skip to content

Commit ee44447

Browse files
bcoetargos
authored andcommitted
errors: display original symbol name
If symbol names array has been populated in source map, include original symbol name in error message. Fixes #35325 PR-URL: #36042 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 6adec63 commit ee44447

14 files changed

+178
-66
lines changed

doc/api/module.md

+1
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ consists of the following keys:
206206
* originalSource: {string}
207207
* originalLine: {number}
208208
* originalColumn: {number}
209+
* name: {string}
209210
210211
[CommonJS]: modules.md
211212
[ES Modules]: esm.md

lib/internal/source_map/prepare_stack_trace.js

+81-43
Original file line numberDiff line numberDiff line change
@@ -47,45 +47,48 @@ const prepareStackTrace = (globalThis, error, trace) => {
4747
}
4848

4949
let errorSource = '';
50-
let firstLine;
51-
let firstColumn;
50+
let lastSourceMap;
51+
let lastFileName;
5252
const preparedTrace = ArrayPrototypeJoin(ArrayPrototypeMap(trace, (t, i) => {
53-
if (i === 0) {
54-
firstLine = t.getLineNumber();
55-
firstColumn = t.getColumnNumber();
56-
}
5753
let str = i !== 0 ? '\n at ' : '';
5854
str = `${str}${t}`;
5955
try {
60-
const sm = findSourceMap(t.getFileName());
56+
// A stack trace will often have several call sites in a row within the
57+
// same file, cache the source map and file content accordingly:
58+
const fileName = t.getFileName();
59+
const sm = fileName === lastFileName ?
60+
lastSourceMap :
61+
findSourceMap(fileName);
62+
lastSourceMap = sm;
63+
lastFileName = fileName;
6164
if (sm) {
62-
// Source Map V3 lines/columns use zero-based offsets whereas, in
63-
// stack traces, they start at 1/1.
65+
// Source Map V3 lines/columns start at 0/0 whereas stack traces
66+
// start at 1/1:
6467
const {
6568
originalLine,
6669
originalColumn,
67-
originalSource
70+
originalSource,
6871
} = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
6972
if (originalSource && originalLine !== undefined &&
7073
originalColumn !== undefined) {
74+
const name = getOriginalSymbolName(sm, trace, i);
7175
if (i === 0) {
72-
firstLine = originalLine + 1;
73-
firstColumn = originalColumn + 1;
74-
75-
// Show error in original source context to help user pinpoint it:
7676
errorSource = getErrorSource(
77-
sm.payload,
77+
sm,
7878
originalSource,
79-
firstLine,
80-
firstColumn
79+
originalLine,
80+
originalColumn
8181
);
8282
}
8383
// Show both original and transpiled stack trace information:
84+
const prefix = (name && name !== t.getFunctionName()) ?
85+
`\n -> at ${name}` :
86+
'\n ->';
8487
const originalSourceNoScheme =
8588
StringPrototypeStartsWith(originalSource, 'file://') ?
8689
fileURLToPath(originalSource) : originalSource;
87-
str += `\n -> ${originalSourceNoScheme}:${originalLine + 1}:` +
88-
`${originalColumn + 1}`;
90+
str += `${prefix} (${originalSourceNoScheme}:${originalLine + 1}:` +
91+
`${originalColumn + 1})`;
8992
}
9093
}
9194
} catch (err) {
@@ -96,18 +99,69 @@ const prepareStackTrace = (globalThis, error, trace) => {
9699
return `${errorSource}${errorString}\n at ${preparedTrace}`;
97100
};
98101

102+
// Transpilers may have removed the original symbol name used in the stack
103+
// trace, if possible restore it from the names field of the source map:
104+
function getOriginalSymbolName(sourceMap, trace, curIndex) {
105+
// First check for a symbol name associated with the enclosing function:
106+
const enclosingEntry = sourceMap.findEntry(
107+
trace[curIndex].getEnclosingLineNumber() - 1,
108+
trace[curIndex].getEnclosingColumnNumber() - 1
109+
);
110+
if (enclosingEntry.name) return enclosingEntry.name;
111+
// Fallback to using the symbol name attached to the next stack frame:
112+
const currentFileName = trace[curIndex].getFileName();
113+
const nextCallSite = trace[curIndex + 1];
114+
if (nextCallSite && currentFileName === nextCallSite.getFileName()) {
115+
const { name } = sourceMap.findEntry(
116+
nextCallSite.getLineNumber() - 1,
117+
nextCallSite.getColumnNumber() - 1
118+
);
119+
return name;
120+
}
121+
}
122+
99123
// Places a snippet of code from where the exception was originally thrown
100124
// above the stack trace. This logic is modeled after GetErrorSource in
101125
// node_errors.cc.
102-
function getErrorSource(payload, originalSource, firstLine, firstColumn) {
126+
function getErrorSource(
127+
sourceMap,
128+
originalSourcePath,
129+
originalLine,
130+
originalColumn
131+
) {
103132
let exceptionLine = '';
104-
const originalSourceNoScheme =
105-
StringPrototypeStartsWith(originalSource, 'file://') ?
106-
fileURLToPath(originalSource) : originalSource;
133+
const originalSourcePathNoScheme =
134+
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
135+
fileURLToPath(originalSourcePath) : originalSourcePath;
136+
const source = getOriginalSource(
137+
sourceMap.payload,
138+
originalSourcePathNoScheme
139+
);
140+
const lines = StringPrototypeSplit(source, /\r?\n/, originalLine + 1);
141+
const line = lines[originalLine];
142+
if (!line) return exceptionLine;
143+
144+
// Display ^ in appropriate position, regardless of whether tabs or
145+
// spaces are used:
146+
let prefix = '';
147+
for (const character of StringPrototypeSlice(line, 0, originalColumn + 1)) {
148+
prefix += (character === '\t') ? '\t' :
149+
StringPrototypeRepeat(' ', getStringWidth(character));
150+
}
151+
prefix = StringPrototypeSlice(prefix, 0, -1); // The last character is '^'.
107152

153+
exceptionLine =
154+
`${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n\n`;
155+
return exceptionLine;
156+
}
157+
158+
function getOriginalSource(payload, originalSourcePath) {
108159
let source;
160+
const originalSourcePathNoScheme =
161+
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
162+
fileURLToPath(originalSourcePath) : originalSourcePath;
109163
const sourceContentIndex =
110-
ArrayPrototypeIndexOf(payload.sources, originalSource);
164+
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
111165
if (payload.sourcesContent?.[sourceContentIndex]) {
112166
// First we check if the original source content was provided in the
113167
// source map itself:
@@ -116,29 +170,13 @@ function getErrorSource(payload, originalSource, firstLine, firstColumn) {
116170
// If no sourcesContent was found, attempt to load the original source
117171
// from disk:
118172
try {
119-
source = readFileSync(originalSourceNoScheme, 'utf8');
173+
source = readFileSync(originalSourcePathNoScheme, 'utf8');
120174
} catch (err) {
121175
debug(err);
122-
return '';
176+
source = '';
123177
}
124178
}
125-
126-
const lines = StringPrototypeSplit(source, /\r?\n/, firstLine);
127-
const line = lines[firstLine - 1];
128-
if (!line) return exceptionLine;
129-
130-
// Display ^ in appropriate position, regardless of whether tabs or
131-
// spaces are used:
132-
let prefix = '';
133-
for (const character of StringPrototypeSlice(line, 0, firstColumn)) {
134-
prefix += (character === '\t') ? '\t' :
135-
StringPrototypeRepeat(' ', getStringWidth(character));
136-
}
137-
prefix = StringPrototypeSlice(prefix, 0, -1); // The last character is '^'.
138-
139-
exceptionLine =
140-
`${originalSourceNoScheme}:${firstLine}\n${line}\n${prefix}^\n\n`;
141-
return exceptionLine;
179+
return source;
142180
}
143181

144182
module.exports = {

lib/internal/source_map/source_map.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ class SourceMap {
203203
generatedColumn: entry[1],
204204
originalSource: entry[2],
205205
originalLine: entry[3],
206-
originalColumn: entry[4]
206+
originalColumn: entry[4],
207+
name: entry[5],
207208
};
208209
}
209210

@@ -214,6 +215,7 @@ class SourceMap {
214215
let sourceIndex = 0;
215216
let sourceLineNumber = 0;
216217
let sourceColumnNumber = 0;
218+
let nameIndex = 0;
217219

218220
const sources = [];
219221
const originalToCanonicalURLMap = {};
@@ -229,7 +231,6 @@ class SourceMap {
229231

230232
const stringCharIterator = new StringCharIterator(map.mappings);
231233
let sourceURL = sources[sourceIndex];
232-
233234
while (true) {
234235
if (stringCharIterator.peek() === ',')
235236
stringCharIterator.next();
@@ -256,12 +257,15 @@ class SourceMap {
256257
}
257258
sourceLineNumber += decodeVLQ(stringCharIterator);
258259
sourceColumnNumber += decodeVLQ(stringCharIterator);
259-
if (!isSeparator(stringCharIterator.peek()))
260-
// Unused index into the names list.
261-
decodeVLQ(stringCharIterator);
260+
261+
let name;
262+
if (!isSeparator(stringCharIterator.peek())) {
263+
nameIndex += decodeVLQ(stringCharIterator);
264+
name = map.names?.[nameIndex];
265+
}
262266

263267
this.#mappings.push([lineNumber, columnNumber, sourceURL,
264-
sourceLineNumber, sourceColumnNumber]);
268+
sourceLineNumber, sourceColumnNumber, name]);
265269
}
266270
};
267271
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
var functionA=function(){functionB()};function functionB(){functionC()}var functionC=function(){functionD()},functionD=function(){if(0<Math.random())throw Error("an error!");},thrower=functionA;try{functionA()}catch(a){throw a;};
2+
//# sourceMappingURL=enclosing-call-site.js.map
3+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const functionA = () => {
2+
functionB()
3+
}
4+
5+
function functionB() {
6+
functionC()
7+
}
8+
9+
const functionC = () => {
10+
functionD()
11+
}
12+
13+
const functionD = () => {
14+
(function functionE () {
15+
if (Math.random() > 0) {
16+
throw new Error('an error!')
17+
}
18+
})()
19+
}
20+
21+
const thrower = functionA
22+
23+
try {
24+
thrower()
25+
} catch (err) {
26+
throw err
27+
}

test/fixtures/source-map/enclosing-call-site.js.map

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Flags: --enable-source-maps
2+
3+
'use strict';
4+
require('../common');
5+
require('../fixtures/source-map/enclosing-call-site-min.js');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
*enclosing-call-site.js:16
2+
throw new Error('an error!')
3+
^
4+
5+
Error: an error!
6+
at functionD (*enclosing-call-site-min.js:1:156)
7+
-> (*enclosing-call-site.js:16:17)
8+
at functionC (*enclosing-call-site-min.js:1:97)
9+
-> (*enclosing-call-site.js:10:3)
10+
at functionB (*enclosing-call-site-min.js:1:60)
11+
-> (*enclosing-call-site.js:6:3)
12+
at functionA (*enclosing-call-site-min.js:1:26)
13+
-> (*enclosing-call-site.js:2:3)
14+
at Object.<anonymous> (*enclosing-call-site-min.js:1:199)
15+
-> (*enclosing-call-site.js:24:3)
16+
at Module._compile (internal/modules/cjs/loader.js:*)
17+
at Object.Module._extensions..js (internal/modules/cjs/loader.js:*)
18+
at Module.load (internal/modules/cjs/loader.js:*)
19+
at Function.Module._load (internal/modules/cjs/loader.js:*)
20+
at Module.require (internal/modules/cjs/loader.js:*)

test/message/source_map_reference_error_tabs.out

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
ReferenceError: alert is not defined
66
at Object.<anonymous> (*tabs.coffee:39:5)
7-
-> *tabs.coffee:26:2
7+
-> *tabs.coffee:26:2*
88
at Object.<anonymous> (*tabs.coffee:53:4)
9-
-> *tabs.coffee:1:14
9+
-> *tabs.coffee:1:14*
1010
at Module._compile (internal/modules/cjs/loader.js:*
1111
at Object.Module._extensions..js (internal/modules/cjs/loader.js:*
1212
at Module.load (internal/modules/cjs/loader.js:*

test/message/source_map_throw_catch.out

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ reachable
44
^
55
Error: an exception
66
at branch (*typescript-throw.js:20:15)
7-
-> *typescript-throw.ts:18:11
7+
-> *typescript-throw.ts:18:11*
88
at Object.<anonymous> (*typescript-throw.js:26:1)
9-
-> *typescript-throw.ts:24:1
9+
-> *typescript-throw.ts:24:1*
1010
at Module._compile (internal/modules/cjs/loader.js:*)
1111
at Object.Module._extensions..js (internal/modules/cjs/loader.js:*)
1212
at Module.load (internal/modules/cjs/loader.js:*)

test/message/source_map_throw_first_tick.out

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ reachable
44
^
55
Error: an exception
66
at branch (*typescript-throw.js:20:15)
7-
-> *typescript-throw.ts:18:11
7+
-> *typescript-throw.ts:18:11*
88
at Object.<anonymous> (*typescript-throw.js:26:1)
9-
-> *typescript-throw.ts:24:1
9+
-> *typescript-throw.ts:24:1*
1010
at Module._compile (internal/modules/cjs/loader.js:*)
1111
at Object.Module._extensions..js (internal/modules/cjs/loader.js:*)
1212
at Module.load (internal/modules/cjs/loader.js:*)

test/message/source_map_throw_icu.out

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
Error: an error
66
at Object.createElement (*icu.js:5:7)
7-
-> *icu.jsx:3:23
7+
-> *icu.jsx:3:23*
88
at Object.<anonymous> (*icu.js:8:82)
9-
-> *icu.jsx:9:5
9+
-> *icu.jsx:9:5*
1010
at Module._compile (internal/modules/cjs/loader.js:*
1111
at Object.Module._extensions..js (internal/modules/cjs/loader.js:*
1212
at Module.load (internal/modules/cjs/loader.js:*

test/message/source_map_throw_set_immediate.out

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Error: goodbye
66
at *uglify-throw.js:1:43
7-
-> *uglify-throw-original.js:5:9
7+
-> at Hello *uglify-throw-original.js:5:9*
88
at Immediate.<anonymous> (*uglify-throw.js:1:60)
9-
-> *uglify-throw-original.js:9:3
9+
-> *uglify-throw-original.js:9:3*
1010
at processImmediate (internal/timers.js:*)

test/parallel/test-source-map-enable.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,15 @@ function nextdir() {
170170
'--enable-source-maps',
171171
require.resolve('../fixtures/source-map/uglify-throw.js')
172172
]);
173-
assert.ok(
174-
output.stderr.toString().match(/->.*uglify-throw-original\.js:5:9/)
173+
assert.match(
174+
output.stderr.toString(),
175+
/->.*uglify-throw-original\.js:5:9/
175176
);
176-
assert.ok(
177-
output.stderr.toString().match(/->.*uglify-throw-original\.js:9:3/)
177+
assert.match(
178+
output.stderr.toString(),
179+
/->.*uglify-throw-original\.js:9:3/
178180
);
181+
assert.match(output.stderr.toString(), /at Hello/);
179182
}
180183

181184
// Applies source-maps generated by tsc to stack trace.
@@ -276,11 +279,14 @@ function nextdir() {
276279
require.resolve('../fixtures/source-map/webpack.js')
277280
]);
278281
// Error in original context of source content:
279-
assert.ok(
280-
output.stderr.toString().match(/throw new Error\('oh no!'\)\r?\n.*\^/)
282+
assert.match(
283+
output.stderr.toString(),
284+
/throw new Error\('oh no!'\)\r?\n.*\^/
281285
);
282286
// Rewritten stack trace:
283-
assert.ok(output.stderr.toString().includes('webpack:///webpack.js:14:9'));
287+
assert.match(output.stderr.toString(), /webpack:\/\/\/webpack\.js:14:9/);
288+
assert.match(output.stderr.toString(), /at functionD.*14:9/);
289+
assert.match(output.stderr.toString(), /at functionC.*10:3/);
284290
}
285291

286292
// Stores and applies source map associated with file that throws while

0 commit comments

Comments
 (0)