From 7bf416a5ebde0f60e5354b2d0d0113c0c0885d42 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Tue, 26 Mar 2019 14:45:40 +0100 Subject: [PATCH] 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. --- lib/internal/util/inspect.js | 69 ++++++++++++++++++++---------- test/parallel/test-util-inspect.js | 64 +++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 22 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index dc0a1dcb279ff7..faa2b3c730a29a 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -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 @@ -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; @@ -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++) { diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index a474fd56e44b71..944f1c520adf8c 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -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']"],