util: add subclass and null prototype support for errors in inspect

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: https://github.com/nodejs/node/pull/26923
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
This commit is contained in:
Ruben Bridgewater 2019-03-26 14:45:40 +01:00 committed by Michaël Zasso
parent 68b04274ca
commit e54f237afe
No known key found for this signature in database
GPG Key ID: 770F7A9A5AE15600
2 changed files with 111 additions and 22 deletions

View File

@ -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
@ -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;
@ -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++) {

View File

@ -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']"],