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: add subclass and null prototype support for errors in inspect #26923

Closed
wants to merge 1 commit into from
Closed
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
69 changes: 47 additions & 22 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -667,25 +667,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 @@ -845,6 +829,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 @@ -992,11 +1022,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 @@ -1660,6 +1660,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