diff --git a/lib/util.js b/lib/util.js index c56b8429f47350..18fcee0282abbb 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 { @@ -79,10 +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 = 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 = uncurryThis(Set.prototype.values); +const mapEntries = uncurryThis(Map.prototype.entries); +const dateGetTime = uncurryThis(Date.prototype.getTime); let CIRCULAR_ERROR_MESSAGE; let internalDeepEqual; @@ -441,7 +479,7 @@ function getConstructorName(obj) { return ''; } -function getPrefix(constructor, tag) { +function getPrefix(constructor, tag, fallback) { if (constructor !== '') { if (tag !== '' && constructor !== tag) { return `${constructor} [${tag}] `; @@ -452,9 +490,42 @@ function getPrefix(constructor, tag) { if (tag !== '') return `[${tag}] `; + if (fallback !== undefined) + return `${fallback} `; + 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') { @@ -535,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; @@ -548,8 +619,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 +654,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,24 +673,24 @@ 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}]`; } 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(value.getTime())) - return ctx.stylize(value.toString(), 'date'); - return ctx.stylize(dateToISOString.call(value), 'date'); + if (Number.isNaN(dateGetTime(value))) + return ctx.stylize(String(value), 'date'); + return ctx.stylize(dateToISOString(value), 'date'); } - // Make dates with properties first say the date - base = dateToISOString.call(value); + base = dateToISOString(value); } else if (isError(value)) { // Make error with message first say the error base = formatError(value); @@ -662,28 +715,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 +748,67 @@ function formatValue(ctx, value, recurseTimes) { } else if (types.isModuleNamespaceObject(value)) { braces[0] = `[${tag}] {`; formatter = formatNamespaceObject; + } else if (isNumberObject(value)) { + base = `[Number: ${getBoxedValue(numberValueOf(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'number'); + } else if (isBooleanObject(value)) { + base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'boolean'); + } else if (isBigIntObject(value)) { + base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'bigint'); + } else if (isSymbolObject(value)) { + base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`; + if (keyLength === 0) + return ctx.stylize(base, 'symbol'); + } else if (isStringObject(value)) { + const raw = stringValueOf(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(value)), keys); + return formatValue(ctx, newVal, recurseTimes); + } else if (isMap(value)) { + 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. + 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 +841,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)); } @@ -831,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) { 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); +});