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');
|
return ctx.stylize(base, 'date');
|
||||||
}
|
}
|
||||||
} else if (isError(value)) {
|
} else if (isError(value)) {
|
||||||
// Make error with message first say the error.
|
base = formatError(value, constructor, tag, ctx);
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
if (keys.length === 0)
|
if (keys.length === 0)
|
||||||
return base;
|
return base;
|
||||||
|
|
||||||
if (ctx.compact === false && stackStart !== -1) {
|
|
||||||
braces[0] += `${base.slice(stackStart)}`;
|
|
||||||
base = `[${base.slice(0, stackStart)}]`;
|
|
||||||
}
|
|
||||||
} else if (isAnyArrayBuffer(value)) {
|
} else if (isAnyArrayBuffer(value)) {
|
||||||
// Fast path for ArrayBuffer and SharedArrayBuffer.
|
// Fast path for ArrayBuffer and SharedArrayBuffer.
|
||||||
// Can't do the same for DataView because it has a non-primitive
|
// 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;
|
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) {
|
function groupArrayElements(ctx, output) {
|
||||||
let totalLength = 0;
|
let totalLength = 0;
|
||||||
let maxLength = 0;
|
let maxLength = 0;
|
||||||
@ -991,11 +1021,6 @@ function formatPrimitive(fn, value, ctx) {
|
|||||||
return fn(value.toString(), 'symbol');
|
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) {
|
function formatNamespaceObject(ctx, value, recurseTimes, keys) {
|
||||||
const output = new Array(keys.length);
|
const output = new Array(keys.length);
|
||||||
for (var i = 0; i < keys.length; i++) {
|
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
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
|
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.
|
// Verify that throwing in valueOf and toString still produces nice results.
|
||||||
[
|
[
|
||||||
[new String(55), "[String: '55']"],
|
[new String(55), "[String: '55']"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user