From e0cd3256ca6bca64b367fe16d14b23bbc8b83029 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 18 Jul 2018 13:14:36 +0200 Subject: [PATCH 1/3] util: harden util.inspect This makes sure values without prototype will still be inspected properly and do not cause errors. It restores the original information if possible. Besides that it fixes an issue with boxed symbols: extra keys were not visualized so far. --- lib/util.js | 204 +++++++++++++++++++---------- test/parallel/test-util-inspect.js | 83 ++++++++++++ 2 files changed, 218 insertions(+), 69 deletions(-) diff --git a/lib/util.js b/lib/util.js index c56b8429f47350..d268da05fef051 100644 --- a/lib/util.js +++ b/lib/util.js @@ -43,6 +43,7 @@ const types = internalBinding('types'); Object.assign(types, require('internal/util/types')); const { isAnyArrayBuffer, + isArrayBuffer, isArgumentsObject, isDataView, isExternal, @@ -55,7 +56,23 @@ const { isWeakSet, isRegExp, isDate, - isTypedArray + isTypedArray, + isStringObject, + isNumberObject, + isBooleanObject, + isSymbolObject, + isBigIntObject, + isUint8Array, + isUint8ClampedArray, + isUint16Array, + isUint32Array, + isInt8Array, + isInt16Array, + isInt32Array, + isFloat32Array, + isFloat64Array, + isBigInt64Array, + isBigUint64Array } = types; const { @@ -84,6 +101,16 @@ const regExpToString = RegExp.prototype.toString; const dateToISOString = Date.prototype.toISOString; const errorToString = Error.prototype.toString; +const bigIntValueOf = BigInt.prototype.valueOf; +const booleanValueOf = Boolean.prototype.valueOf; +const numberValueOf = Number.prototype.valueOf; +const symbolValueOf = Symbol.prototype.valueOf; +const stringValueOf = String.prototype.valueOf; + +const setValues = Set.prototype.values; +const mapEntries = Map.prototype.entries; +const dateGetTime = Date.prototype.getTime; + let CIRCULAR_ERROR_MESSAGE; let internalDeepEqual; @@ -455,6 +482,36 @@ function getPrefix(constructor, tag) { return ''; } +function addExtraKeys(source, target, keys) { + for (const key of keys) { + target[key] = source[key]; + } + return target; +} + +function findTypedConstructor(value) { + for (const [check, clazz] of [ + [isUint8Array, Uint8Array], + [isUint8ClampedArray, Uint8ClampedArray], + [isUint16Array, Uint16Array], + [isUint32Array, Uint32Array], + [isInt8Array, Int8Array], + [isInt16Array, Int16Array], + [isInt32Array, Int32Array], + [isFloat32Array, Float32Array], + [isFloat64Array, Float64Array], + [isBigInt64Array, BigInt64Array], + [isBigUint64Array, BigUint64Array] + ]) { + if (check(value)) { + return new clazz(value); + } + } + return value; +} + +const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor); + function formatValue(ctx, value, recurseTimes) { // Primitive types cannot have properties if (typeof value !== 'object' && typeof value !== 'function') { @@ -548,8 +605,8 @@ function formatValue(ctx, value, recurseTimes) { let formatter = formatObject; let braces; let noIterator = true; - let raw; let extra; + let i = 0; // Iterators and the rest are split to reduce checks if (value[Symbol.iterator]) { @@ -583,34 +640,16 @@ function formatValue(ctx, value, recurseTimes) { braces = [`[${tag}] {`, '}']; formatter = formatSetIterator; } else { - // Check for boxed strings with valueOf() - // The .valueOf() call can fail for a multitude of reasons - try { - raw = value.valueOf(); - } catch (e) { /* ignore */ } - - if (typeof raw === 'string') { - const formatted = formatPrimitive(stylizeNoColor, raw, ctx); - if (keyLength === raw.length) - return ctx.stylize(`[String: ${formatted}]`, 'string'); - base = `[String: ${formatted}]`; - // For boxed Strings, we have to remove the 0-n indexed entries, - // since they just noisy up the output and are redundant - // Make boxed primitive Strings look like such - keys = keys.slice(value.length); - braces = ['{', '}']; - } else { - noIterator = true; - } + noIterator = true; } } if (noIterator) { braces = ['{', '}']; if (constructor === 'Object') { if (isArgumentsObject(value)) { - braces[0] = '[Arguments] {'; if (keyLength === 0) return '[Arguments] {}'; + braces[0] = '[Arguments] {'; } else if (tag !== '') { braces[0] = `${getPrefix(constructor, tag)}{`; if (keyLength === 0) { @@ -620,8 +659,8 @@ function formatValue(ctx, value, recurseTimes) { return '{}'; } } else if (typeof value === 'function') { - const name = - `${constructor || tag}${value.name ? `: ${value.name}` : ''}`; + const type = constructor || tag || 'Function'; + const name = `${type}${value.name ? `: ${value.name}` : ''}`; if (keyLength === 0) return ctx.stylize(`[${name}]`, 'special'); base = `[${name}]`; @@ -631,12 +670,12 @@ function formatValue(ctx, value, recurseTimes) { return ctx.stylize(regExpToString.call(value), 'regexp'); base = `${regExpToString.call(value)}`; } else if (isDate(value)) { + // Make dates with properties first say the date if (keyLength === 0) { - if (Number.isNaN(value.getTime())) - return ctx.stylize(value.toString(), 'date'); + if (Number.isNaN(dateGetTime.call(value))) + return ctx.stylize(String(value), 'date'); return ctx.stylize(dateToISOString.call(value), 'date'); } - // Make dates with properties first say the date base = dateToISOString.call(value); } else if (isError(value)) { // Make error with message first say the error @@ -662,28 +701,31 @@ function formatValue(ctx, value, recurseTimes) { // Fast path for ArrayBuffer and SharedArrayBuffer. // Can't do the same for DataView because it has a non-primitive // .buffer property that we need to recurse for. - const prefix = getPrefix(constructor, tag); + let prefix = getPrefix(constructor, tag); + if (prefix === '') { + prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer '; + } if (keyLength === 0) return prefix + `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; braces[0] = `${prefix}{`; keys.unshift('byteLength'); } else if (isDataView(value)) { - braces[0] = `${getPrefix(constructor, tag)}{`; + braces[0] = `${getPrefix(constructor, tag) || 'DataView '}{`; // .buffer goes last, it's not a primitive like the others. keys.unshift('byteLength', 'byteOffset', 'buffer'); } else if (isPromise(value)) { - braces[0] = `${getPrefix(constructor, tag)}{`; + braces[0] = `${getPrefix(constructor, tag) || 'Promise '}{`; formatter = formatPromise; } else if (isWeakSet(value)) { - braces[0] = `${getPrefix(constructor, tag)}{`; + braces[0] = `${getPrefix(constructor, tag) || 'WeakSet '}{`; if (ctx.showHidden) { formatter = formatWeakSet; } else { extra = ''; } } else if (isWeakMap(value)) { - braces[0] = `${getPrefix(constructor, tag)}{`; + braces[0] = `${getPrefix(constructor, tag) || 'WeakMap '}{`; if (ctx.showHidden) { formatter = formatWeakMap; } else { @@ -692,43 +734,67 @@ function formatValue(ctx, value, recurseTimes) { } else if (types.isModuleNamespaceObject(value)) { braces[0] = `[${tag}] {`; formatter = formatNamespaceObject; + } else if (isNumberObject(value)) { + base = `[Number: ${getBoxedValue(numberValueOf.call(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'number'); + } else if (isBooleanObject(value)) { + base = `[Boolean: ${getBoxedValue(booleanValueOf.call(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'boolean'); + } else if (isBigIntObject(value)) { + base = `[BigInt: ${getBoxedValue(bigIntValueOf.call(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'bigint'); + } else if (isSymbolObject(value)) { + base = `[Symbol: ${getBoxedValue(symbolValueOf.call(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'symbol'); + } else if (isStringObject(value)) { + const raw = stringValueOf.call(value); + base = `[String: ${getBoxedValue(raw, ctx)}]`; + if (keyLength === raw.length) + return ctx.stylize(base, 'string'); + // For boxed Strings, we have to remove the 0-n indexed entries, + // since they just noisy up the output and are redundant + // Make boxed primitive Strings look like such + keys = keys.slice(value.length); + braces = ['{', '}']; + // The input prototype got manipulated. Special handle these. + // We have to rebuild the information so we are able to display everything. + } else if (isSet(value)) { + const newVal = addExtraKeys(value, new Set(setValues.call(value)), keys); + return formatValue(ctx, newVal, recurseTimes); + } else if (isMap(value)) { + const newVal = addExtraKeys(value, new Map(mapEntries.call(value)), keys); + return formatValue(ctx, newVal, recurseTimes); + } else if (Array.isArray(value)) { + // The prefix is not always possible to fully reconstruct. + const prefix = getPrefix(constructor, tag); + braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']']; + formatter = formatArray; + const newValue = []; + newValue.length = value.length; + value = addExtraKeys(value, newValue, keys); + } else if (isTypedArray(value)) { + const newValue = findTypedConstructor(value); + value = addExtraKeys(value, newValue, keys.slice(newValue.length)); + // The prefix is not always possible to fully reconstruct. + braces = [`${getPrefix(getConstructorName(value), tag)}[`, ']']; + formatter = formatTypedArray; + } else if (isMapIterator(value)) { + braces = [`[${tag || 'Map Iterator'}] {`, '}']; + formatter = formatMapIterator; + } else if (isSetIterator(value)) { + braces = [`[${tag || 'Set Iterator'}] {`, '}']; + formatter = formatSetIterator; + // Handle other regular objects again. + } else if (keyLength === 0) { + if (isExternal(value)) + return ctx.stylize('[External]', 'special'); + return `${getPrefix(constructor, tag)}{}`; } else { - // Check boxed primitives other than string with valueOf() - // NOTE: `Date` has to be checked first! - // The .valueOf() call can fail for a multitude of reasons - try { - raw = value.valueOf(); - } catch (e) { /* ignore */ } - - if (typeof raw === 'number') { - // Make boxed primitive Numbers look like such - const formatted = formatPrimitive(stylizeNoColor, raw); - if (keyLength === 0) - return ctx.stylize(`[Number: ${formatted}]`, 'number'); - base = `[Number: ${formatted}]`; - } else if (typeof raw === 'boolean') { - // Make boxed primitive Booleans look like such - const formatted = formatPrimitive(stylizeNoColor, raw); - if (keyLength === 0) - return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean'); - base = `[Boolean: ${formatted}]`; - // eslint-disable-next-line valid-typeof - } else if (typeof raw === 'bigint') { - // Make boxed primitive BigInts look like such - const formatted = formatPrimitive(stylizeNoColor, raw); - if (keyLength === 0) - return ctx.stylize(`[BigInt: ${formatted}]`, 'bigint'); - base = `[BigInt: ${formatted}]`; - } else if (typeof raw === 'symbol') { - const formatted = formatPrimitive(stylizeNoColor, raw); - return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol'); - } else if (keyLength === 0) { - if (isExternal(value)) - return ctx.stylize('[External]', 'special'); - return `${getPrefix(constructor, tag)}{}`; - } else { - braces[0] = `${getPrefix(constructor, tag)}{`; - } + braces[0] = `${getPrefix(constructor, tag)}{`; } } @@ -761,7 +827,7 @@ function formatValue(ctx, value, recurseTimes) { if (extra !== undefined) output.unshift(extra); - for (var i = 0; i < symbols.length; i++) { + for (i = 0; i < symbols.length; i++) { output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0)); } diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 0dc3f11bfe5d79..09ddc85b1b2c9d 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -801,6 +801,14 @@ assert.strictEqual(util.inspect(new Number(13.37)), '[Number: 13.37]'); const num = new Number(13.37); num.foo = 'bar'; assert.strictEqual(util.inspect(num), "{ [Number: 13.37] foo: 'bar' }"); + + const sym = Object(Symbol('foo')); + sym.foo = 'bar'; + assert.strictEqual(util.inspect(sym), "{ [Symbol: Symbol(foo)] foo: 'bar' }"); + + const big = Object(BigInt(55)); + big.foo = 'bar'; + assert.strictEqual(util.inspect(big), "{ [BigInt: 55n] foo: 'bar' }"); } // Test es6 Symbol. @@ -1430,3 +1438,78 @@ assert.strictEqual(util.inspect("'"), '"\'"'); assert.strictEqual(util.inspect('"\''), '`"\'`'); // eslint-disable-next-line no-template-curly-in-string assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'"); + +// Manipulating the Symbol.iterator should still produce nice results. +[ + [[1, 2], '[ 1, 2 ]'], + [[, , 5, , , , ], '[ <2 empty items>, 5, <3 empty items> ]'], + [new Set([1, 2]), 'Set { 1, 2 }'], + [new Map([[1, 2]]), 'Map { 1 => 2 }'], + [new Uint8Array(2), 'Uint8Array [ 0, 0 ]'], + // It seems like the following can not be fully restored :( + [new Set([1, 2]).entries(), 'Object [Set Iterator] {}'], + [new Map([[1, 2]]).keys(), 'Object [Map Iterator] {}'], +].forEach(([value, expected]) => { + // "Remove the Symbol.iterator" + Object.defineProperty(value, Symbol.iterator, { + value: false + }); + assert.strictEqual(util.inspect(value), expected); +}); + +// Verify the output in case the value has no prototype. +// Sadly, these cases can not be fully inspected :( +[ + [/a/, '/undefined/undefined'], + [new DataView(new ArrayBuffer(2)), + 'DataView {\n byteLength: undefined,\n byteOffset: undefined,\n ' + + 'buffer: undefined }'], + [new SharedArrayBuffer(2), 'SharedArrayBuffer { byteLength: undefined }'] +].forEach(([value, expected]) => { + assert.strictEqual( + util.inspect(Object.setPrototypeOf(value, null)), + expected + ); +}); + +// Verify that throwing in valueOf and having no prototype still produces nice +// results. +[ + [new String(55), "[String: '55']"], + [new Boolean(true), '[Boolean: true]'], + [new Number(55), '[Number: 55]'], + [Object(BigInt(55)), '[BigInt: 55n]'], + [Object(Symbol('foo')), '[Symbol: Symbol(foo)]'], + [function() {}, '[Function]'], + [() => {}, '[Function]'], + [[1, 2], '[ 1, 2 ]'], + [[, , 5, , , , ], '[ <2 empty items>, 5, <3 empty items> ]'], + [{ a: 5 }, '{ a: 5 }'], + [new Set([1, 2]), 'Set { 1, 2 }'], + [new Map([[1, 2]]), 'Map { 1 => 2 }'], + [new Set([1, 2]).entries(), '[Set Iterator] { 1, 2 }'], + [new Map([[1, 2]]).keys(), '[Map Iterator] { 1 }'], + [new Date(2000), '1970-01-01T00:00:02.000Z'], + [new Uint8Array(2), 'Uint8Array [ 0, 0 ]'], + [new Promise((resolve) => setTimeout(resolve, 10)), 'Promise { }'], + [new WeakSet(), 'WeakSet { }'], + [new WeakMap(), 'WeakMap { }'], +].forEach(([value, expected]) => { + Object.defineProperty(value, 'valueOf', { + get() { + throw new Error('valueOf'); + } + }); + Object.defineProperty(value, 'toString', { + get() { + throw new Error('toString'); + } + }); + assert.strictEqual(util.inspect(value), expected); + assert.strictEqual( + util.inspect(Object.setPrototypeOf(value, null)), + expected + ); + value.foo = 'bar'; + assert.notStrictEqual(util.inspect(value), expected); +}); From 173e72b43388f0bd29e58adf7d13baa820603afc Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Fri, 27 Jul 2018 15:23:37 +0200 Subject: [PATCH 2/3] fixup: address comment --- lib/util.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/util.js b/lib/util.js index d268da05fef051..99bc16f28b56b7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -468,7 +468,7 @@ function getConstructorName(obj) { return ''; } -function getPrefix(constructor, tag) { +function getPrefix(constructor, tag, fallback) { if (constructor !== '') { if (tag !== '' && constructor !== tag) { return `${constructor} [${tag}] `; @@ -479,6 +479,9 @@ function getPrefix(constructor, tag) { if (tag !== '') return `[${tag}] `; + if (fallback !== undefined) + return `${fallback} `; + return ''; } @@ -711,21 +714,21 @@ function formatValue(ctx, value, recurseTimes) { braces[0] = `${prefix}{`; keys.unshift('byteLength'); } else if (isDataView(value)) { - braces[0] = `${getPrefix(constructor, tag) || 'DataView '}{`; + braces[0] = `${getPrefix(constructor, tag, 'DataView')}{`; // .buffer goes last, it's not a primitive like the others. keys.unshift('byteLength', 'byteOffset', 'buffer'); } else if (isPromise(value)) { - braces[0] = `${getPrefix(constructor, tag) || 'Promise '}{`; + braces[0] = `${getPrefix(constructor, tag, 'Promise')}{`; formatter = formatPromise; } else if (isWeakSet(value)) { - braces[0] = `${getPrefix(constructor, tag) || 'WeakSet '}{`; + braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`; if (ctx.showHidden) { formatter = formatWeakSet; } else { extra = ''; } } else if (isWeakMap(value)) { - braces[0] = `${getPrefix(constructor, tag) || 'WeakMap '}{`; + braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`; if (ctx.showHidden) { formatter = formatWeakMap; } else { From fa542c63e90b7d4738ae04b60d460355778ec5cf Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Fri, 27 Jul 2018 15:28:59 +0200 Subject: [PATCH 3/3] fixup: use reflect apply --- lib/util.js | 63 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/lib/util.js b/lib/util.js index 99bc16f28b56b7..18fcee0282abbb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -96,20 +96,31 @@ const inspectDefaultOptions = Object.seal({ compact: true }); -const propertyIsEnumerable = Object.prototype.propertyIsEnumerable; -const regExpToString = RegExp.prototype.toString; -const dateToISOString = Date.prototype.toISOString; -const errorToString = Error.prototype.toString; +const ReflectApply = Reflect.apply; + +// This function is borrowed from the function with the same name on V8 Extras' +// `utils` object. V8 implements Reflect.apply very efficiently in conjunction +// with the spread syntax, such that no additional special case is needed for +// function calls w/o arguments. +// Refs: https://github.com/v8/v8/blob/d6ead37d265d7215cf9c5f768f279e21bd170212/src/js/prologue.js#L152-L156 +function uncurryThis(func) { + return (thisArg, ...args) => ReflectApply(func, thisArg, args); +} + +const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable); +const regExpToString = uncurryThis(RegExp.prototype.toString); +const dateToISOString = uncurryThis(Date.prototype.toISOString); +const errorToString = uncurryThis(Error.prototype.toString); -const bigIntValueOf = BigInt.prototype.valueOf; -const booleanValueOf = Boolean.prototype.valueOf; -const numberValueOf = Number.prototype.valueOf; -const symbolValueOf = Symbol.prototype.valueOf; -const stringValueOf = String.prototype.valueOf; +const bigIntValueOf = uncurryThis(BigInt.prototype.valueOf); +const booleanValueOf = uncurryThis(Boolean.prototype.valueOf); +const numberValueOf = uncurryThis(Number.prototype.valueOf); +const symbolValueOf = uncurryThis(Symbol.prototype.valueOf); +const stringValueOf = uncurryThis(String.prototype.valueOf); -const setValues = Set.prototype.values; -const mapEntries = Map.prototype.entries; -const dateGetTime = Date.prototype.getTime; +const setValues = uncurryThis(Set.prototype.values); +const mapEntries = uncurryThis(Map.prototype.entries); +const dateGetTime = uncurryThis(Date.prototype.getTime); let CIRCULAR_ERROR_MESSAGE; let internalDeepEqual; @@ -595,7 +606,7 @@ function formatValue(ctx, value, recurseTimes) { } if (symbols.length !== 0) - symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key)); + symbols = symbols.filter((key) => propertyIsEnumerable(value, key)); } const keyLength = keys.length + symbols.length; @@ -670,16 +681,16 @@ function formatValue(ctx, value, recurseTimes) { } else if (isRegExp(value)) { // Make RegExps say that they are RegExps if (keyLength === 0 || recurseTimes < 0) - return ctx.stylize(regExpToString.call(value), 'regexp'); - base = `${regExpToString.call(value)}`; + return ctx.stylize(regExpToString(value), 'regexp'); + base = `${regExpToString(value)}`; } else if (isDate(value)) { // Make dates with properties first say the date if (keyLength === 0) { - if (Number.isNaN(dateGetTime.call(value))) + if (Number.isNaN(dateGetTime(value))) return ctx.stylize(String(value), 'date'); - return ctx.stylize(dateToISOString.call(value), 'date'); + return ctx.stylize(dateToISOString(value), 'date'); } - base = dateToISOString.call(value); + base = dateToISOString(value); } else if (isError(value)) { // Make error with message first say the error base = formatError(value); @@ -738,23 +749,23 @@ function formatValue(ctx, value, recurseTimes) { braces[0] = `[${tag}] {`; formatter = formatNamespaceObject; } else if (isNumberObject(value)) { - base = `[Number: ${getBoxedValue(numberValueOf.call(value))}]`; + base = `[Number: ${getBoxedValue(numberValueOf(value))}]`; if (keyLength === 0) return ctx.stylize(base, 'number'); } else if (isBooleanObject(value)) { - base = `[Boolean: ${getBoxedValue(booleanValueOf.call(value))}]`; + base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`; if (keyLength === 0) return ctx.stylize(base, 'boolean'); } else if (isBigIntObject(value)) { - base = `[BigInt: ${getBoxedValue(bigIntValueOf.call(value))}]`; + base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`; if (keyLength === 0) return ctx.stylize(base, 'bigint'); } else if (isSymbolObject(value)) { - base = `[Symbol: ${getBoxedValue(symbolValueOf.call(value))}]`; + base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`; if (keyLength === 0) return ctx.stylize(base, 'symbol'); } else if (isStringObject(value)) { - const raw = stringValueOf.call(value); + const raw = stringValueOf(value); base = `[String: ${getBoxedValue(raw, ctx)}]`; if (keyLength === raw.length) return ctx.stylize(base, 'string'); @@ -766,10 +777,10 @@ function formatValue(ctx, value, recurseTimes) { // The input prototype got manipulated. Special handle these. // We have to rebuild the information so we are able to display everything. } else if (isSet(value)) { - const newVal = addExtraKeys(value, new Set(setValues.call(value)), keys); + const newVal = addExtraKeys(value, new Set(setValues(value)), keys); return formatValue(ctx, newVal, recurseTimes); } else if (isMap(value)) { - const newVal = addExtraKeys(value, new Map(mapEntries.call(value)), keys); + const newVal = addExtraKeys(value, new Map(mapEntries(value)), keys); return formatValue(ctx, newVal, recurseTimes); } else if (Array.isArray(value)) { // The prefix is not always possible to fully reconstruct. @@ -900,7 +911,7 @@ function formatPrimitive(fn, value, ctx) { } function formatError(value) { - return value.stack || errorToString.call(value); + return value.stack || errorToString(value); } function formatObject(ctx, value, recurseTimes, keys) {