Skip to content

Commit 9a85066

Browse files
committed
lib: add source map support for assert messages
Map source lines in assert messages with cached source maps.
1 parent 23a5714 commit 9a85066

14 files changed

+135
-59
lines changed

lib/internal/errors/error_source.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ const {
88
const {
99
getErrorSourcePositions,
1010
} = internalBinding('errors');
11+
const {
12+
getSourceMapsSupport,
13+
findSourceMap,
14+
getSourceLine,
15+
} = require('internal/source_map/source_map_cache');
1116

1217
/**
13-
* Get the source location of an error.
18+
* Get the source location of an error. If source map is enabled, resolve the source location
19+
* based on the source map.
1420
*
1521
* The `error.stack` must not have been accessed. The resolution is based on the structured
1622
* error stack data.
@@ -21,10 +27,35 @@ function getErrorSourceLocation(error) {
2127
const pos = getErrorSourcePositions(error);
2228
const {
2329
sourceLine,
30+
scriptResourceName,
31+
lineNumber,
2432
startColumn,
2533
} = pos;
2634

27-
return { sourceLine, startColumn };
35+
// Source map is not enabled. Return the source line directly.
36+
if (!getSourceMapsSupport().enabled) {
37+
return { sourceLine, startColumn };
38+
}
39+
40+
const sm = findSourceMap(scriptResourceName);
41+
if (sm === undefined) {
42+
return;
43+
}
44+
const {
45+
originalLine,
46+
originalColumn,
47+
originalSource,
48+
} = sm.findEntry(lineNumber - 1, startColumn);
49+
const originalSourceLine = getSourceLine(sm, originalSource, originalLine, originalColumn);
50+
51+
if (!originalSourceLine) {
52+
return;
53+
}
54+
55+
return {
56+
sourceLine: originalSourceLine,
57+
startColumn: originalColumn,
58+
};
2859
}
2960

3061
const memberAccessTokens = [ '.', '?.', '[', ']' ];
@@ -103,7 +134,8 @@ function getFirstExpression(code, startColumn) {
103134
}
104135

105136
/**
106-
* Get the source expression of an error.
137+
* Get the source expression of an error. If source map is enabled, resolve the source location
138+
* based on the source map.
107139
*
108140
* The `error.stack` must not have been accessed, or the source location may be incorrect. The
109141
* resolution is based on the structured error stack data.

lib/internal/source_map/prepare_stack_trace.js

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
'use strict';
22

33
const {
4-
ArrayPrototypeIndexOf,
54
ArrayPrototypeJoin,
65
ArrayPrototypeMap,
76
ErrorPrototypeToString,
8-
RegExpPrototypeSymbolSplit,
97
SafeStringIterator,
108
StringPrototypeRepeat,
119
StringPrototypeSlice,
@@ -16,8 +14,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
1614
debug = fn;
1715
});
1816
const { getStringWidth } = require('internal/util/inspect');
19-
const { readFileSync } = require('fs');
20-
const { findSourceMap } = require('internal/source_map/source_map_cache');
17+
const { findSourceMap, getSourceLine } = require('internal/source_map/source_map_cache');
2118
const {
2219
kIsNodeError,
2320
} = require('internal/errors');
@@ -155,21 +152,13 @@ function getErrorSource(
155152
originalLine,
156153
originalColumn,
157154
) {
158-
const originalSourcePathNoScheme =
159-
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
160-
fileURLToPath(originalSourcePath) : originalSourcePath;
161-
const source = getOriginalSource(
162-
sourceMap.payload,
163-
originalSourcePath,
164-
);
165-
if (typeof source !== 'string') {
166-
return;
167-
}
168-
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
169-
const line = lines[originalLine];
155+
const line = getSourceLine(sourceMap, originalSourcePath, originalLine);
170156
if (!line) {
171157
return;
172158
}
159+
const originalSourcePathNoScheme =
160+
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
161+
fileURLToPath(originalSourcePath) : originalSourcePath;
173162

174163
// Display ^ in appropriate position, regardless of whether tabs or
175164
// spaces are used:
@@ -182,39 +171,10 @@ function getErrorSource(
182171
prefix = StringPrototypeSlice(prefix, 0, -1); // The last character is '^'.
183172

184173
const exceptionLine =
185-
`${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n\n`;
174+
`${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n`;
186175
return exceptionLine;
187176
}
188177

189-
/**
190-
* Retrieve the original source code from the source map's `sources` list or disk.
191-
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
192-
* @param {string} originalSourcePath - path or url of the original source
193-
* @returns {string | undefined} - the source content or undefined if file not found
194-
*/
195-
function getOriginalSource(payload, originalSourcePath) {
196-
let source;
197-
// payload.sources has been normalized to be an array of absolute urls.
198-
const sourceContentIndex =
199-
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
200-
if (payload.sourcesContent?.[sourceContentIndex]) {
201-
// First we check if the original source content was provided in the
202-
// source map itself:
203-
source = payload.sourcesContent[sourceContentIndex];
204-
} else if (StringPrototypeStartsWith(originalSourcePath, 'file://')) {
205-
// If no sourcesContent was found, attempt to load the original source
206-
// from disk:
207-
debug(`read source of ${originalSourcePath} from filesystem`);
208-
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
209-
try {
210-
source = readFileSync(originalSourcePathNoScheme, 'utf8');
211-
} catch (err) {
212-
debug(err);
213-
}
214-
}
215-
return source;
216-
}
217-
218178
/**
219179
* Retrieve exact line in the original source code from the source map's `sources` list or disk.
220180
* @param {string} fileName - actual file name

lib/internal/source_map/source_map_cache.js

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeIndexOf,
45
ArrayPrototypePush,
56
JSONParse,
67
ObjectFreeze,
78
RegExpPrototypeExec,
9+
RegExpPrototypeSymbolSplit,
810
SafeMap,
911
StringPrototypeCodePointAt,
1012
StringPrototypeSplit,
13+
StringPrototypeStartsWith,
1114
} = primordials;
1215

1316
// See https://tc39.es/ecma426/ for SourceMap V3 specification.
@@ -16,6 +19,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
1619
debug = fn;
1720
});
1821

22+
const { readFileSync } = require('fs');
1923
const { validateBoolean, validateObject } = require('internal/validators');
2024
const {
2125
setSourceMapsEnabled: setSourceMapsNative,
@@ -277,8 +281,7 @@ function lineLengths(content) {
277281
*/
278282
function sourceMapFromFile(mapURL) {
279283
try {
280-
const fs = require('fs');
281-
const content = fs.readFileSync(fileURLToPath(mapURL), 'utf8');
284+
const content = readFileSync(fileURLToPath(mapURL), 'utf8');
282285
const data = JSONParse(content);
283286
return sourcesToAbsolute(mapURL, data);
284287
} catch (err) {
@@ -400,8 +403,62 @@ function findSourceMap(sourceURL) {
400403
}
401404
}
402405

406+
/**
407+
* Retrieve the original source code from the source map's `sources` list or disk.
408+
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
409+
* @param {string} originalSourcePath - path or url of the original source
410+
* @returns {string | undefined} - the source content or undefined if file not found
411+
*/
412+
function getOriginalSource(payload, originalSourcePath) {
413+
let source;
414+
// payload.sources has been normalized to be an array of absolute urls.
415+
const sourceContentIndex =
416+
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
417+
if (payload.sourcesContent?.[sourceContentIndex]) {
418+
// First we check if the original source content was provided in the
419+
// source map itself:
420+
source = payload.sourcesContent[sourceContentIndex];
421+
} else if (StringPrototypeStartsWith(originalSourcePath, 'file://')) {
422+
// If no sourcesContent was found, attempt to load the original source
423+
// from disk:
424+
debug(`read source of ${originalSourcePath} from filesystem`);
425+
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
426+
try {
427+
source = readFileSync(originalSourcePathNoScheme, 'utf8');
428+
} catch (err) {
429+
debug(err);
430+
}
431+
}
432+
return source;
433+
}
434+
435+
/**
436+
* Get the line of source in the source map.
437+
* @param {import('internal/source_map/source_map').SourceMap} sourceMap
438+
* @param {string} originalSourcePath path or url of the original source
439+
* @param {number} originalLine line number in the original source
440+
* @returns {string|undefined} source line if found
441+
*/
442+
function getSourceLine(
443+
sourceMap,
444+
originalSourcePath,
445+
originalLine,
446+
) {
447+
const source = getOriginalSource(
448+
sourceMap.payload,
449+
originalSourcePath,
450+
);
451+
if (typeof source !== 'string') {
452+
return;
453+
}
454+
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
455+
const line = lines[originalLine];
456+
return line;
457+
}
458+
403459
module.exports = {
404460
findSourceMap,
461+
getSourceLine,
405462
getSourceMapsSupport,
406463
setSourceMapsSupport,
407464
maybeCacheSourceMap,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
2+
3+
assert(false)
4+
5+
at Object.<anonymous> (*/test/fixtures/source-map/output/source_map_assert_source_line.ts:11:3)
6+
*
7+
*
8+
*
9+
*
10+
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
11+
*
12+
*
13+
*
14+
generatedMessage: true,
15+
code: 'ERR_ASSERTION',
16+
actual: false,
17+
expected: true,
18+
operator: '==',
19+
diff: 'simple'
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Flags: --enable-source-maps --experimental-transform-types --no-warnings
2+
3+
require('../../../common');
4+
const assert = require('node:assert');
5+
6+
enum Bar {
7+
makeSureTransformTypes,
8+
}
9+
10+
try {
11+
assert(false);
12+
} catch (e) {
13+
console.log(e);
14+
}

test/fixtures/source-map/output/source_map_enclosing_function.snapshot

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
throw err
33
^
44

5-
65
Error: an error!
76
at functionD (*/test/fixtures/source-map/enclosing-call-site.js:16:17)
87
at functionC (*/test/fixtures/source-map/enclosing-call-site.js:10:3)

test/fixtures/source-map/output/source_map_eval.snapshot

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
alert "I knew it!"
33
^
44

5-
65
ReferenceError: alert is not defined
76
at Object.eval (*/synthesized/workspace/tabs-source-url.coffee:26:2)
87
at eval (*/synthesized/workspace/tabs-source-url.coffee:1:14)

test/fixtures/source-map/output/source_map_reference_error_tabs.snapshot

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
alert "I knew it!"
33
^
44

5-
65
ReferenceError: alert is not defined
76
at Object.<anonymous> (*/test/fixtures/source-map/tabs.coffee:26:2)
87
at Object.<anonymous> (*/test/fixtures/source-map/tabs.coffee:1:14)

test/fixtures/source-map/output/source_map_throw_async_stack_trace.snapshot

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
throw new Error('message')
33
^
44

5-
65
Error: message
76
at Throw (*/test/fixtures/source-map/output/source_map_throw_async_stack_trace.mts:13:9)
87
at async Promise.all (index 3)

test/fixtures/source-map/output/source_map_throw_construct.snapshot

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
throw new Error('message');
33
^
44

5-
65
Error: message
76
at new Foo (*/test/fixtures/source-map/output/source_map_throw_construct.mts:13:11)
87
at <anonymous> (*/test/fixtures/source-map/output/source_map_throw_construct.mts:17:1)

0 commit comments

Comments
 (0)