From 3df083c5fb3f1ca4c69765b64a647555c89bc486 Mon Sep 17 00:00:00 2001 From: Anto Aravinth Date: Mon, 15 Oct 2018 06:51:15 +0530 Subject: [PATCH] util: handle null prototype on inspect This makes sure the prototype is always detected properly. Backport-PR-URL: https://github.com/nodejs/node/pull/23655 PR-URL: https://github.com/nodejs/node/pull/22331 Fixes: https://github.com/nodejs/node/issues/22141 Reviewed-By: Ruben Bridgewater Reviewed-By: Matteo Collina Reviewed-By: James M Snell Reviewed-By: John-David Dalton --- lib/internal/util/inspect.js | 87 ++++++++++++++++++++------- test/parallel/test-util-inspect.js | 96 ++++++++++++++++++++++++------ 2 files changed, 141 insertions(+), 42 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 5642d7e798dd69..6cec84355211d4 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -286,6 +286,7 @@ function getEmptyFormatArray() { } function getConstructorName(obj) { + let firstProto; while (obj) { const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); if (descriptor !== undefined && @@ -295,12 +296,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}] `; @@ -308,12 +325,6 @@ function getPrefix(constructor, tag, fallback) { return `${constructor} `; } - if (tag !== '') - return `[${tag}] `; - - if (fallback !== undefined) - return `${fallback} `; - return ''; } @@ -387,21 +398,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) { @@ -492,7 +531,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]}]`; @@ -500,21 +539,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; @@ -540,7 +582,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]}}`; } @@ -587,13 +629,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)} }`; @@ -658,9 +699,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 a6e6c2f1bf9737..f12b49c073b840 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -258,7 +258,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( @@ -266,7 +266,7 @@ assert.strictEqual( name: { value: 'Tim', enumerable: true }, hidden: { value: 'secret' } })), - "{ name: 'Tim' }" + "[Object: null prototype] { name: 'Tim' }" ); // Dynamic properties. @@ -502,11 +502,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] }' ); } @@ -1134,7 +1140,7 @@ if (typeof Symbol !== 'undefined') { { const x = Object.create(null); - assert.strictEqual(util.inspect(x), '{}'); + assert.strictEqual(util.inspect(x), '[Object: null prototype] {}'); } { @@ -1274,7 +1280,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' }"); @@ -1618,20 +1624,12 @@ util.inspect(process); 'prematurely. Maximum call stack size exceeded.]')); } -// 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. @@ -1667,6 +1665,39 @@ util.inspect(process); } }); 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] { [items unknown] }'], + [new WeakMap(), '[WeakMap: null prototype] { [items unknown] }'], + [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] [ 0, 0 ]'], + [new BigUint64Array(2), '[BigUint64Array: null prototype] [ 0, 0 ]'], + [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 @@ -1748,3 +1779,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 }'); +}