Skip to content

Commit 458677f

Browse files
committed
errors: print original exception context
When --enable-source-maps is set, the error context displayed above the stack trace now shows original source rather than transpiled. PR-URL: #33491 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
1 parent 8f10bb2 commit 458677f

18 files changed

+270
-21
lines changed

lib/internal/bootstrap/pre_execution.js

-8
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,6 @@ function prepareMainThreadExecution(expandArgv1 = false) {
2626
setupCoverageHooks(process.env.NODE_V8_COVERAGE);
2727
}
2828

29-
// If source-map support has been enabled, we substitute in a new
30-
// prepareStackTrace method, replacing the default in errors.js.
31-
if (getOptionValue('--enable-source-maps')) {
32-
const { prepareStackTrace } =
33-
require('internal/source_map/prepare_stack_trace');
34-
const { setPrepareStackTraceCallback } = internalBinding('errors');
35-
setPrepareStackTraceCallback(prepareStackTrace);
36-
}
3729

3830
setupDebugEnv();
3931

lib/internal/modules/cjs/loader.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const {
5050

5151
const { NativeModule } = require('internal/bootstrap/loaders');
5252
const {
53+
getSourceMapsEnabled,
5354
maybeCacheSourceMap,
5455
rekeySourceMap
5556
} = require('internal/source_map/source_map_cache');
@@ -71,7 +72,6 @@ const {
7172
loadNativeModule
7273
} = require('internal/modules/cjs/helpers');
7374
const { getOptionValue } = require('internal/options');
74-
const enableSourceMaps = getOptionValue('--enable-source-maps');
7575
const preserveSymlinks = getOptionValue('--preserve-symlinks');
7676
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
7777
const manifest = getOptionValue('--experimental-policy') ?
@@ -941,7 +941,7 @@ Module._load = function(request, parent, isMain) {
941941
// Intercept exceptions that occur during the first tick and rekey them
942942
// on error instance rather than module instance (which will immediately be
943943
// garbage collected).
944-
if (enableSourceMaps) {
944+
if (getSourceMapsEnabled()) {
945945
try {
946946
module.load(filename);
947947
} catch (err) {

lib/internal/source_map/prepare_stack_trace.js

+54-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
} = primordials;
66

77
const debug = require('internal/util/debuglog').debuglog('source_map');
8+
const { getStringWidth } = require('internal/util/inspect');
9+
const { readFileSync } = require('fs');
810
const { findSourceMap } = require('internal/source_map/source_map_cache');
911
const {
1012
kNoOverride,
@@ -34,7 +36,17 @@ const prepareStackTrace = (globalThis, error, trace) => {
3436
if (trace.length === 0) {
3537
return errorString;
3638
}
39+
40+
let errorSource = '';
41+
let firstSource;
42+
let firstLine;
43+
let firstColumn;
3744
const preparedTrace = trace.map((t, i) => {
45+
if (i === 0) {
46+
firstLine = t.getLineNumber();
47+
firstColumn = t.getColumnNumber();
48+
firstSource = t.getFileName();
49+
}
3850
let str = i !== 0 ? '\n at ' : '';
3951
str = `${str}${t}`;
4052
try {
@@ -49,18 +61,57 @@ const prepareStackTrace = (globalThis, error, trace) => {
4961
} = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
5062
if (originalSource && originalLine !== undefined &&
5163
originalColumn !== undefined) {
52-
str +=
53-
`\n -> ${originalSource.replace('file://', '')}:${originalLine + 1}:${originalColumn + 1}`;
64+
const originalSourceNoScheme = originalSource
65+
.replace(/^file:\/\//, '');
66+
if (i === 0) {
67+
firstLine = originalLine + 1;
68+
firstColumn = originalColumn + 1;
69+
firstSource = originalSourceNoScheme;
70+
// Show error in original source context to help user pinpoint it:
71+
errorSource = getErrorSource(firstSource, firstLine, firstColumn);
72+
}
73+
// Show both original and transpiled stack trace information:
74+
str += `\n -> ${originalSourceNoScheme}:${originalLine + 1}:` +
75+
`${originalColumn + 1}`;
5476
}
5577
}
5678
} catch (err) {
5779
debug(err.stack);
5880
}
5981
return str;
6082
});
61-
return `${errorString}\n at ${preparedTrace.join('')}`;
83+
return `${errorSource}${errorString}\n at ${preparedTrace.join('')}`;
6284
};
6385

86+
// Places a snippet of code from where the exception was originally thrown
87+
// above the stack trace. This logic is modeled after GetErrorSource in
88+
// node_errors.cc.
89+
function getErrorSource(firstSource, firstLine, firstColumn) {
90+
let exceptionLine = '';
91+
let source;
92+
try {
93+
source = readFileSync(firstSource, 'utf8');
94+
} catch (err) {
95+
debug(err);
96+
return exceptionLine;
97+
}
98+
const lines = source.split(/\r?\n/, firstLine);
99+
const line = lines[firstLine - 1];
100+
if (!line) return exceptionLine;
101+
102+
// Display ^ in appropriate position, regardless of whether tabs or
103+
// spaces are used:
104+
let prefix = '';
105+
for (const character of line.slice(0, firstColumn)) {
106+
prefix += (character === '\t') ? '\t' :
107+
' '.repeat(getStringWidth(character));
108+
}
109+
prefix = prefix.slice(0, -1); // The last character is the '^'.
110+
111+
exceptionLine = `${firstSource}:${firstLine}\n${line}\n${prefix}^\n\n`;
112+
return exceptionLine;
113+
}
114+
64115
module.exports = {
65116
prepareStackTrace,
66117
};

lib/internal/source_map/source_map_cache.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,28 @@ const { fileURLToPath, URL } = require('url');
3939
let Module;
4040
let SourceMap;
4141

42-
let experimentalSourceMaps;
43-
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
44-
if (experimentalSourceMaps === undefined) {
45-
experimentalSourceMaps = getOptionValue('--enable-source-maps');
42+
let sourceMapsEnabled;
43+
function getSourceMapsEnabled() {
44+
if (sourceMapsEnabled === undefined) {
45+
sourceMapsEnabled = getOptionValue('--enable-source-maps');
46+
if (sourceMapsEnabled) {
47+
const {
48+
enableSourceMaps,
49+
setPrepareStackTraceCallback
50+
} = internalBinding('errors');
51+
const {
52+
prepareStackTrace
53+
} = require('internal/source_map/prepare_stack_trace');
54+
setPrepareStackTraceCallback(prepareStackTrace);
55+
enableSourceMaps();
56+
}
4657
}
47-
if (!(process.env.NODE_V8_COVERAGE || experimentalSourceMaps)) return;
58+
return sourceMapsEnabled;
59+
}
60+
61+
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
62+
const sourceMapsEnabled = getSourceMapsEnabled();
63+
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
4864
let basePath;
4965
try {
5066
filename = normalizeReferrerURL(filename);
@@ -248,6 +264,7 @@ function findSourceMap(uri, error) {
248264

249265
module.exports = {
250266
findSourceMap,
267+
getSourceMapsEnabled,
251268
maybeCacheSourceMap,
252269
rekeySourceMap,
253270
sourceMapCacheToObject,

src/env-inl.h

+8
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,14 @@ void Environment::set_emit_insecure_umask_warning(bool on) {
795795
emit_insecure_umask_warning_ = on;
796796
}
797797

798+
void Environment::set_source_maps_enabled(bool on) {
799+
source_maps_enabled_ = on;
800+
}
801+
802+
bool Environment::source_maps_enabled() const {
803+
return source_maps_enabled_;
804+
}
805+
798806
inline uint64_t Environment::thread_id() const {
799807
return thread_id_;
800808
}

src/env.h

+5
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,9 @@ class Environment : public MemoryRetainer {
10591059
inline bool emit_insecure_umask_warning() const;
10601060
inline void set_emit_insecure_umask_warning(bool on);
10611061

1062+
inline void set_source_maps_enabled(bool on);
1063+
inline bool source_maps_enabled() const;
1064+
10621065
inline void ThrowError(const char* errmsg);
10631066
inline void ThrowTypeError(const char* errmsg);
10641067
inline void ThrowRangeError(const char* errmsg);
@@ -1277,6 +1280,8 @@ class Environment : public MemoryRetainer {
12771280
bool emit_err_name_warning_ = true;
12781281
bool emit_filehandle_warning_ = true;
12791282
bool emit_insecure_umask_warning_ = true;
1283+
bool source_maps_enabled_ = false;
1284+
12801285
size_t async_callback_scope_depth_ = 0;
12811286
std::vector<double> destroy_async_id_list_;
12821287

src/node_errors.cc

+16
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ static std::string GetErrorSource(Isolate* isolate,
5757
node::Utf8Value encoded_source(isolate, source_line_maybe.ToLocalChecked());
5858
std::string sourceline(*encoded_source, encoded_source.length());
5959

60+
// If source maps have been enabled, the exception line will instead be
61+
// added in the JavaScript context:
62+
Environment* env = Environment::GetCurrent(isolate);
63+
const bool has_source_map_url =
64+
!message->GetScriptOrigin().SourceMapUrl().IsEmpty();
65+
if (has_source_map_url && env->source_maps_enabled()) {
66+
*added_exception_line = false;
67+
return sourceline;
68+
}
69+
6070
if (sourceline.find("node-do-not-add-exception-line") != std::string::npos) {
6171
*added_exception_line = false;
6272
return sourceline;
@@ -802,6 +812,11 @@ void SetPrepareStackTraceCallback(const FunctionCallbackInfo<Value>& args) {
802812
env->set_prepare_stack_trace_callback(args[0].As<Function>());
803813
}
804814

815+
static void EnableSourceMaps(const FunctionCallbackInfo<Value>& args) {
816+
Environment* env = Environment::GetCurrent(args);
817+
env->set_source_maps_enabled(true);
818+
}
819+
805820
static void SetEnhanceStackForFatalException(
806821
const FunctionCallbackInfo<Value>& args) {
807822
Environment* env = Environment::GetCurrent(args);
@@ -840,6 +855,7 @@ void Initialize(Local<Object> target,
840855
Environment* env = Environment::GetCurrent(context);
841856
env->SetMethod(
842857
target, "setPrepareStackTraceCallback", SetPrepareStackTraceCallback);
858+
env->SetMethod(target, "enableSourceMaps", EnableSourceMaps);
843859
env->SetMethod(target,
844860
"setEnhanceStackForFatalException",
845861
SetEnhanceStackForFatalException);

test/fixtures/source-map/icu.js

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/source-map/icu.jsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const React = {
2+
createElement: () => {
3+
("あ 🐕 🐕", throw Error("an error"));
4+
}
5+
};
6+
7+
const profile = (
8+
<div>
9+
<img src="avatar.png" className="profile" />
10+
<h3>{["hello"]}</h3>
11+
</div>
12+
);

test/fixtures/source-map/tabs.coffee

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Assignment:
2+
number = 42
3+
opposite = true
4+
5+
# Conditions:
6+
number = -42 if opposite
7+
8+
# Functions:
9+
square = (x) -> x * x
10+
11+
# Arrays:
12+
list = [1, 2, 3, 4, 5]
13+
14+
# Objects:
15+
math =
16+
root: Math.sqrt
17+
square: square
18+
cube: (x) -> x * square x
19+
20+
# Splats:
21+
race = (winner, runners...) ->
22+
print winner, runners
23+
24+
# Existence:
25+
if true
26+
alert "I knew it!"
27+
28+
# Array comprehensions:
29+
cubes = (math.cube num for num in list)

test/fixtures/source-map/tabs.js

+56
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/tabs.js');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
*tabs.coffee:26
2+
alert "I knew it!"
3+
^
4+
5+
ReferenceError: alert is not defined
6+
at Object.<anonymous> (*tabs.coffee:39:5)
7+
-> *tabs.coffee:26:2
8+
at Object.<anonymous> (*tabs.coffee:53:4)
9+
-> *tabs.coffee:1:14
10+
at Module._compile (internal/modules/cjs/loader.js:*
11+
at Object.Module._extensions..js (internal/modules/cjs/loader.js:*
12+
at Module.load (internal/modules/cjs/loader.js:*
13+
at Function.Module._load (internal/modules/cjs/loader.js:*
14+
at Module.require (internal/modules/cjs/loader.js:*
15+
at require (internal/modules/cjs/helpers.js:*
16+
at Object.<anonymous> (*source_map_reference_error_tabs.js:*
17+
at Module._compile (internal/modules/cjs/loader.js:*

test/message/source_map_throw_catch.out

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
reachable
2+
*typescript-throw.ts:18
3+
throw Error('an exception');
4+
^
25
Error: an exception
36
at branch (*typescript-throw.js:20:15)
47
-> *typescript-throw.ts:18:11

test/message/source_map_throw_first_tick.out

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
reachable
2+
*typescript-throw.ts:18
3+
throw Error('an exception');
4+
^
25
Error: an exception
36
at branch (*typescript-throw.js:20:15)
47
-> *typescript-throw.ts:18:11

0 commit comments

Comments
 (0)