Skip to content

Commit a39f341

Browse files
antsmartianabelginrayen
authored and
abelginrayen
committed
util: handle null prototype on inspect
This makes sure the prototype is always detected properly. PR-URL: nodejs#22331 Fixes: nodejs#22141 Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: John-David Dalton <[email protected]>
1 parent 8e7a12f commit a39f341

File tree

2 files changed

+141
-42
lines changed

2 files changed

+141
-42
lines changed

lib/internal/util/inspect.js

+64-23
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ function getEmptyFormatArray() {
286286
}
287287

288288
function getConstructorName(obj) {
289+
let firstProto;
289290
while (obj) {
290291
const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
291292
if (descriptor !== undefined &&
@@ -295,25 +296,35 @@ function getConstructorName(obj) {
295296
}
296297

297298
obj = Object.getPrototypeOf(obj);
299+
if (firstProto === undefined) {
300+
firstProto = obj;
301+
}
302+
}
303+
304+
if (firstProto === null) {
305+
return null;
298306
}
307+
// TODO(BridgeAR): Improve prototype inspection.
308+
// We could use inspect on the prototype itself to improve the output.
299309

300310
return '';
301311
}
302312

303313
function getPrefix(constructor, tag, fallback) {
314+
if (constructor === null) {
315+
if (tag !== '') {
316+
return `[${fallback}: null prototype] [${tag}] `;
317+
}
318+
return `[${fallback}: null prototype] `;
319+
}
320+
304321
if (constructor !== '') {
305322
if (tag !== '' && constructor !== tag) {
306323
return `${constructor} [${tag}] `;
307324
}
308325
return `${constructor} `;
309326
}
310327

311-
if (tag !== '')
312-
return `[${tag}] `;
313-
314-
if (fallback !== undefined)
315-
return `${fallback} `;
316-
317328
return '';
318329
}
319330

@@ -387,21 +398,49 @@ function findTypedConstructor(value) {
387398
}
388399
}
389400

401+
let lazyNullPrototypeCache;
402+
// Creates a subclass and name
403+
// the constructor as `${clazz} : null prototype`
404+
function clazzWithNullPrototype(clazz, name) {
405+
if (lazyNullPrototypeCache === undefined) {
406+
lazyNullPrototypeCache = new Map();
407+
} else {
408+
const cachedClass = lazyNullPrototypeCache.get(clazz);
409+
if (cachedClass !== undefined) {
410+
return cachedClass;
411+
}
412+
}
413+
class NullPrototype extends clazz {
414+
get [Symbol.toStringTag]() {
415+
return '';
416+
}
417+
}
418+
Object.defineProperty(NullPrototype.prototype.constructor, 'name',
419+
{ value: `[${name}: null prototype]` });
420+
lazyNullPrototypeCache.set(clazz, NullPrototype);
421+
return NullPrototype;
422+
}
423+
390424
function noPrototypeIterator(ctx, value, recurseTimes) {
391425
let newVal;
392-
// TODO: Create a Subclass in case there's no prototype and show
393-
// `null-prototype`.
394426
if (isSet(value)) {
395-
const clazz = Object.getPrototypeOf(value) || Set;
427+
const clazz = Object.getPrototypeOf(value) ||
428+
clazzWithNullPrototype(Set, 'Set');
396429
newVal = new clazz(setValues(value));
397430
} else if (isMap(value)) {
398-
const clazz = Object.getPrototypeOf(value) || Map;
431+
const clazz = Object.getPrototypeOf(value) ||
432+
clazzWithNullPrototype(Map, 'Map');
399433
newVal = new clazz(mapEntries(value));
400434
} else if (Array.isArray(value)) {
401-
const clazz = Object.getPrototypeOf(value) || Array;
435+
const clazz = Object.getPrototypeOf(value) ||
436+
clazzWithNullPrototype(Array, 'Array');
402437
newVal = new clazz(value.length || 0);
403438
} else if (isTypedArray(value)) {
404-
const clazz = findTypedConstructor(value) || Uint8Array;
439+
let clazz = Object.getPrototypeOf(value);
440+
if (!clazz) {
441+
const constructor = findTypedConstructor(value);
442+
clazz = clazzWithNullPrototype(constructor, constructor.name);
443+
}
405444
newVal = new clazz(value);
406445
}
407446
if (newVal) {
@@ -492,29 +531,32 @@ function formatRaw(ctx, value, recurseTimes) {
492531
if (Array.isArray(value)) {
493532
keys = getOwnNonIndexProperties(value, filter);
494533
// Only set the constructor for non ordinary ("Array [...]") arrays.
495-
const prefix = getPrefix(constructor, tag);
534+
const prefix = getPrefix(constructor, tag, 'Array');
496535
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
497536
if (value.length === 0 && keys.length === 0)
498537
return `${braces[0]}]`;
499538
extrasType = kArrayExtrasType;
500539
formatter = formatArray;
501540
} else if (isSet(value)) {
502541
keys = getKeys(value, ctx.showHidden);
503-
const prefix = getPrefix(constructor, tag);
542+
const prefix = getPrefix(constructor, tag, 'Set');
504543
if (value.size === 0 && keys.length === 0)
505544
return `${prefix}{}`;
506545
braces = [`${prefix}{`, '}'];
507546
formatter = formatSet;
508547
} else if (isMap(value)) {
509548
keys = getKeys(value, ctx.showHidden);
510-
const prefix = getPrefix(constructor, tag);
549+
const prefix = getPrefix(constructor, tag, 'Map');
511550
if (value.size === 0 && keys.length === 0)
512551
return `${prefix}{}`;
513552
braces = [`${prefix}{`, '}'];
514553
formatter = formatMap;
515554
} else if (isTypedArray(value)) {
516555
keys = getOwnNonIndexProperties(value, filter);
517-
braces = [`${getPrefix(constructor, tag)}[`, ']'];
556+
const prefix = constructor !== null ?
557+
getPrefix(constructor, tag) :
558+
getPrefix(constructor, tag, findTypedConstructor(value).name);
559+
braces = [`${prefix}[`, ']'];
518560
if (value.length === 0 && keys.length === 0 && !ctx.showHidden)
519561
return `${braces[0]}]`;
520562
formatter = formatTypedArray;
@@ -540,7 +582,7 @@ function formatRaw(ctx, value, recurseTimes) {
540582
return '[Arguments] {}';
541583
braces[0] = '[Arguments] {';
542584
} else if (tag !== '') {
543-
braces[0] = `${getPrefix(constructor, tag)}{`;
585+
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
544586
if (keys.length === 0) {
545587
return `${braces[0]}}`;
546588
}
@@ -587,13 +629,12 @@ function formatRaw(ctx, value, recurseTimes) {
587629
base = `[${base.slice(0, stackStart)}]`;
588630
}
589631
} else if (isAnyArrayBuffer(value)) {
590-
let prefix = getPrefix(constructor, tag);
591-
if (prefix === '') {
592-
prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer ';
593-
}
594632
// Fast path for ArrayBuffer and SharedArrayBuffer.
595633
// Can't do the same for DataView because it has a non-primitive
596634
// .buffer property that we need to recurse for.
635+
const arrayType = isArrayBuffer(value) ? 'ArrayBuffer' :
636+
'SharedArrayBuffer';
637+
const prefix = getPrefix(constructor, tag, arrayType);
597638
if (keys.length === 0)
598639
return prefix +
599640
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
@@ -658,9 +699,9 @@ function formatRaw(ctx, value, recurseTimes) {
658699
} else if (keys.length === 0) {
659700
if (isExternal(value))
660701
return ctx.stylize('[External]', 'special');
661-
return `${getPrefix(constructor, tag)}{}`;
702+
return `${getPrefix(constructor, tag, 'Object')}{}`;
662703
} else {
663-
braces[0] = `${getPrefix(constructor, tag)}{`;
704+
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
664705
}
665706
}
666707
}

test/parallel/test-util-inspect.js

+77-19
Original file line numberDiff line numberDiff line change
@@ -258,15 +258,15 @@ assert.strictEqual(
258258
name: { value: 'Tim', enumerable: true },
259259
hidden: { value: 'secret' }
260260
}), { showHidden: true }),
261-
"{ name: 'Tim', [hidden]: 'secret' }"
261+
"[Object: null prototype] { name: 'Tim', [hidden]: 'secret' }"
262262
);
263263

264264
assert.strictEqual(
265265
util.inspect(Object.create(null, {
266266
name: { value: 'Tim', enumerable: true },
267267
hidden: { value: 'secret' }
268268
})),
269-
"{ name: 'Tim' }"
269+
"[Object: null prototype] { name: 'Tim' }"
270270
);
271271

272272
// Dynamic properties.
@@ -502,11 +502,17 @@ assert.strictEqual(util.inspect(-5e-324), '-5e-324');
502502
set: function() {}
503503
}
504504
});
505-
assert.strictEqual(util.inspect(getter, true), '{ [a]: [Getter] }');
506-
assert.strictEqual(util.inspect(setter, true), '{ [b]: [Setter] }');
505+
assert.strictEqual(
506+
util.inspect(getter, true),
507+
'[Object: null prototype] { [a]: [Getter] }'
508+
);
509+
assert.strictEqual(
510+
util.inspect(setter, true),
511+
'[Object: null prototype] { [b]: [Setter] }'
512+
);
507513
assert.strictEqual(
508514
util.inspect(getterAndSetter, true),
509-
'{ [c]: [Getter/Setter] }'
515+
'[Object: null prototype] { [c]: [Getter/Setter] }'
510516
);
511517
}
512518

@@ -1134,7 +1140,7 @@ if (typeof Symbol !== 'undefined') {
11341140

11351141
{
11361142
const x = Object.create(null);
1137-
assert.strictEqual(util.inspect(x), '{}');
1143+
assert.strictEqual(util.inspect(x), '[Object: null prototype] {}');
11381144
}
11391145

11401146
{
@@ -1274,7 +1280,7 @@ util.inspect(process);
12741280

12751281
assert.strictEqual(util.inspect(
12761282
Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })),
1277-
'[foo] {}');
1283+
'[Object: null prototype] [foo] {}');
12781284

12791285
assert.strictEqual(util.inspect(new Foo()), "Foo [bar] { foo: 'bar' }");
12801286

@@ -1618,20 +1624,12 @@ util.inspect(process);
16181624
'prematurely. Maximum call stack size exceeded.]'));
16191625
}
16201626

1621-
// Verify the output in case the value has no prototype.
1622-
// Sadly, these cases can not be fully inspected :(
1623-
[
1624-
[/a/, '/undefined/undefined'],
1625-
[new DataView(new ArrayBuffer(2)),
1626-
'DataView {\n byteLength: undefined,\n byteOffset: undefined,\n ' +
1627-
'buffer: undefined }'],
1628-
[new SharedArrayBuffer(2), 'SharedArrayBuffer { byteLength: undefined }']
1629-
].forEach(([value, expected]) => {
1627+
{
16301628
assert.strictEqual(
1631-
util.inspect(Object.setPrototypeOf(value, null)),
1632-
expected
1629+
util.inspect(Object.setPrototypeOf(/a/, null)),
1630+
'/undefined/undefined'
16331631
);
1634-
});
1632+
}
16351633

16361634
// Verify that throwing in valueOf and having no prototype still produces nice
16371635
// results.
@@ -1667,6 +1665,39 @@ util.inspect(process);
16671665
}
16681666
});
16691667
assert.strictEqual(util.inspect(value), expected);
1668+
value.foo = 'bar';
1669+
assert.notStrictEqual(util.inspect(value), expected);
1670+
delete value.foo;
1671+
value[Symbol('foo')] = 'yeah';
1672+
assert.notStrictEqual(util.inspect(value), expected);
1673+
});
1674+
1675+
[
1676+
[[1, 3, 4], '[Array: null prototype] [ 1, 3, 4 ]'],
1677+
[new Set([1, 2]), '[Set: null prototype] { 1, 2 }'],
1678+
[new Map([[1, 2]]), '[Map: null prototype] { 1 => 2 }'],
1679+
[new Promise((resolve) => setTimeout(resolve, 10)),
1680+
'[Promise: null prototype] { <pending> }'],
1681+
[new WeakSet(), '[WeakSet: null prototype] { [items unknown] }'],
1682+
[new WeakMap(), '[WeakMap: null prototype] { [items unknown] }'],
1683+
[new Uint8Array(2), '[Uint8Array: null prototype] [ 0, 0 ]'],
1684+
[new Uint16Array(2), '[Uint16Array: null prototype] [ 0, 0 ]'],
1685+
[new Uint32Array(2), '[Uint32Array: null prototype] [ 0, 0 ]'],
1686+
[new Int8Array(2), '[Int8Array: null prototype] [ 0, 0 ]'],
1687+
[new Int16Array(2), '[Int16Array: null prototype] [ 0, 0 ]'],
1688+
[new Int32Array(2), '[Int32Array: null prototype] [ 0, 0 ]'],
1689+
[new Float32Array(2), '[Float32Array: null prototype] [ 0, 0 ]'],
1690+
[new Float64Array(2), '[Float64Array: null prototype] [ 0, 0 ]'],
1691+
[new BigInt64Array(2), '[BigInt64Array: null prototype] [ 0, 0 ]'],
1692+
[new BigUint64Array(2), '[BigUint64Array: null prototype] [ 0, 0 ]'],
1693+
[new ArrayBuffer(16), '[ArrayBuffer: null prototype] ' +
1694+
'{ byteLength: undefined }'],
1695+
[new DataView(new ArrayBuffer(16)),
1696+
'[DataView: null prototype] {\n byteLength: undefined,\n ' +
1697+
'byteOffset: undefined,\n buffer: undefined }'],
1698+
[new SharedArrayBuffer(2), '[SharedArrayBuffer: null prototype] ' +
1699+
'{ byteLength: undefined }']
1700+
].forEach(([value, expected]) => {
16701701
assert.strictEqual(
16711702
util.inspect(Object.setPrototypeOf(value, null)),
16721703
expected
@@ -1748,3 +1779,30 @@ assert.strictEqual(
17481779
'[ 3, 2, 1, [Symbol(a)]: false, [Symbol(b)]: true, a: 1, b: 2, c: 3 ]'
17491780
);
17501781
}
1782+
1783+
// Manipulate the prototype to one that we can not handle.
1784+
{
1785+
let obj = { a: true };
1786+
let value = (function() { return function() {}; })();
1787+
Object.setPrototypeOf(value, null);
1788+
Object.setPrototypeOf(obj, value);
1789+
assert.strictEqual(util.inspect(obj), '{ a: true }');
1790+
1791+
obj = { a: true };
1792+
value = [];
1793+
Object.setPrototypeOf(value, null);
1794+
Object.setPrototypeOf(obj, value);
1795+
assert.strictEqual(util.inspect(obj), '{ a: true }');
1796+
}
1797+
1798+
// Check that the fallback always works.
1799+
{
1800+
const obj = new Set([1, 2]);
1801+
const iterator = obj[Symbol.iterator];
1802+
Object.setPrototypeOf(obj, null);
1803+
Object.defineProperty(obj, Symbol.iterator, {
1804+
value: iterator,
1805+
configurable: true
1806+
});
1807+
assert.strictEqual(util.inspect(obj), '[Set: null prototype] { 1, 2 }');
1808+
}

0 commit comments

Comments
 (0)