Skip to content

Commit f4e4ef5

Browse files
antsmartianBridgeAR
authored andcommitted
util: handle null prototype on inspect
This makes sure the `null` prototype is always detected properly. PR-URL: #22331 Fixes: #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>
1 parent 97f1e94 commit f4e4ef5

File tree

2 files changed

+142
-43
lines changed

2 files changed

+142
-43
lines changed

lib/internal/util/inspect.js

+64-23
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ function getEmptyFormatArray() {
326326
}
327327

328328
function getConstructorName(obj) {
329+
let firstProto;
329330
while (obj) {
330331
const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
331332
if (descriptor !== undefined &&
@@ -335,25 +336,35 @@ function getConstructorName(obj) {
335336
}
336337

337338
obj = Object.getPrototypeOf(obj);
339+
if (firstProto === undefined) {
340+
firstProto = obj;
341+
}
342+
}
343+
344+
if (firstProto === null) {
345+
return null;
338346
}
347+
// TODO(BridgeAR): Improve prototype inspection.
348+
// We could use inspect on the prototype itself to improve the output.
339349

340350
return '';
341351
}
342352

343353
function getPrefix(constructor, tag, fallback) {
354+
if (constructor === null) {
355+
if (tag !== '') {
356+
return `[${fallback}: null prototype] [${tag}] `;
357+
}
358+
return `[${fallback}: null prototype] `;
359+
}
360+
344361
if (constructor !== '') {
345362
if (tag !== '' && constructor !== tag) {
346363
return `${constructor} [${tag}] `;
347364
}
348365
return `${constructor} `;
349366
}
350367

351-
if (tag !== '')
352-
return `[${tag}] `;
353-
354-
if (fallback !== undefined)
355-
return `${fallback} `;
356-
357368
return '';
358369
}
359370

@@ -427,21 +438,49 @@ function findTypedConstructor(value) {
427438
}
428439
}
429440

441+
let lazyNullPrototypeCache;
442+
// Creates a subclass and name
443+
// the constructor as `${clazz} : null prototype`
444+
function clazzWithNullPrototype(clazz, name) {
445+
if (lazyNullPrototypeCache === undefined) {
446+
lazyNullPrototypeCache = new Map();
447+
} else {
448+
const cachedClass = lazyNullPrototypeCache.get(clazz);
449+
if (cachedClass !== undefined) {
450+
return cachedClass;
451+
}
452+
}
453+
class NullPrototype extends clazz {
454+
get [Symbol.toStringTag]() {
455+
return '';
456+
}
457+
}
458+
Object.defineProperty(NullPrototype.prototype.constructor, 'name',
459+
{ value: `[${name}: null prototype]` });
460+
lazyNullPrototypeCache.set(clazz, NullPrototype);
461+
return NullPrototype;
462+
}
463+
430464
function noPrototypeIterator(ctx, value, recurseTimes) {
431465
let newVal;
432-
// TODO: Create a Subclass in case there's no prototype and show
433-
// `null-prototype`.
434466
if (isSet(value)) {
435-
const clazz = Object.getPrototypeOf(value) || Set;
467+
const clazz = Object.getPrototypeOf(value) ||
468+
clazzWithNullPrototype(Set, 'Set');
436469
newVal = new clazz(setValues(value));
437470
} else if (isMap(value)) {
438-
const clazz = Object.getPrototypeOf(value) || Map;
471+
const clazz = Object.getPrototypeOf(value) ||
472+
clazzWithNullPrototype(Map, 'Map');
439473
newVal = new clazz(mapEntries(value));
440474
} else if (Array.isArray(value)) {
441-
const clazz = Object.getPrototypeOf(value) || Array;
475+
const clazz = Object.getPrototypeOf(value) ||
476+
clazzWithNullPrototype(Array, 'Array');
442477
newVal = new clazz(value.length || 0);
443478
} else if (isTypedArray(value)) {
444-
const clazz = findTypedConstructor(value) || Uint8Array;
479+
let clazz = Object.getPrototypeOf(value);
480+
if (!clazz) {
481+
const constructor = findTypedConstructor(value);
482+
clazz = clazzWithNullPrototype(constructor, constructor.name);
483+
}
445484
newVal = new clazz(value);
446485
}
447486
if (newVal) {
@@ -527,29 +566,32 @@ function formatRaw(ctx, value, recurseTimes) {
527566
if (Array.isArray(value)) {
528567
keys = getOwnNonIndexProperties(value, filter);
529568
// Only set the constructor for non ordinary ("Array [...]") arrays.
530-
const prefix = getPrefix(constructor, tag);
569+
const prefix = getPrefix(constructor, tag, 'Array');
531570
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
532571
if (value.length === 0 && keys.length === 0)
533572
return `${braces[0]}]`;
534573
extrasType = kArrayExtrasType;
535574
formatter = formatArray;
536575
} else if (isSet(value)) {
537576
keys = getKeys(value, ctx.showHidden);
538-
const prefix = getPrefix(constructor, tag);
577+
const prefix = getPrefix(constructor, tag, 'Set');
539578
if (value.size === 0 && keys.length === 0)
540579
return `${prefix}{}`;
541580
braces = [`${prefix}{`, '}'];
542581
formatter = formatSet;
543582
} else if (isMap(value)) {
544583
keys = getKeys(value, ctx.showHidden);
545-
const prefix = getPrefix(constructor, tag);
584+
const prefix = getPrefix(constructor, tag, 'Map');
546585
if (value.size === 0 && keys.length === 0)
547586
return `${prefix}{}`;
548587
braces = [`${prefix}{`, '}'];
549588
formatter = formatMap;
550589
} else if (isTypedArray(value)) {
551590
keys = getOwnNonIndexProperties(value, filter);
552-
braces = [`${getPrefix(constructor, tag)}[`, ']'];
591+
const prefix = constructor !== null ?
592+
getPrefix(constructor, tag) :
593+
getPrefix(constructor, tag, findTypedConstructor(value).name);
594+
braces = [`${prefix}[`, ']'];
553595
if (value.length === 0 && keys.length === 0 && !ctx.showHidden)
554596
return `${braces[0]}]`;
555597
formatter = formatTypedArray;
@@ -575,7 +617,7 @@ function formatRaw(ctx, value, recurseTimes) {
575617
return '[Arguments] {}';
576618
braces[0] = '[Arguments] {';
577619
} else if (tag !== '') {
578-
braces[0] = `${getPrefix(constructor, tag)}{`;
620+
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
579621
if (keys.length === 0) {
580622
return `${braces[0]}}`;
581623
}
@@ -622,13 +664,12 @@ function formatRaw(ctx, value, recurseTimes) {
622664
base = `[${base.slice(0, stackStart)}]`;
623665
}
624666
} else if (isAnyArrayBuffer(value)) {
625-
let prefix = getPrefix(constructor, tag);
626-
if (prefix === '') {
627-
prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer ';
628-
}
629667
// Fast path for ArrayBuffer and SharedArrayBuffer.
630668
// Can't do the same for DataView because it has a non-primitive
631669
// .buffer property that we need to recurse for.
670+
const arrayType = isArrayBuffer(value) ? 'ArrayBuffer' :
671+
'SharedArrayBuffer';
672+
const prefix = getPrefix(constructor, tag, arrayType);
632673
if (keys.length === 0)
633674
return prefix +
634675
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
@@ -693,9 +734,9 @@ function formatRaw(ctx, value, recurseTimes) {
693734
} else if (keys.length === 0) {
694735
if (isExternal(value))
695736
return ctx.stylize('[External]', 'special');
696-
return `${getPrefix(constructor, tag)}{}`;
737+
return `${getPrefix(constructor, tag, 'Object')}{}`;
697738
} else {
698-
braces[0] = `${getPrefix(constructor, tag)}{`;
739+
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
699740
}
700741
}
701742
}

test/parallel/test-util-inspect.js

+78-20
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
const common = require('../common');
2626
const assert = require('assert');
2727
const { internalBinding } = require('internal/test/binding');
28-
const { JSStream } = internalBinding('js_stream');
28+
const JSStream = process.binding('js_stream').JSStream;
2929
const util = require('util');
3030
const vm = require('vm');
3131
const { previewEntries } = internalBinding('util');
@@ -261,15 +261,15 @@ assert.strictEqual(
261261
name: { value: 'Tim', enumerable: true },
262262
hidden: { value: 'secret' }
263263
}), { showHidden: true }),
264-
"{ name: 'Tim', [hidden]: 'secret' }"
264+
"[Object: null prototype] { name: 'Tim', [hidden]: 'secret' }"
265265
);
266266

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

275275
// Dynamic properties.
@@ -505,11 +505,17 @@ assert.strictEqual(util.inspect(-5e-324), '-5e-324');
505505
set: function() {}
506506
}
507507
});
508-
assert.strictEqual(util.inspect(getter, true), '{ [a]: [Getter] }');
509-
assert.strictEqual(util.inspect(setter, true), '{ [b]: [Setter] }');
508+
assert.strictEqual(
509+
util.inspect(getter, true),
510+
'[Object: null prototype] { [a]: [Getter] }'
511+
);
512+
assert.strictEqual(
513+
util.inspect(setter, true),
514+
'[Object: null prototype] { [b]: [Setter] }'
515+
);
510516
assert.strictEqual(
511517
util.inspect(getterAndSetter, true),
512-
'{ [c]: [Getter/Setter] }'
518+
'[Object: null prototype] { [c]: [Getter/Setter] }'
513519
);
514520
}
515521

@@ -1084,7 +1090,7 @@ if (typeof Symbol !== 'undefined') {
10841090

10851091
{
10861092
const x = Object.create(null);
1087-
assert.strictEqual(util.inspect(x), '{}');
1093+
assert.strictEqual(util.inspect(x), '[Object: null prototype] {}');
10881094
}
10891095

10901096
{
@@ -1224,7 +1230,7 @@ util.inspect(process);
12241230

12251231
assert.strictEqual(util.inspect(
12261232
Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })),
1227-
'[foo] {}');
1233+
'[Object: null prototype] [foo] {}');
12281234

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

@@ -1574,20 +1580,12 @@ assert.strictEqual(util.inspect('"\''), '`"\'`');
15741580
// eslint-disable-next-line no-template-curly-in-string
15751581
assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
15761582

1577-
// Verify the output in case the value has no prototype.
1578-
// Sadly, these cases can not be fully inspected :(
1579-
[
1580-
[/a/, '/undefined/undefined'],
1581-
[new DataView(new ArrayBuffer(2)),
1582-
'DataView {\n byteLength: undefined,\n byteOffset: undefined,\n ' +
1583-
'buffer: undefined }'],
1584-
[new SharedArrayBuffer(2), 'SharedArrayBuffer { byteLength: undefined }']
1585-
].forEach(([value, expected]) => {
1583+
{
15861584
assert.strictEqual(
1587-
util.inspect(Object.setPrototypeOf(value, null)),
1588-
expected
1585+
util.inspect(Object.setPrototypeOf(/a/, null)),
1586+
'/undefined/undefined'
15891587
);
1590-
});
1588+
}
15911589

15921590
// Verify that throwing in valueOf and having no prototype still produces nice
15931591
// results.
@@ -1623,6 +1621,39 @@ assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
16231621
}
16241622
});
16251623
assert.strictEqual(util.inspect(value), expected);
1624+
value.foo = 'bar';
1625+
assert.notStrictEqual(util.inspect(value), expected);
1626+
delete value.foo;
1627+
value[Symbol('foo')] = 'yeah';
1628+
assert.notStrictEqual(util.inspect(value), expected);
1629+
});
1630+
1631+
[
1632+
[[1, 3, 4], '[Array: null prototype] [ 1, 3, 4 ]'],
1633+
[new Set([1, 2]), '[Set: null prototype] { 1, 2 }'],
1634+
[new Map([[1, 2]]), '[Map: null prototype] { 1 => 2 }'],
1635+
[new Promise((resolve) => setTimeout(resolve, 10)),
1636+
'[Promise: null prototype] { <pending> }'],
1637+
[new WeakSet(), '[WeakSet: null prototype] { <items unknown> }'],
1638+
[new WeakMap(), '[WeakMap: null prototype] { <items unknown> }'],
1639+
[new Uint8Array(2), '[Uint8Array: null prototype] [ 0, 0 ]'],
1640+
[new Uint16Array(2), '[Uint16Array: null prototype] [ 0, 0 ]'],
1641+
[new Uint32Array(2), '[Uint32Array: null prototype] [ 0, 0 ]'],
1642+
[new Int8Array(2), '[Int8Array: null prototype] [ 0, 0 ]'],
1643+
[new Int16Array(2), '[Int16Array: null prototype] [ 0, 0 ]'],
1644+
[new Int32Array(2), '[Int32Array: null prototype] [ 0, 0 ]'],
1645+
[new Float32Array(2), '[Float32Array: null prototype] [ 0, 0 ]'],
1646+
[new Float64Array(2), '[Float64Array: null prototype] [ 0, 0 ]'],
1647+
[new BigInt64Array(2), '[BigInt64Array: null prototype] [ 0n, 0n ]'],
1648+
[new BigUint64Array(2), '[BigUint64Array: null prototype] [ 0n, 0n ]'],
1649+
[new ArrayBuffer(16), '[ArrayBuffer: null prototype] ' +
1650+
'{ byteLength: undefined }'],
1651+
[new DataView(new ArrayBuffer(16)),
1652+
'[DataView: null prototype] {\n byteLength: undefined,\n ' +
1653+
'byteOffset: undefined,\n buffer: undefined }'],
1654+
[new SharedArrayBuffer(2), '[SharedArrayBuffer: null prototype] ' +
1655+
'{ byteLength: undefined }']
1656+
].forEach(([value, expected]) => {
16261657
assert.strictEqual(
16271658
util.inspect(Object.setPrototypeOf(value, null)),
16281659
expected
@@ -1706,3 +1737,30 @@ assert.strictEqual(
17061737
'[ 3, 2, 1, [Symbol(a)]: false, [Symbol(b)]: true, a: 1, b: 2, c: 3 ]'
17071738
);
17081739
}
1740+
1741+
// Manipulate the prototype to one that we can not handle.
1742+
{
1743+
let obj = { a: true };
1744+
let value = (function() { return function() {}; })();
1745+
Object.setPrototypeOf(value, null);
1746+
Object.setPrototypeOf(obj, value);
1747+
assert.strictEqual(util.inspect(obj), '{ a: true }');
1748+
1749+
obj = { a: true };
1750+
value = [];
1751+
Object.setPrototypeOf(value, null);
1752+
Object.setPrototypeOf(obj, value);
1753+
assert.strictEqual(util.inspect(obj), '{ a: true }');
1754+
}
1755+
1756+
// Check that the fallback always works.
1757+
{
1758+
const obj = new Set([1, 2]);
1759+
const iterator = obj[Symbol.iterator];
1760+
Object.setPrototypeOf(obj, null);
1761+
Object.defineProperty(obj, Symbol.iterator, {
1762+
value: iterator,
1763+
configurable: true
1764+
});
1765+
assert.strictEqual(util.inspect(obj), '[Set: null prototype] { 1, 2 }');
1766+
}

0 commit comments

Comments
 (0)