Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3df083c

Browse filesBrowse files
antsmartianMylesBorins
authored andcommitted
util: handle null prototype on inspect
This makes sure the prototype is always detected properly. Backport-PR-URL: #23655 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 1e9b4a2 commit 3df083c
Copy full SHA for 3df083c

File tree

Expand file treeCollapse file tree

2 files changed

+141
-42
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

2 files changed

+141
-42
lines changed
Open diff view settings
Collapse file

‎lib/internal/util/inspect.js‎

Copy file name to clipboardExpand all lines: lib/internal/util/inspect.js
+64-23Lines changed: 64 additions & 23 deletions
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
}
Collapse file

‎test/parallel/test-util-inspect.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-util-inspect.js
+77-19Lines changed: 77 additions & 19 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.