Skip to content

Commit

Permalink
src,lib: print source map error source on demand
Browse files Browse the repository at this point in the history
The source context is not prepended to the value of the `stack` property
when the source map is not enabled. Rather than prepending the error
source context to the value of the `stack` property unconditionally,
this patch aligns the behavior and only prints the source context when
the error is not handled by userland (e.g. fatal errors).

Also, this patch fixes that when source-map support is enabled, the
error source context is not pointing to where the error was thrown.

PR-URL: nodejs#43875
Fixes: nodejs#43186
Fixes: nodejs#41541
Reviewed-By: Ben Coe <bencoe@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information
legendecas authored and Fyko committed Sep 15, 2022
1 parent 5fae245 commit d49d9eb
Show file tree
Hide file tree
Showing 18 changed files with 219 additions and 87 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Expand Up @@ -6,6 +6,7 @@ test/message/esm_display_syntax_error.mjs
tools/icu
tools/lint-md/lint-md.mjs
benchmark/tmp
benchmark/fixtures
doc/**/*.js
!doc/api_assets/*.js
!.eslintrc.js
34 changes: 34 additions & 0 deletions benchmark/es/error-stack.js
@@ -0,0 +1,34 @@
'use strict';

const common = require('../common.js');
const modPath = require.resolve('../fixtures/simple-error-stack.js');

const bench = common.createBenchmark(main, {
method: ['without-sourcemap', 'sourcemap'],
n: [1e5],
});

function runN(n) {
delete require.cache[modPath];
const mod = require(modPath);
bench.start();
for (let i = 0; i < n; i++) {
mod.simpleErrorStack();
}
bench.end(n);
}

function main({ n, method }) {
switch (method) {
case 'without-sourcemap':
process.setSourceMapsEnabled(false);
runN(n);
break;
case 'sourcemap':
process.setSourceMapsEnabled(true);
runN(n);
break;
default:
throw new Error(`Unexpected method "${method}"`);
}
}
15 changes: 15 additions & 0 deletions benchmark/fixtures/simple-error-stack.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions benchmark/fixtures/simple-error-stack.ts
@@ -0,0 +1,17 @@
'use strict';

// Compile with `tsc --inlineSourceMap benchmark/fixtures/simple-error-stack.ts`.

const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';

function simpleErrorStack() {
try {
(lorem as any).BANG();
} catch (e) {
return e.stack;
}
}

export {
simpleErrorStack,
};
44 changes: 26 additions & 18 deletions lib/internal/source_map/prepare_stack_trace.js
Expand Up @@ -25,6 +25,7 @@ const {
kIsNodeError,
} = require('internal/errors');
const { fileURLToPath } = require('internal/url');
const { setGetSourceMapErrorSource } = internalBinding('errors');

// Create a prettified stacktrace, inserting context from source maps
// if possible.
Expand Down Expand Up @@ -53,7 +54,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
return errorString;
}

let errorSource = '';
let lastSourceMap;
let lastFileName;
const preparedTrace = ArrayPrototypeJoin(ArrayPrototypeMap(trace, (t, i) => {
Expand All @@ -62,14 +62,12 @@ const prepareStackTrace = (globalThis, error, trace) => {
// A stack trace will often have several call sites in a row within the
// same file, cache the source map and file content accordingly:
let fileName = t.getFileName();
let generated = false;
if (fileName === undefined) {
fileName = t.getEvalOrigin();
generated = true;
}
const sm = fileName === lastFileName ?
lastSourceMap :
findSourceMap(fileName, generated);
findSourceMap(fileName);
lastSourceMap = sm;
lastFileName = fileName;
if (sm) {
Expand All @@ -83,14 +81,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
if (originalSource && originalLine !== undefined &&
originalColumn !== undefined) {
const name = getOriginalSymbolName(sm, trace, i);
if (i === 0) {
errorSource = getErrorSource(
sm,
originalSource,
originalLine,
originalColumn
);
}
// Construct call site name based on: v8.dev/docs/stack-trace-api:
const fnName = t.getFunctionName() ?? t.getMethodName();
const typeName = t.getTypeName();
Expand All @@ -116,7 +106,7 @@ const prepareStackTrace = (globalThis, error, trace) => {
}
return `${str}${t}`;
}), '');
return `${errorSource}${errorString}\n at ${preparedTrace}`;
return `${errorString}\n at ${preparedTrace}`;
};

// Transpilers may have removed the original symbol name used in the stack
Expand Down Expand Up @@ -155,7 +145,7 @@ function getErrorSource(
fileURLToPath(originalSourcePath) : originalSourcePath;
const source = getOriginalSource(
sourceMap.payload,
originalSourcePathNoScheme
originalSourcePath
);
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
const line = lines[originalLine];
Expand All @@ -178,28 +168,46 @@ function getErrorSource(

function getOriginalSource(payload, originalSourcePath) {
let source;
const originalSourcePathNoScheme =
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
fileURLToPath(originalSourcePath) : originalSourcePath;
// 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 {
} 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);
source = '';
}
} else {
source = '';
}
return source;
}

function getSourceMapErrorSource(fileName, lineNumber, columnNumber) {
const sm = findSourceMap(fileName);
if (sm === null) {
return;
}
const {
originalLine,
originalColumn,
originalSource,
} = sm.findEntry(lineNumber - 1, columnNumber);
const errorSource = getErrorSource(sm, originalSource, originalLine, originalColumn);
return errorSource;
}

setGetSourceMapErrorSource(getSourceMapErrorSource);

module.exports = {
prepareStackTrace,
};
91 changes: 54 additions & 37 deletions lib/internal/source_map/source_map_cache.js
Expand Up @@ -41,6 +41,8 @@ const esmSourceMapCache = new SafeMap();
// The generated sources is not mutable, so we can use a Map without memory concerns:
const generatedSourceMapCache = new SafeMap();
const kLeadingProtocol = /^\w+:\/\//;
const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/;
const kSourceURLMagicComment = /\/[*/]#\s+sourceURL=(?<sourceURL>[^\s]+)/;

const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
let SourceMap;
Expand Down Expand Up @@ -77,7 +79,22 @@ function setSourceMapsEnabled(val) {
sourceMapsEnabled = val;
}

function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource) {
function extractSourceURLMagicComment(content) {
const matchSourceURL = RegExpPrototypeExec(
kSourceURLMagicComment,
content
);
if (matchSourceURL === null) {
return null;
}
let sourceURL = matchSourceURL.groups.sourceURL;
if (sourceURL != null && RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
sourceURL = pathToFileURL(sourceURL).href;
}
return sourceURL;
}

function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
try {
Expand All @@ -87,10 +104,10 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
debug(err);
return;
}
const match = RegExpPrototypeExec(
/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/,
content,
);
const match = RegExpPrototypeExec(kSourceMappingURLMagicComment, content);
if (sourceURL === undefined) {
sourceURL = extractSourceURLMagicComment(content);
}
if (match) {
const data = dataFromUrl(filename, match.groups.sourceMappingURL);
const url = data ? null : match.groups.sourceMappingURL;
Expand All @@ -99,22 +116,33 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo
filename,
lineLengths: lineLengths(content),
data,
url
url,
sourceURL,
});
} else if (isGeneratedSource) {
generatedSourceMapCache.set(filename, {
const entry = {
lineLengths: lineLengths(content),
data,
url
});
url,
sourceURL
};
generatedSourceMapCache.set(filename, entry);
if (sourceURL) {
generatedSourceMapCache.set(sourceURL, entry);
}
} else {
// If there is no cjsModuleInstance and is not generated source assume we are in a
// "modules/esm" context.
esmSourceMapCache.set(filename, {
const entry = {
lineLengths: lineLengths(content),
data,
url
});
url,
sourceURL,
};
esmSourceMapCache.set(filename, entry);
if (sourceURL) {
esmSourceMapCache.set(sourceURL, entry);
}
}
}
}
Expand All @@ -123,19 +151,12 @@ function maybeCacheGeneratedSourceMap(content) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;

const matchSourceURL = RegExpPrototypeExec(
/\/[*/]#\s+sourceURL=(?<sourceURL>[^\s]+)/,
content
);
if (matchSourceURL == null) {
const sourceURL = extractSourceURLMagicComment(content);
if (sourceURL === null) {
return;
}
let sourceURL = matchSourceURL.groups.sourceURL;
if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
sourceURL = pathToFileURL(sourceURL).href;
}
try {
maybeCacheSourceMap(sourceURL, content, null, true);
maybeCacheSourceMap(sourceURL, content, null, true, sourceURL);
} catch (err) {
// This can happen if the filename is not a valid URL.
// If we fail to cache the source map, we should not fail the whole process.
Expand Down Expand Up @@ -254,33 +275,29 @@ function appendCJSCache(obj) {
}
}

function findSourceMap(sourceURL, isGenerated) {
function findSourceMap(sourceURL) {
if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
sourceURL = pathToFileURL(sourceURL).href;
}
if (!SourceMap) {
SourceMap = require('internal/source_map/source_map').SourceMap;
}
let sourceMap;
if (isGenerated) {
sourceMap = generatedSourceMapCache.get(sourceURL);
} else {
sourceMap = esmSourceMapCache.get(sourceURL);
if (sourceMap === undefined) {
for (const value of cjsSourceMapCache) {
const filename = ObjectGetValueSafe(value, 'filename');
if (sourceURL === filename) {
sourceMap = {
data: ObjectGetValueSafe(value, 'data')
};
}
let sourceMap = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
if (sourceMap === undefined) {
for (const value of cjsSourceMapCache) {
const filename = ObjectGetValueSafe(value, 'filename');
const cachedSourceURL = ObjectGetValueSafe(value, 'sourceURL');
if (sourceURL === filename || sourceURL === cachedSourceURL) {
sourceMap = {
data: ObjectGetValueSafe(value, 'data')
};
}
}
}
if (sourceMap && sourceMap.data) {
return new SourceMap(sourceMap.data);
}
return undefined;
return null;
}

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Expand Up @@ -525,6 +525,7 @@ class NoArrayBufferZeroFillScope {
V(enhance_fatal_stack_after_inspector, v8::Function) \
V(enhance_fatal_stack_before_inspector, v8::Function) \
V(fs_use_promises_symbol, v8::Symbol) \
V(get_source_map_error_source, v8::Function) \
V(host_import_module_dynamically_callback, v8::Function) \
V(host_initialize_import_meta_object_callback, v8::Function) \
V(http2session_on_altsvc_function, v8::Function) \
Expand Down

0 comments on commit d49d9eb

Please sign in to comment.