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

util: always visualize cause property in errors during inspection #41002

Merged
merged 2 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 94 additions & 30 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1162,25 +1162,58 @@ function getFunctionBase(value, constructor, tag) {
return base;
}

function formatError(err, constructor, tag, ctx, keys) {
const name = err.name != null ? String(err.name) : 'Error';
let len = name.length;
let stack = err.stack ? String(err.stack) : ErrorPrototypeToString(err);
function identicalSequenceRange(a, b) {
for (let i = 0; i < a.length - 3; i++) {
// Find the first entry of b that matches the current entry of a.
const pos = b.indexOf(a[i]);
if (pos !== -1) {
const rest = b.length - pos;
if (rest > 3) {
let len = 1;
const maxLen = MathMin(a.length - i, rest);
// Count the number of consecutive entries.
while (maxLen > len && a[i + len] === b[pos + len]) {
len++;
}
if (len > 3) {
return { len, offset: i };
}
}
}
}

// Do not "duplicate" error properties that are already included in the output
// otherwise.
if (!ctx.showHidden && keys.length !== 0) {
for (const name of ['name', 'message', 'stack']) {
const index = keys.indexOf(name);
// Only hide the property in case it's part of the original stack
if (index !== -1 && stack.includes(err[name])) {
keys.splice(index, 1);
return { len: 0, offset: 0 };
}

function getStackString(error) {
return error.stack ? String(error.stack) : ErrorPrototypeToString(error);
}

function getStackFrames(ctx, err, stack) {
const frames = stack.split('\n');

// Remove stack frames identical to frames in cause.
if (err.cause) {
const causeStack = getStackString(err.cause);
const causeStackStart = causeStack.indexOf('\n at');
if (causeStackStart !== -1) {
const causeFrames = causeStack.slice(causeStackStart + 1).split('\n');
const { len, offset } = identicalSequenceRange(frames, causeFrames);
if (len > 0) {
const skipped = len - 2;
const msg = ` ... ${skipped} lines matching cause stack trace ...`;
frames.splice(offset + 1, skipped, ctx.stylize(msg, 'undefined'));
}
}
}
return frames;
}

function improveStack(stack, constructor, name, tag) {
// A stack trace may contain arbitrary data. Only manipulate the output
// for "regular errors" (errors that "look normal") for now.
let len = name.length;

if (constructor === null ||
(name.endsWith('Error') &&
stack.startsWith(name) &&
Expand All @@ -1206,6 +1239,33 @@ function formatError(err, constructor, tag, ctx, keys) {
}
}
}
return stack;
}

function removeDuplicateErrorKeys(ctx, keys, err, stack) {
if (!ctx.showHidden && keys.length !== 0) {
for (const name of ['name', 'message', 'stack']) {
const index = keys.indexOf(name);
// Only hide the property in case it's part of the original stack
if (index !== -1 && stack.includes(err[name])) {
keys.splice(index, 1);
}
}
}
}

function formatError(err, constructor, tag, ctx, keys) {
const name = err.name != null ? String(err.name) : 'Error';
let stack = getStackString(err);

removeDuplicateErrorKeys(ctx, keys, err, stack);

if (err.cause && (keys.length === 0 || !keys.includes('cause'))) {
keys.push('cause');
}

stack = improveStack(stack, constructor, name, tag);

// Ignore the error message if it's contained in the stack.
let pos = (err.message && stack.indexOf(err.message)) || -1;
if (pos !== -1)
Expand All @@ -1214,27 +1274,31 @@ function formatError(err, constructor, tag, ctx, keys) {
const stackStart = stack.indexOf('\n at', pos);
if (stackStart === -1) {
stack = `[${stack}]`;
} else if (ctx.colors) {
// Highlight userland code and node modules.
} else {
let newStack = stack.slice(0, stackStart);
const lines = stack.slice(stackStart + 1).split('\n');
for (const line of lines) {
const core = line.match(coreModuleRegExp);
if (core !== null && NativeModule.exists(core[1])) {
newStack += `\n${ctx.stylize(line, 'undefined')}`;
} else {
// This adds underscores to all node_modules to quickly identify them.
let nodeModule;
newStack += '\n';
let pos = 0;
while (nodeModule = nodeModulesRegExp.exec(line)) {
// '/node_modules/'.length === 14
newStack += line.slice(pos, nodeModule.index + 14);
newStack += ctx.stylize(nodeModule[1], 'module');
pos = nodeModule.index + nodeModule[0].length;
const lines = getStackFrames(ctx, err, stack.slice(stackStart + 1));
if (ctx.colors) {
// Highlight userland code and node modules.
for (const line of lines) {
const core = line.match(coreModuleRegExp);
if (core !== null && NativeModule.exists(core[1])) {
newStack += `\n${ctx.stylize(line, 'undefined')}`;
} else {
// This adds underscores to all node_modules to quickly identify them.
let nodeModule;
newStack += '\n';
let pos = 0;
while (nodeModule = nodeModulesRegExp.exec(line)) {
// '/node_modules/'.length === 14
newStack += line.slice(pos, nodeModule.index + 14);
newStack += ctx.stylize(nodeModule[1], 'module');
pos = nodeModule.index + nodeModule[0].length;
}
newStack += pos === 0 ? line : line.slice(pos);
}
newStack += pos === 0 ? line : line.slice(pos);
}
} else {
newStack += `\n${lines.join('\n')}`;
}
stack = newStack;
}
Expand Down
31 changes: 31 additions & 0 deletions test/message/util-inspect-error-cause.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

require('../common');

const { inspect } = require('util');

class FoobarError extends Error {
status = 'Feeling good';
}

const cause1 = new TypeError('Inner error');
const cause2 = new FoobarError('Individual message', { cause: cause1 });
cause2.extraProperties = 'Yes!';
const cause3 = new Error('Stack causes', { cause: cause2 });

process.nextTick(() => {
const error = new RangeError('New Stack Frames', { cause: cause2 });
const error2 = new RangeError('New Stack Frames', { cause: cause3 });

inspect.defaultOptions.colors = true;

console.log(inspect(error));
console.log(inspect(cause3));
console.log(inspect(error2));

inspect.defaultOptions.colors = false;

console.log(inspect(error));
console.log(inspect(cause3));
console.log(inspect(error2));
});
136 changes: 136 additions & 0 deletions test/message/util-inspect-error-cause.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
RangeError: New Stack Frames
at *
*[90m at *[39m {
[cause]: FoobarError: Individual message
at *
*[90m at *[39m
*[90m ... 4 lines matching cause stack trace ...*[39m
*[90m at *[39m {
status: *[32m'Feeling good'*[39m,
extraProperties: *[32m'Yes!'*[39m,
[cause]: TypeError: Inner error
at *
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
}
}
Error: Stack causes
at *
*[90m at *[39m
*[90m ... 4 lines matching cause stack trace ...*[39m
*[90m at *[39m {
[cause]: FoobarError: Individual message
at *
*[90m at *[39m
*[90m ... 4 lines matching cause stack trace ...*[39m
*[90m at *[39m {
status: *[32m'Feeling good'*[39m,
extraProperties: *[32m'Yes!'*[39m,
[cause]: TypeError: Inner error
at *
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
}
}
RangeError: New Stack Frames
at *
*[90m at *[39m {
[cause]: Error: Stack causes
at *
*[90m at *[39m
*[90m ... 4 lines matching cause stack trace ...*[39m
*[90m at *[39m {
[cause]: FoobarError: Individual message
at *
*[90m at *[39m
*[90m ... 4 lines matching cause stack trace ...*[39m
*[90m at *[39m {
status: *[32m'Feeling good'*[39m,
extraProperties: *[32m'Yes!'*[39m,
[cause]: TypeError: Inner error
at *
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
}
}
}
RangeError: New Stack Frames
at *
at * {
[cause]: FoobarError: Individual message
at *
at *
... 4 lines matching cause stack trace ...
at * {
status: 'Feeling good',
extraProperties: 'Yes!',
[cause]: TypeError: Inner error
at *
at *
at *
at *
at *
at *
at *
}
}
Error: Stack causes
at *
at *
... 4 lines matching cause stack trace ...
at * {
[cause]: FoobarError: Individual message
at *
at *
... 4 lines matching cause stack trace ...
at *
status: 'Feeling good',
extraProperties: 'Yes!',
[cause]: TypeError: Inner error
at *
at *
at *
at *
at *
at *
at *
}
}
RangeError: New Stack Frames
at *
at * {
[cause]: Error: Stack causes
at *
at *
... 4 lines matching cause stack trace ...
at * {
[cause]: FoobarError: Individual message
at *
at *
... 4 lines matching cause stack trace ...
at * {
status: 'Feeling good',
extraProperties: 'Yes!',
[cause]: TypeError: Inner error
at *
at *
at *
at *
at *
at *
at *
}
}
}