mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 00:20:08 +01:00
lib: add source map support for assert messages
Map source lines in assert messages with cached source maps. PR-URL: https://github.com/nodejs/node/pull/59751 Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
This commit is contained in:
parent
d35bd2088e
commit
f1b56d6200
|
|
@ -8,9 +8,15 @@ const {
|
|||
const {
|
||||
getErrorSourcePositions,
|
||||
} = internalBinding('errors');
|
||||
const {
|
||||
getSourceMapsSupport,
|
||||
findSourceMap,
|
||||
getSourceLine,
|
||||
} = require('internal/source_map/source_map_cache');
|
||||
|
||||
/**
|
||||
* Get the source location of an error.
|
||||
* Get the source location of an error. If source map is enabled, resolve the source location
|
||||
* based on the source map.
|
||||
*
|
||||
* The `error.stack` must not have been accessed. The resolution is based on the structured
|
||||
* error stack data.
|
||||
|
|
@ -21,10 +27,35 @@ function getErrorSourceLocation(error) {
|
|||
const pos = getErrorSourcePositions(error);
|
||||
const {
|
||||
sourceLine,
|
||||
scriptResourceName,
|
||||
lineNumber,
|
||||
startColumn,
|
||||
} = pos;
|
||||
|
||||
return { sourceLine, startColumn };
|
||||
// Source map is not enabled. Return the source line directly.
|
||||
if (!getSourceMapsSupport().enabled) {
|
||||
return { sourceLine, startColumn };
|
||||
}
|
||||
|
||||
const sm = findSourceMap(scriptResourceName);
|
||||
if (sm === undefined) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
originalLine,
|
||||
originalColumn,
|
||||
originalSource,
|
||||
} = sm.findEntry(lineNumber - 1, startColumn);
|
||||
const originalSourceLine = getSourceLine(sm, originalSource, originalLine, originalColumn);
|
||||
|
||||
if (!originalSourceLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
sourceLine: originalSourceLine,
|
||||
startColumn: originalColumn,
|
||||
};
|
||||
}
|
||||
|
||||
const memberAccessTokens = [ '.', '?.', '[', ']' ];
|
||||
|
|
@ -111,7 +142,8 @@ function getFirstExpression(code, startColumn) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the source expression of an error.
|
||||
* Get the source expression of an error. If source map is enabled, resolve the source location
|
||||
* based on the source map.
|
||||
*
|
||||
* The `error.stack` must not have been accessed, or the source location may be incorrect. The
|
||||
* resolution is based on the structured error stack data.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeMap,
|
||||
ErrorPrototypeToString,
|
||||
RegExpPrototypeSymbolSplit,
|
||||
SafeStringIterator,
|
||||
StringPrototypeRepeat,
|
||||
StringPrototypeSlice,
|
||||
|
|
@ -16,8 +14,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
|
|||
debug = fn;
|
||||
});
|
||||
const { getStringWidth } = require('internal/util/inspect');
|
||||
const { readFileSync } = require('fs');
|
||||
const { findSourceMap } = require('internal/source_map/source_map_cache');
|
||||
const { findSourceMap, getSourceLine } = require('internal/source_map/source_map_cache');
|
||||
const {
|
||||
kIsNodeError,
|
||||
} = require('internal/errors');
|
||||
|
|
@ -155,21 +152,13 @@ function getErrorSource(
|
|||
originalLine,
|
||||
originalColumn,
|
||||
) {
|
||||
const originalSourcePathNoScheme =
|
||||
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
|
||||
fileURLToPath(originalSourcePath) : originalSourcePath;
|
||||
const source = getOriginalSource(
|
||||
sourceMap.payload,
|
||||
originalSourcePath,
|
||||
);
|
||||
if (typeof source !== 'string') {
|
||||
return;
|
||||
}
|
||||
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
|
||||
const line = lines[originalLine];
|
||||
const line = getSourceLine(sourceMap, originalSourcePath, originalLine);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
const originalSourcePathNoScheme =
|
||||
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
|
||||
fileURLToPath(originalSourcePath) : originalSourcePath;
|
||||
|
||||
// Display ^ in appropriate position, regardless of whether tabs or
|
||||
// spaces are used:
|
||||
|
|
@ -182,39 +171,10 @@ function getErrorSource(
|
|||
prefix = StringPrototypeSlice(prefix, 0, -1); // The last character is '^'.
|
||||
|
||||
const exceptionLine =
|
||||
`${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n\n`;
|
||||
`${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n`;
|
||||
return exceptionLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the original source code from the source map's `sources` list or disk.
|
||||
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
|
||||
* @param {string} originalSourcePath - path or url of the original source
|
||||
* @returns {string | undefined} - the source content or undefined if file not found
|
||||
*/
|
||||
function getOriginalSource(payload, originalSourcePath) {
|
||||
let source;
|
||||
// payload.sources has been normalized to be an array of absolute urls.
|
||||
const sourceContentIndex =
|
||||
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
|
||||
if (payload.sourcesContent?.[sourceContentIndex]) {
|
||||
// First we check if the original source content was provided in the
|
||||
// source map itself:
|
||||
source = payload.sourcesContent[sourceContentIndex];
|
||||
} else if (StringPrototypeStartsWith(originalSourcePath, 'file://')) {
|
||||
// If no sourcesContent was found, attempt to load the original source
|
||||
// from disk:
|
||||
debug(`read source of ${originalSourcePath} from filesystem`);
|
||||
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
|
||||
try {
|
||||
source = readFileSync(originalSourcePathNoScheme, 'utf8');
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve exact line in the original source code from the source map's `sources` list or disk.
|
||||
* @param {string} fileName - actual file name
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypePush,
|
||||
JSONParse,
|
||||
ObjectFreeze,
|
||||
RegExpPrototypeExec,
|
||||
RegExpPrototypeSymbolSplit,
|
||||
SafeMap,
|
||||
StringPrototypeCodePointAt,
|
||||
StringPrototypeSplit,
|
||||
StringPrototypeStartsWith,
|
||||
} = primordials;
|
||||
|
||||
// See https://tc39.es/ecma426/ for SourceMap V3 specification.
|
||||
|
|
@ -16,6 +19,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
|
|||
debug = fn;
|
||||
});
|
||||
|
||||
const { readFileSync } = require('fs');
|
||||
const { validateBoolean, validateObject } = require('internal/validators');
|
||||
const {
|
||||
setSourceMapsEnabled: setSourceMapsNative,
|
||||
|
|
@ -277,8 +281,7 @@ function lineLengths(content) {
|
|||
*/
|
||||
function sourceMapFromFile(mapURL) {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync(fileURLToPath(mapURL), 'utf8');
|
||||
const content = readFileSync(fileURLToPath(mapURL), 'utf8');
|
||||
const data = JSONParse(content);
|
||||
return sourcesToAbsolute(mapURL, data);
|
||||
} catch (err) {
|
||||
|
|
@ -400,8 +403,62 @@ function findSourceMap(sourceURL) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the original source code from the source map's `sources` list or disk.
|
||||
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
|
||||
* @param {string} originalSourcePath - path or url of the original source
|
||||
* @returns {string | undefined} - the source content or undefined if file not found
|
||||
*/
|
||||
function getOriginalSource(payload, originalSourcePath) {
|
||||
let source;
|
||||
// payload.sources has been normalized to be an array of absolute urls.
|
||||
const sourceContentIndex =
|
||||
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
|
||||
if (payload.sourcesContent?.[sourceContentIndex]) {
|
||||
// First we check if the original source content was provided in the
|
||||
// source map itself:
|
||||
source = payload.sourcesContent[sourceContentIndex];
|
||||
} else if (StringPrototypeStartsWith(originalSourcePath, 'file://')) {
|
||||
// If no sourcesContent was found, attempt to load the original source
|
||||
// from disk:
|
||||
debug(`read source of ${originalSourcePath} from filesystem`);
|
||||
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
|
||||
try {
|
||||
source = readFileSync(originalSourcePathNoScheme, 'utf8');
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line of source in the source map.
|
||||
* @param {import('internal/source_map/source_map').SourceMap} sourceMap
|
||||
* @param {string} originalSourcePath path or url of the original source
|
||||
* @param {number} originalLine line number in the original source
|
||||
* @returns {string|undefined} source line if found
|
||||
*/
|
||||
function getSourceLine(
|
||||
sourceMap,
|
||||
originalSourcePath,
|
||||
originalLine,
|
||||
) {
|
||||
const source = getOriginalSource(
|
||||
sourceMap.payload,
|
||||
originalSourcePath,
|
||||
);
|
||||
if (typeof source !== 'string') {
|
||||
return;
|
||||
}
|
||||
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
|
||||
const line = lines[originalLine];
|
||||
return line;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findSourceMap,
|
||||
getSourceLine,
|
||||
getSourceMapsSupport,
|
||||
setSourceMapsSupport,
|
||||
maybeCacheSourceMap,
|
||||
|
|
|
|||
20
test/fixtures/source-map/output/source_map_assert_source_line.snapshot
vendored
Normal file
20
test/fixtures/source-map/output/source_map_assert_source_line.snapshot
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
|
||||
|
||||
assert(false)
|
||||
|
||||
at Object.<anonymous> (*/test/fixtures/source-map/output/source_map_assert_source_line.ts:11:3)
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
|
||||
*
|
||||
*
|
||||
*
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
14
test/fixtures/source-map/output/source_map_assert_source_line.ts
vendored
Normal file
14
test/fixtures/source-map/output/source_map_assert_source_line.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Flags: --enable-source-maps --experimental-transform-types --no-warnings
|
||||
|
||||
require('../../../common');
|
||||
const assert = require('node:assert');
|
||||
|
||||
enum Bar {
|
||||
makeSureTransformTypes,
|
||||
}
|
||||
|
||||
try {
|
||||
assert(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
throw err
|
||||
^
|
||||
|
||||
|
||||
Error: an error!
|
||||
at functionD (*/test/fixtures/source-map/enclosing-call-site.js:16:17)
|
||||
at functionC (*/test/fixtures/source-map/enclosing-call-site.js:10:3)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
alert "I knew it!"
|
||||
^
|
||||
|
||||
|
||||
ReferenceError: alert is not defined
|
||||
at Object.eval (*/synthesized/workspace/tabs-source-url.coffee:26:2)
|
||||
at eval (*/synthesized/workspace/tabs-source-url.coffee:1:14)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
alert "I knew it!"
|
||||
^
|
||||
|
||||
|
||||
ReferenceError: alert is not defined
|
||||
at Object.<anonymous> (*/test/fixtures/source-map/tabs.coffee:26:2)
|
||||
at Object.<anonymous> (*/test/fixtures/source-map/tabs.coffee:1:14)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
throw new Error('message')
|
||||
^
|
||||
|
||||
|
||||
Error: message
|
||||
at Throw (*/test/fixtures/source-map/output/source_map_throw_async_stack_trace.mts:13:9)
|
||||
at async Promise.all (index 3)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
throw new Error('message');
|
||||
^
|
||||
|
||||
|
||||
Error: message
|
||||
at new Foo (*/test/fixtures/source-map/output/source_map_throw_construct.mts:13:11)
|
||||
at <anonymous> (*/test/fixtures/source-map/output/source_map_throw_construct.mts:17:1)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ reachable
|
|||
throw Error('an exception');
|
||||
^
|
||||
|
||||
|
||||
Error: an exception
|
||||
at branch (*/test/fixtures/source-map/typescript-throw.ts:18:11)
|
||||
at Object.<anonymous> (*/test/fixtures/source-map/typescript-throw.ts:24:1)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
("あ 🐕 🐕", throw Error("an error"));
|
||||
^
|
||||
|
||||
|
||||
Error: an error
|
||||
at Object.createElement (*/test/fixtures/source-map/icu.jsx:3:23)
|
||||
at Object.<anonymous> (*/test/fixtures/source-map/icu.jsx:9:5)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
throw Error('goodbye');
|
||||
^
|
||||
|
||||
|
||||
Error: goodbye
|
||||
at Hello (*/test/fixtures/source-map/uglify-throw-original.js:5:9)
|
||||
at Immediate.<anonymous> (*/test/fixtures/source-map/uglify-throw-original.js:9:3)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ describe('sourcemaps output', { concurrency: !process.env.TEST_PARALLEL }, () =>
|
|||
);
|
||||
|
||||
const tests = [
|
||||
{ name: 'source-map/output/source_map_assert_source_line.ts' },
|
||||
{ name: 'source-map/output/source_map_disabled_by_api.js' },
|
||||
{ name: 'source-map/output/source_map_disabled_by_process_api.js' },
|
||||
{ name: 'source-map/output/source_map_enabled_by_api.js' },
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user