util: handle null prototype on inspect
This makes sure the `null` prototype is always detected properly. PR-URL: https://github.com/nodejs/node/pull/22331 Fixes: https://github.com/nodejs/node/issues/22141 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
This commit is contained in:
parent
97f1e94fd5
commit
f4e4ef5cad
@ -326,6 +326,7 @@ function getEmptyFormatArray() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getConstructorName(obj) {
|
function getConstructorName(obj) {
|
||||||
|
let firstProto;
|
||||||
while (obj) {
|
while (obj) {
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
|
const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
|
||||||
if (descriptor !== undefined &&
|
if (descriptor !== undefined &&
|
||||||
@ -335,12 +336,28 @@ function getConstructorName(obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
obj = Object.getPrototypeOf(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 '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrefix(constructor, tag, fallback) {
|
function getPrefix(constructor, tag, fallback) {
|
||||||
|
if (constructor === null) {
|
||||||
|
if (tag !== '') {
|
||||||
|
return `[${fallback}: null prototype] [${tag}] `;
|
||||||
|
}
|
||||||
|
return `[${fallback}: null prototype] `;
|
||||||
|
}
|
||||||
|
|
||||||
if (constructor !== '') {
|
if (constructor !== '') {
|
||||||
if (tag !== '' && constructor !== tag) {
|
if (tag !== '' && constructor !== tag) {
|
||||||
return `${constructor} [${tag}] `;
|
return `${constructor} [${tag}] `;
|
||||||
@ -348,12 +365,6 @@ function getPrefix(constructor, tag, fallback) {
|
|||||||
return `${constructor} `;
|
return `${constructor} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag !== '')
|
|
||||||
return `[${tag}] `;
|
|
||||||
|
|
||||||
if (fallback !== undefined)
|
|
||||||
return `${fallback} `;
|
|
||||||
|
|
||||||
return '';
|
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) {
|
function noPrototypeIterator(ctx, value, recurseTimes) {
|
||||||
let newVal;
|
let newVal;
|
||||||
// TODO: Create a Subclass in case there's no prototype and show
|
|
||||||
// `null-prototype`.
|
|
||||||
if (isSet(value)) {
|
if (isSet(value)) {
|
||||||
const clazz = Object.getPrototypeOf(value) || Set;
|
const clazz = Object.getPrototypeOf(value) ||
|
||||||
|
clazzWithNullPrototype(Set, 'Set');
|
||||||
newVal = new clazz(setValues(value));
|
newVal = new clazz(setValues(value));
|
||||||
} else if (isMap(value)) {
|
} else if (isMap(value)) {
|
||||||
const clazz = Object.getPrototypeOf(value) || Map;
|
const clazz = Object.getPrototypeOf(value) ||
|
||||||
|
clazzWithNullPrototype(Map, 'Map');
|
||||||
newVal = new clazz(mapEntries(value));
|
newVal = new clazz(mapEntries(value));
|
||||||
} else if (Array.isArray(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);
|
newVal = new clazz(value.length || 0);
|
||||||
} else if (isTypedArray(value)) {
|
} 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);
|
newVal = new clazz(value);
|
||||||
}
|
}
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
@ -527,7 +566,7 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
keys = getOwnNonIndexProperties(value, filter);
|
keys = getOwnNonIndexProperties(value, filter);
|
||||||
// Only set the constructor for non ordinary ("Array [...]") arrays.
|
// Only set the constructor for non ordinary ("Array [...]") arrays.
|
||||||
const prefix = getPrefix(constructor, tag);
|
const prefix = getPrefix(constructor, tag, 'Array');
|
||||||
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
|
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
|
||||||
if (value.length === 0 && keys.length === 0)
|
if (value.length === 0 && keys.length === 0)
|
||||||
return `${braces[0]}]`;
|
return `${braces[0]}]`;
|
||||||
@ -535,21 +574,24 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
formatter = formatArray;
|
formatter = formatArray;
|
||||||
} else if (isSet(value)) {
|
} else if (isSet(value)) {
|
||||||
keys = getKeys(value, ctx.showHidden);
|
keys = getKeys(value, ctx.showHidden);
|
||||||
const prefix = getPrefix(constructor, tag);
|
const prefix = getPrefix(constructor, tag, 'Set');
|
||||||
if (value.size === 0 && keys.length === 0)
|
if (value.size === 0 && keys.length === 0)
|
||||||
return `${prefix}{}`;
|
return `${prefix}{}`;
|
||||||
braces = [`${prefix}{`, '}'];
|
braces = [`${prefix}{`, '}'];
|
||||||
formatter = formatSet;
|
formatter = formatSet;
|
||||||
} else if (isMap(value)) {
|
} else if (isMap(value)) {
|
||||||
keys = getKeys(value, ctx.showHidden);
|
keys = getKeys(value, ctx.showHidden);
|
||||||
const prefix = getPrefix(constructor, tag);
|
const prefix = getPrefix(constructor, tag, 'Map');
|
||||||
if (value.size === 0 && keys.length === 0)
|
if (value.size === 0 && keys.length === 0)
|
||||||
return `${prefix}{}`;
|
return `${prefix}{}`;
|
||||||
braces = [`${prefix}{`, '}'];
|
braces = [`${prefix}{`, '}'];
|
||||||
formatter = formatMap;
|
formatter = formatMap;
|
||||||
} else if (isTypedArray(value)) {
|
} else if (isTypedArray(value)) {
|
||||||
keys = getOwnNonIndexProperties(value, filter);
|
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)
|
if (value.length === 0 && keys.length === 0 && !ctx.showHidden)
|
||||||
return `${braces[0]}]`;
|
return `${braces[0]}]`;
|
||||||
formatter = formatTypedArray;
|
formatter = formatTypedArray;
|
||||||
@ -575,7 +617,7 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
return '[Arguments] {}';
|
return '[Arguments] {}';
|
||||||
braces[0] = '[Arguments] {';
|
braces[0] = '[Arguments] {';
|
||||||
} else if (tag !== '') {
|
} else if (tag !== '') {
|
||||||
braces[0] = `${getPrefix(constructor, tag)}{`;
|
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return `${braces[0]}}`;
|
return `${braces[0]}}`;
|
||||||
}
|
}
|
||||||
@ -622,13 +664,12 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
base = `[${base.slice(0, stackStart)}]`;
|
base = `[${base.slice(0, stackStart)}]`;
|
||||||
}
|
}
|
||||||
} else if (isAnyArrayBuffer(value)) {
|
} else if (isAnyArrayBuffer(value)) {
|
||||||
let prefix = getPrefix(constructor, tag);
|
|
||||||
if (prefix === '') {
|
|
||||||
prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer ';
|
|
||||||
}
|
|
||||||
// Fast path for ArrayBuffer and SharedArrayBuffer.
|
// Fast path for ArrayBuffer and SharedArrayBuffer.
|
||||||
// Can't do the same for DataView because it has a non-primitive
|
// Can't do the same for DataView because it has a non-primitive
|
||||||
// .buffer property that we need to recurse for.
|
// .buffer property that we need to recurse for.
|
||||||
|
const arrayType = isArrayBuffer(value) ? 'ArrayBuffer' :
|
||||||
|
'SharedArrayBuffer';
|
||||||
|
const prefix = getPrefix(constructor, tag, arrayType);
|
||||||
if (keys.length === 0)
|
if (keys.length === 0)
|
||||||
return prefix +
|
return prefix +
|
||||||
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
|
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
|
||||||
@ -693,9 +734,9 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
} else if (keys.length === 0) {
|
} else if (keys.length === 0) {
|
||||||
if (isExternal(value))
|
if (isExternal(value))
|
||||||
return ctx.stylize('[External]', 'special');
|
return ctx.stylize('[External]', 'special');
|
||||||
return `${getPrefix(constructor, tag)}{}`;
|
return `${getPrefix(constructor, tag, 'Object')}{}`;
|
||||||
} else {
|
} else {
|
||||||
braces[0] = `${getPrefix(constructor, tag)}{`;
|
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const { internalBinding } = require('internal/test/binding');
|
const { internalBinding } = require('internal/test/binding');
|
||||||
const { JSStream } = internalBinding('js_stream');
|
const JSStream = process.binding('js_stream').JSStream;
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const vm = require('vm');
|
const vm = require('vm');
|
||||||
const { previewEntries } = internalBinding('util');
|
const { previewEntries } = internalBinding('util');
|
||||||
@ -261,7 +261,7 @@ assert.strictEqual(
|
|||||||
name: { value: 'Tim', enumerable: true },
|
name: { value: 'Tim', enumerable: true },
|
||||||
hidden: { value: 'secret' }
|
hidden: { value: 'secret' }
|
||||||
}), { showHidden: true }),
|
}), { showHidden: true }),
|
||||||
"{ name: 'Tim', [hidden]: 'secret' }"
|
"[Object: null prototype] { name: 'Tim', [hidden]: 'secret' }"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
@ -269,7 +269,7 @@ assert.strictEqual(
|
|||||||
name: { value: 'Tim', enumerable: true },
|
name: { value: 'Tim', enumerable: true },
|
||||||
hidden: { value: 'secret' }
|
hidden: { value: 'secret' }
|
||||||
})),
|
})),
|
||||||
"{ name: 'Tim' }"
|
"[Object: null prototype] { name: 'Tim' }"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Dynamic properties.
|
// Dynamic properties.
|
||||||
@ -505,11 +505,17 @@ assert.strictEqual(util.inspect(-5e-324), '-5e-324');
|
|||||||
set: function() {}
|
set: function() {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assert.strictEqual(util.inspect(getter, true), '{ [a]: [Getter] }');
|
assert.strictEqual(
|
||||||
assert.strictEqual(util.inspect(setter, true), '{ [b]: [Setter] }');
|
util.inspect(getter, true),
|
||||||
|
'[Object: null prototype] { [a]: [Getter] }'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
util.inspect(setter, true),
|
||||||
|
'[Object: null prototype] { [b]: [Setter] }'
|
||||||
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
util.inspect(getterAndSetter, true),
|
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);
|
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(
|
assert.strictEqual(util.inspect(
|
||||||
Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })),
|
Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })),
|
||||||
'[foo] {}');
|
'[Object: null prototype] [foo] {}');
|
||||||
|
|
||||||
assert.strictEqual(util.inspect(new Foo()), "Foo [bar] { foo: 'bar' }");
|
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
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
|
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(
|
assert.strictEqual(
|
||||||
util.inspect(Object.setPrototypeOf(value, null)),
|
util.inspect(Object.setPrototypeOf(/a/, null)),
|
||||||
expected
|
'/undefined/undefined'
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
// Verify that throwing in valueOf and having no prototype still produces nice
|
// Verify that throwing in valueOf and having no prototype still produces nice
|
||||||
// results.
|
// results.
|
||||||
@ -1623,6 +1621,39 @@ assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
assert.strictEqual(util.inspect(value), expected);
|
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] { <pending> }'],
|
||||||
|
[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] [ 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(
|
assert.strictEqual(
|
||||||
util.inspect(Object.setPrototypeOf(value, null)),
|
util.inspect(Object.setPrototypeOf(value, null)),
|
||||||
expected
|
expected
|
||||||
@ -1706,3 +1737,30 @@ assert.strictEqual(
|
|||||||
'[ 3, 2, 1, [Symbol(a)]: false, [Symbol(b)]: true, a: 1, b: 2, c: 3 ]'
|
'[ 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 }');
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user