diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index dc66c5f9dfedd3..81d0f52fbf31cf 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -326,6 +326,7 @@ function getEmptyFormatArray() { } function getConstructorName(obj) { + let firstProto; while (obj) { const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); if (descriptor !== undefined && @@ -335,12 +336,28 @@ function getConstructorName(obj) { } obj = Object.getPrototypeOf(obj); + if (firstProto === undefined) { + firstProto = obj; + } + } + + if (firstProto === null) { + return null; } + // TODO(BridgeAR): Improve prototype inspection. + // We could use inspect on the prototype itself to improve the output. return ''; } function getPrefix(constructor, tag, fallback) { + if (constructor === null) { + if (tag !== '') { + return `[${fallback}: null prototype] [${tag}] `; + } + return `[${fallback}: null prototype] `; + } + if (constructor !== '') { if (tag !== '' && constructor !== tag) { return `${constructor} [${tag}] `; @@ -348,12 +365,6 @@ function getPrefix(constructor, tag, fallback) { return `${constructor} `; } - if (tag !== '') - return `[${tag}] `; - - if (fallback !== undefined) - return `${fallback} `; - return ''; } @@ -427,21 +438,49 @@ function findTypedConstructor(value) { } } +let lazyNullPrototypeCache; +// Creates a subclass and name +// the constructor as `${clazz} : null prototype` +function clazzWithNullPrototype(clazz, name) { + if (lazyNullPrototypeCache === undefined) { + lazyNullPrototypeCache = new Map(); + } else { + const cachedClass = lazyNullPrototypeCache.get(clazz); + if (cachedClass !== undefined) { + return cachedClass; + } + } + class NullPrototype extends clazz { + get [Symbol.toStringTag]() { + return ''; + } + } + Object.defineProperty(NullPrototype.prototype.constructor, 'name', + { value: `[${name}: null prototype]` }); + lazyNullPrototypeCache.set(clazz, NullPrototype); + return NullPrototype; +} + function noPrototypeIterator(ctx, value, recurseTimes) { let newVal; - // TODO: Create a Subclass in case there's no prototype and show - // `null-prototype`. if (isSet(value)) { - const clazz = Object.getPrototypeOf(value) || Set; + const clazz = Object.getPrototypeOf(value) || + clazzWithNullPrototype(Set, 'Set'); newVal = new clazz(setValues(value)); } else if (isMap(value)) { - const clazz = Object.getPrototypeOf(value) || Map; + const clazz = Object.getPrototypeOf(value) || + clazzWithNullPrototype(Map, 'Map'); newVal = new clazz(mapEntries(value)); } else if (Array.isArray(value)) { - const clazz = Object.getPrototypeOf(value) || Array; + const clazz = Object.getPrototypeOf(value) || + clazzWithNullPrototype(Array, 'Array'); newVal = new clazz(value.length || 0); } else if (isTypedArray(value)) { - const clazz = findTypedConstructor(value) || Uint8Array; + let clazz = Object.getPrototypeOf(value); + if (!clazz) { + const constructor = findTypedConstructor(value); + clazz = clazzWithNullPrototype(constructor, constructor.name); + } newVal = new clazz(value); } if (newVal) { @@ -527,7 +566,7 @@ function formatRaw(ctx, value, recurseTimes) { if (Array.isArray(value)) { keys = getOwnNonIndexProperties(value, filter); // Only set the constructor for non ordinary ("Array [...]") arrays. - const prefix = getPrefix(constructor, tag); + const prefix = getPrefix(constructor, tag, 'Array'); braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']']; if (value.length === 0 && keys.length === 0) return `${braces[0]}]`; @@ -535,21 +574,24 @@ function formatRaw(ctx, value, recurseTimes) { formatter = formatArray; } else if (isSet(value)) { keys = getKeys(value, ctx.showHidden); - const prefix = getPrefix(constructor, tag); + const prefix = getPrefix(constructor, tag, 'Set'); if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatSet; } else if (isMap(value)) { keys = getKeys(value, ctx.showHidden); - const prefix = getPrefix(constructor, tag); + const prefix = getPrefix(constructor, tag, 'Map'); if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatMap; } else if (isTypedArray(value)) { keys = getOwnNonIndexProperties(value, filter); - braces = [`${getPrefix(constructor, tag)}[`, ']']; + const prefix = constructor !== null ? + getPrefix(constructor, tag) : + getPrefix(constructor, tag, findTypedConstructor(value).name); + braces = [`${prefix}[`, ']']; if (value.length === 0 && keys.length === 0 && !ctx.showHidden) return `${braces[0]}]`; formatter = formatTypedArray; @@ -575,7 +617,7 @@ function formatRaw(ctx, value, recurseTimes) { return '[Arguments] {}'; braces[0] = '[Arguments] {'; } else if (tag !== '') { - braces[0] = `${getPrefix(constructor, tag)}{`; + braces[0] = `${getPrefix(constructor, tag, 'Object')}{`; if (keys.length === 0) { return `${braces[0]}}`; } @@ -622,13 +664,12 @@ function formatRaw(ctx, value, recurseTimes) { base = `[${base.slice(0, stackStart)}]`; } } else if (isAnyArrayBuffer(value)) { - let prefix = getPrefix(constructor, tag); - if (prefix === '') { - prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer '; - } // 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 arrayType = isArrayBuffer(value) ? 'ArrayBuffer' : + 'SharedArrayBuffer'; + const prefix = getPrefix(constructor, tag, arrayType); if (keys.length === 0) return prefix + `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; @@ -693,9 +734,9 @@ function formatRaw(ctx, value, recurseTimes) { } else if (keys.length === 0) { if (isExternal(value)) return ctx.stylize('[External]', 'special'); - return `${getPrefix(constructor, tag)}{}`; + return `${getPrefix(constructor, tag, 'Object')}{}`; } else { - braces[0] = `${getPrefix(constructor, tag)}{`; + braces[0] = `${getPrefix(constructor, tag, 'Object')}{`; } } } diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 1d37e97897dd34..c0fc3219ce736f 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -25,7 +25,7 @@ const common = require('../common'); const assert = require('assert'); const { internalBinding } = require('internal/test/binding'); -const { JSStream } = internalBinding('js_stream'); +const JSStream = process.binding('js_stream').JSStream; const util = require('util'); const vm = require('vm'); const { previewEntries } = internalBinding('util'); @@ -261,7 +261,7 @@ assert.strictEqual( name: { value: 'Tim', enumerable: true }, hidden: { value: 'secret' } }), { showHidden: true }), - "{ name: 'Tim', [hidden]: 'secret' }" + "[Object: null prototype] { name: 'Tim', [hidden]: 'secret' }" ); assert.strictEqual( @@ -269,7 +269,7 @@ assert.strictEqual( name: { value: 'Tim', enumerable: true }, hidden: { value: 'secret' } })), - "{ name: 'Tim' }" + "[Object: null prototype] { name: 'Tim' }" ); // Dynamic properties. @@ -505,11 +505,17 @@ assert.strictEqual(util.inspect(-5e-324), '-5e-324'); set: function() {} } }); - assert.strictEqual(util.inspect(getter, true), '{ [a]: [Getter] }'); - assert.strictEqual(util.inspect(setter, true), '{ [b]: [Setter] }'); + assert.strictEqual( + util.inspect(getter, true), + '[Object: null prototype] { [a]: [Getter] }' + ); + assert.strictEqual( + util.inspect(setter, true), + '[Object: null prototype] { [b]: [Setter] }' + ); assert.strictEqual( util.inspect(getterAndSetter, true), - '{ [c]: [Getter/Setter] }' + '[Object: null prototype] { [c]: [Getter/Setter] }' ); } @@ -1084,7 +1090,7 @@ if (typeof Symbol !== 'undefined') { { const x = Object.create(null); - assert.strictEqual(util.inspect(x), '{}'); + assert.strictEqual(util.inspect(x), '[Object: null prototype] {}'); } { @@ -1224,7 +1230,7 @@ util.inspect(process); assert.strictEqual(util.inspect( Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })), - '[foo] {}'); + '[Object: null prototype] [foo] {}'); assert.strictEqual(util.inspect(new Foo()), "Foo [bar] { foo: 'bar' }"); @@ -1574,20 +1580,12 @@ assert.strictEqual(util.inspect('"\''), '`"\'`'); // eslint-disable-next-line no-template-curly-in-string assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'"); -// 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 + util.inspect(Object.setPrototypeOf(/a/, null)), + '/undefined/undefined' ); -}); +} // Verify that throwing in valueOf and having no prototype still produces nice // results. @@ -1623,6 +1621,39 @@ assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'"); } }); assert.strictEqual(util.inspect(value), expected); + value.foo = 'bar'; + assert.notStrictEqual(util.inspect(value), expected); + delete value.foo; + value[Symbol('foo')] = 'yeah'; + assert.notStrictEqual(util.inspect(value), expected); +}); + +[ + [[1, 3, 4], '[Array: null prototype] [ 1, 3, 4 ]'], + [new Set([1, 2]), '[Set: null prototype] { 1, 2 }'], + [new Map([[1, 2]]), '[Map: null prototype] { 1 => 2 }'], + [new Promise((resolve) => setTimeout(resolve, 10)), + '[Promise: null prototype] { }'], + [new WeakSet(), '[WeakSet: null prototype] { }'], + [new WeakMap(), '[WeakMap: null prototype] { }'], + [new Uint8Array(2), '[Uint8Array: null prototype] [ 0, 0 ]'], + [new Uint16Array(2), '[Uint16Array: null prototype] [ 0, 0 ]'], + [new Uint32Array(2), '[Uint32Array: null prototype] [ 0, 0 ]'], + [new Int8Array(2), '[Int8Array: null prototype] [ 0, 0 ]'], + [new Int16Array(2), '[Int16Array: null prototype] [ 0, 0 ]'], + [new Int32Array(2), '[Int32Array: null prototype] [ 0, 0 ]'], + [new Float32Array(2), '[Float32Array: null prototype] [ 0, 0 ]'], + [new Float64Array(2), '[Float64Array: null prototype] [ 0, 0 ]'], + [new BigInt64Array(2), '[BigInt64Array: null prototype] [ 0n, 0n ]'], + [new BigUint64Array(2), '[BigUint64Array: null prototype] [ 0n, 0n ]'], + [new ArrayBuffer(16), '[ArrayBuffer: null prototype] ' + + '{ byteLength: undefined }'], + [new DataView(new ArrayBuffer(16)), + '[DataView: null prototype] {\n byteLength: undefined,\n ' + + 'byteOffset: undefined,\n buffer: undefined }'], + [new SharedArrayBuffer(2), '[SharedArrayBuffer: null prototype] ' + + '{ byteLength: undefined }'] +].forEach(([value, expected]) => { assert.strictEqual( util.inspect(Object.setPrototypeOf(value, null)), expected @@ -1706,3 +1737,30 @@ assert.strictEqual( '[ 3, 2, 1, [Symbol(a)]: false, [Symbol(b)]: true, a: 1, b: 2, c: 3 ]' ); } + +// Manipulate the prototype to one that we can not handle. +{ + let obj = { a: true }; + let value = (function() { return function() {}; })(); + Object.setPrototypeOf(value, null); + Object.setPrototypeOf(obj, value); + assert.strictEqual(util.inspect(obj), '{ a: true }'); + + obj = { a: true }; + value = []; + Object.setPrototypeOf(value, null); + Object.setPrototypeOf(obj, value); + assert.strictEqual(util.inspect(obj), '{ a: true }'); +} + +// Check that the fallback always works. +{ + const obj = new Set([1, 2]); + const iterator = obj[Symbol.iterator]; + Object.setPrototypeOf(obj, null); + Object.defineProperty(obj, Symbol.iterator, { + value: iterator, + configurable: true + }); + assert.strictEqual(util.inspect(obj), '[Set: null prototype] { 1, 2 }'); +}