Skip to content

Commit

Permalink
util: add subclass and null prototype support for errors in inspect
Browse files Browse the repository at this point in the history
This adds support to visualize the difference between errors with
null prototype or subclassed errors. This has a couple safeguards
to be sure that the output is not intrusive.

PR-URL: #26923
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
  • Loading branch information
BridgeAR authored and targos committed Mar 30, 2019
1 parent 68b0427 commit e54f237
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 22 deletions.
69 changes: 47 additions & 22 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,25 +666,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
return ctx.stylize(base, 'date');
}
} else if (isError(value)) {
// Make error with message first say the error.
base = formatError(value);
// Wrap the error in brackets in case it has no stack trace.
const stackStart = base.indexOf('\n at');
if (stackStart === -1) {
base = `[${base}]`;
}
// The message and the stack have to be indented as well!
if (ctx.indentationLvl !== 0) {
const indentation = ' '.repeat(ctx.indentationLvl);
base = formatError(value).replace(/\n/g, `\n${indentation}`);
}
base = formatError(value, constructor, tag, ctx);
if (keys.length === 0)
return base;

if (ctx.compact === false && stackStart !== -1) {
braces[0] += `${base.slice(stackStart)}`;
base = `[${base.slice(0, stackStart)}]`;
}
} else if (isAnyArrayBuffer(value)) {
// Fast path for ArrayBuffer and SharedArrayBuffer.
// Can't do the same for DataView because it has a non-primitive
Expand Down Expand Up @@ -844,6 +828,52 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
return res;
}

function formatError(err, constructor, tag, ctx) {
// TODO(BridgeAR): Always show the error code if present.
let stack = err.stack || errorToString(err);

// A stack trace may contain arbitrary data. Only manipulate the output
// for "regular errors" (errors that "look normal") for now.
const name = err.name || 'Error';
let len = name.length;
if (constructor === null ||
name.endsWith('Error') &&
stack.startsWith(name) &&
(stack.length === len || stack[len] === ':' || stack[len] === '\n')) {
let fallback = 'Error';
if (constructor === null) {
const start = stack.match(/^([A-Z][a-z_ A-Z0-9[\]()-]+)(?::|\n {4}at)/) ||
stack.match(/^([a-z_A-Z0-9-]*Error)$/);
fallback = start && start[1] || '';
len = fallback.length;
fallback = fallback || 'Error';
}
const prefix = getPrefix(constructor, tag, fallback).slice(0, -1);
if (name !== prefix) {
if (prefix.includes(name)) {
if (len === 0) {
stack = `${prefix}: ${stack}`;
} else {
stack = `${prefix}${stack.slice(len)}`;
}
} else {
stack = `${prefix} [${name}]${stack.slice(len)}`;
}
}
}
// Wrap the error in brackets in case it has no stack trace.
const stackStart = stack.indexOf('\n at');
if (stackStart === -1) {
stack = `[${stack}]`;
}
// The message and the stack have to be indented as well!
if (ctx.indentationLvl !== 0) {
const indentation = ' '.repeat(ctx.indentationLvl);
stack = stack.replace(/\n/g, `\n${indentation}`);
}
return stack;
}

function groupArrayElements(ctx, output) {
let totalLength = 0;
let maxLength = 0;
Expand Down Expand Up @@ -991,11 +1021,6 @@ function formatPrimitive(fn, value, ctx) {
return fn(value.toString(), 'symbol');
}

function formatError(value) {
// TODO(BridgeAR): Always show the error code if present.
return value.stack || errorToString(value);
}

function formatNamespaceObject(ctx, value, recurseTimes, keys) {
const output = new Array(keys.length);
for (var i = 0; i < keys.length; i++) {
Expand Down
64 changes: 64 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,70 @@ assert.strictEqual(util.inspect('"\''), '`"\'`');
// eslint-disable-next-line no-template-curly-in-string
assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");

// Errors should visualize as much information as possible.
// If the name is not included in the stack, visualize it as well.
[
[class Foo extends TypeError {}, 'test'],
[class Foo extends TypeError {}, undefined],
[class BarError extends Error {}, 'test'],
[class BazError extends Error {
get name() {
return 'BazError';
}
}, undefined]
].forEach(([Class, message, messages], i) => {
console.log('Test %i', i);
const foo = new Class(message);
const name = foo.name;
const extra = Class.name.includes('Error') ? '' : ` [${foo.name}]`;
assert(
util.inspect(foo).startsWith(
`${Class.name}${extra}${message ? `: ${message}` : '\n'}`),
util.inspect(foo)
);
Object.defineProperty(foo, Symbol.toStringTag, {
value: 'WOW',
writable: true,
configurable: true
});
const stack = foo.stack;
foo.stack = 'This is a stack';
assert.strictEqual(
util.inspect(foo),
'[This is a stack]'
);
foo.stack = stack;
assert(
util.inspect(foo).startsWith(
`${Class.name} [WOW]${extra}${message ? `: ${message}` : '\n'}`),
util.inspect(foo)
);
Object.setPrototypeOf(foo, null);
assert(
util.inspect(foo).startsWith(
`[${name}: null prototype] [WOW]${message ? `: ${message}` : '\n'}`
),
util.inspect(foo)
);
foo.bar = true;
delete foo[Symbol.toStringTag];
assert(
util.inspect(foo).startsWith(
`{ [${name}: null prototype]${message ? `: ${message}` : '\n'}`),
util.inspect(foo)
);
foo.stack = 'This is a stack';
assert.strictEqual(
util.inspect(foo),
'{ [[Error: null prototype]: This is a stack] bar: true }'
);
foo.stack = stack.split('\n')[0];
assert.strictEqual(
util.inspect(foo),
`{ [[${name}: null prototype]${message ? `: ${message}` : ''}] bar: true }`
);
});

// Verify that throwing in valueOf and toString still produces nice results.
[
[new String(55), "[String: '55']"],
Expand Down

0 comments on commit e54f237

Please sign in to comment.