mirror of https://github.com/nodejs/node.git
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:
parent
68b04274ca
commit
e54f237afe
|
@ -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++) {
|
||||
|
|
|
@ -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']"],
|
||||
|
|
Loading…
Reference in New Issue