Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v14.x backport] errors: print original exception context #37700

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 0 additions & 8 deletions lib/internal/bootstrap/pre_execution.js
Expand Up @@ -31,14 +31,6 @@ function prepareMainThreadExecution(expandArgv1 = false) {
setupCoverageHooks(process.env.NODE_V8_COVERAGE);
}

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

setupDebugEnv();

Expand Down
4 changes: 2 additions & 2 deletions lib/internal/modules/cjs/loader.js
Expand Up @@ -59,6 +59,7 @@ module.exports = {

const { NativeModule } = require('internal/bootstrap/loaders');
const {
getSourceMapsEnabled,
maybeCacheSourceMap,
rekeySourceMap
} = require('internal/source_map/source_map_cache');
Expand All @@ -81,7 +82,6 @@ const {
loadNativeModule
} = require('internal/modules/cjs/helpers');
const { getOptionValue } = require('internal/options');
const enableSourceMaps = getOptionValue('--enable-source-maps');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
// Do not eagerly grab .manifest, it may be in TDZ
Expand Down Expand Up @@ -758,7 +758,7 @@ Module._load = function(request, parent, isMain) {
// Intercept exceptions that occur during the first tick and rekey them
// on error instance rather than module instance (which will immediately be
// garbage collected).
if (enableSourceMaps) {
if (getSourceMapsEnabled()) {
try {
module.load(filename);
} catch (err) {
Expand Down
57 changes: 54 additions & 3 deletions lib/internal/source_map/prepare_stack_trace.js
Expand Up @@ -7,6 +7,8 @@ const {
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 {
kNoOverride,
Expand Down Expand Up @@ -36,7 +38,17 @@ const prepareStackTrace = (globalThis, error, trace) => {
if (trace.length === 0) {
return errorString;
}

let errorSource = '';
let firstSource;
let firstLine;
let firstColumn;
const preparedTrace = trace.map((t, i) => {
if (i === 0) {
firstLine = t.getLineNumber();
firstColumn = t.getColumnNumber();
firstSource = t.getFileName();
}
let str = i !== 0 ? '\n at ' : '';
str = `${str}${t}`;
try {
Expand All @@ -51,18 +63,57 @@ const prepareStackTrace = (globalThis, error, trace) => {
} = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
if (originalSource && originalLine !== undefined &&
originalColumn !== undefined) {
str +=
`\n -> ${originalSource.replace('file://', '')}:${originalLine + 1}:${originalColumn + 1}`;
const originalSourceNoScheme = originalSource
.replace(/^file:\/\//, '');
if (i === 0) {
firstLine = originalLine + 1;
firstColumn = originalColumn + 1;
firstSource = originalSourceNoScheme;
// Show error in original source context to help user pinpoint it:
errorSource = getErrorSource(firstSource, firstLine, firstColumn);
}
// Show both original and transpiled stack trace information:
str += `\n -> ${originalSourceNoScheme}:${originalLine + 1}:` +
`${originalColumn + 1}`;
}
}
} catch (err) {
debug(err.stack);
}
return str;
});
return `${errorString}\n at ${preparedTrace.join('')}`;
return `${errorSource}${errorString}\n at ${preparedTrace.join('')}`;
};

// Places a snippet of code from where the exception was originally thrown
// above the stack trace. This logic is modeled after GetErrorSource in
// node_errors.cc.
function getErrorSource(firstSource, firstLine, firstColumn) {
let exceptionLine = '';
let source;
try {
source = readFileSync(firstSource, 'utf8');
} catch (err) {
debug(err);
return exceptionLine;
}
const lines = source.split(/\r?\n/, firstLine);
const line = lines[firstLine - 1];
if (!line) return exceptionLine;

// Display ^ in appropriate position, regardless of whether tabs or
// spaces are used:
let prefix = '';
for (const character of line.slice(0, firstColumn)) {
prefix += (character === '\t') ? '\t' :
' '.repeat(getStringWidth(character));
}
prefix = prefix.slice(0, -1); // The last character is the '^'.

exceptionLine = `${firstSource}:${firstLine}\n${line}\n${prefix}^\n\n`;
return exceptionLine;
}

module.exports = {
prepareStackTrace,
};
27 changes: 22 additions & 5 deletions lib/internal/source_map/source_map_cache.js
Expand Up @@ -41,12 +41,28 @@ const { fileURLToPath, URL } = require('url');
let Module;
let SourceMap;

let experimentalSourceMaps;
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
if (experimentalSourceMaps === undefined) {
experimentalSourceMaps = getOptionValue('--enable-source-maps');
let sourceMapsEnabled;
function getSourceMapsEnabled() {
if (sourceMapsEnabled === undefined) {
sourceMapsEnabled = getOptionValue('--enable-source-maps');
if (sourceMapsEnabled) {
const {
enableSourceMaps,
setPrepareStackTraceCallback
} = internalBinding('errors');
const {
prepareStackTrace
} = require('internal/source_map/prepare_stack_trace');
setPrepareStackTraceCallback(prepareStackTrace);
enableSourceMaps();
}
}
if (!(process.env.NODE_V8_COVERAGE || experimentalSourceMaps)) return;
return sourceMapsEnabled;
}

function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
let basePath;
try {
filename = normalizeReferrerURL(filename);
Expand Down Expand Up @@ -250,6 +266,7 @@ function findSourceMap(uri, error) {

module.exports = {
findSourceMap,
getSourceMapsEnabled,
maybeCacheSourceMap,
rekeySourceMap,
sourceMapCacheToObject,
Expand Down
8 changes: 8 additions & 0 deletions src/env-inl.h
Expand Up @@ -837,6 +837,14 @@ void Environment::set_filehandle_close_warning(bool on) {
emit_filehandle_warning_ = on;
}

void Environment::set_source_maps_enabled(bool on) {
source_maps_enabled_ = on;
}

bool Environment::source_maps_enabled() const {
return source_maps_enabled_;
}

inline uint64_t Environment::thread_id() const {
return thread_id_;
}
Expand Down
5 changes: 5 additions & 0 deletions src/env.h
Expand Up @@ -1037,6 +1037,9 @@ class Environment : public MemoryRetainer {
inline bool filehandle_close_warning() const;
inline void set_filehandle_close_warning(bool on);

inline void set_source_maps_enabled(bool on);
inline bool source_maps_enabled() const;

inline void ThrowError(const char* errmsg);
inline void ThrowTypeError(const char* errmsg);
inline void ThrowRangeError(const char* errmsg);
Expand Down Expand Up @@ -1257,6 +1260,8 @@ class Environment : public MemoryRetainer {
bool emit_env_nonstring_warning_ = true;
bool emit_err_name_warning_ = true;
bool emit_filehandle_warning_ = true;
bool source_maps_enabled_ = false;

size_t async_callback_scope_depth_ = 0;
std::vector<double> destroy_async_id_list_;

Expand Down
16 changes: 16 additions & 0 deletions src/node_errors.cc
Expand Up @@ -56,6 +56,16 @@ static std::string GetErrorSource(Isolate* isolate,
node::Utf8Value encoded_source(isolate, source_line_maybe.ToLocalChecked());
std::string sourceline(*encoded_source, encoded_source.length());

// If source maps have been enabled, the exception line will instead be
// added in the JavaScript context:
Environment* env = Environment::GetCurrent(isolate);
const bool has_source_map_url =
!message->GetScriptOrigin().SourceMapUrl().IsEmpty();
if (has_source_map_url && env->source_maps_enabled()) {
*added_exception_line = false;
return sourceline;
}

if (sourceline.find("node-do-not-add-exception-line") != std::string::npos) {
*added_exception_line = false;
return sourceline;
Expand Down Expand Up @@ -801,6 +811,11 @@ void SetPrepareStackTraceCallback(const FunctionCallbackInfo<Value>& args) {
env->set_prepare_stack_trace_callback(args[0].As<Function>());
}

static void EnableSourceMaps(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
env->set_source_maps_enabled(true);
}

static void SetEnhanceStackForFatalException(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Expand Down Expand Up @@ -839,6 +854,7 @@ void Initialize(Local<Object> target,
Environment* env = Environment::GetCurrent(context);
env->SetMethod(
target, "setPrepareStackTraceCallback", SetPrepareStackTraceCallback);
env->SetMethod(target, "enableSourceMaps", EnableSourceMaps);
env->SetMethod(target,
"setEnhanceStackForFatalException",
SetEnhanceStackForFatalException);
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/source-map/icu.js

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

12 changes: 12 additions & 0 deletions test/fixtures/source-map/icu.jsx
@@ -0,0 +1,12 @@
const React = {
createElement: () => {
("あ 🐕 🐕", throw Error("an error"));
}
};

const profile = (
<div>
<img src="avatar.png" className="profile" />
<h3>{["hello"]}</h3>
</div>
);
29 changes: 29 additions & 0 deletions test/fixtures/source-map/tabs.coffee
@@ -0,0 +1,29 @@
# Assignment:
number = 42
opposite = true

# Conditions:
number = -42 if opposite

# Functions:
square = (x) -> x * x

# Arrays:
list = [1, 2, 3, 4, 5]

# Objects:
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x

# Splats:
race = (winner, runners...) ->
print winner, runners

# Existence:
if true
alert "I knew it!"

# Array comprehensions:
cubes = (math.cube num for num in list)
56 changes: 56 additions & 0 deletions test/fixtures/source-map/tabs.js

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

5 changes: 5 additions & 0 deletions test/message/source_map_reference_error_tabs.js
@@ -0,0 +1,5 @@
// Flags: --enable-source-maps

'use strict';
require('../common');
require('../fixtures/source-map/tabs.js');
17 changes: 17 additions & 0 deletions test/message/source_map_reference_error_tabs.out
@@ -0,0 +1,17 @@
*tabs.coffee:26
alert "I knew it!"
^

ReferenceError: alert is not defined
at Object.<anonymous> (*tabs.coffee:39:5)
-> *tabs.coffee:26:2
at Object.<anonymous> (*tabs.coffee:53:4)
-> *tabs.coffee:1:14
at Module._compile (internal/modules/cjs/loader.js:*
at Object.Module._extensions..js (internal/modules/cjs/loader.js:*
at Module.load (internal/modules/cjs/loader.js:*
at Function.Module._load (internal/modules/cjs/loader.js:*
at Module.require (internal/modules/cjs/loader.js:*
at require (internal/modules/cjs/helpers.js:*
at Object.<anonymous> (*source_map_reference_error_tabs.js:*
at Module._compile (internal/modules/cjs/loader.js:*
3 changes: 3 additions & 0 deletions test/message/source_map_throw_catch.out
@@ -1,4 +1,7 @@
reachable
*typescript-throw.ts:18
throw Error('an exception');
^
Error: an exception
at branch (*typescript-throw.js:20:15)
-> *typescript-throw.ts:18:11
Expand Down
3 changes: 3 additions & 0 deletions test/message/source_map_throw_first_tick.out
@@ -1,4 +1,7 @@
reachable
*typescript-throw.ts:18
throw Error('an exception');
^
Error: an exception
at branch (*typescript-throw.js:20:15)
-> *typescript-throw.ts:18:11
Expand Down