Skip to content

Commit b0e3aec

Browse files
BridgeARMylesBorins
authored andcommitted
util: inspect (user defined) prototype properties
This is only active if the `showHidden` option is truthy. The implementation is a trade-off between accuracy and performance. This will miss properties such as properties added to built-in data types. The goal is mainly to visualize prototype getters and setters such as: class Foo { ownProperty = true get bar() { return 'Hello world!' } } const a = new Foo() The `bar` property is a non-enumerable property on the prototype while `ownProperty` will be set directly on the created instance. The output is similar to the one of Chromium when inspecting objects closer. The output from Firefox is difficult to compare, since it's always a structured interactive output and was therefore not taken into account. Backport-PR-URL: #31431 PR-URL: #30768 Fixes: #30183 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 40a724c commit b0e3aec

File tree

4 files changed

+198
-22
lines changed

4 files changed

+198
-22
lines changed

doc/api/util.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@ stream.write('With ES6');
398398
<!-- YAML
399399
added: v0.3.0
400400
changes:
401+
- version: REPLACEME
402+
pr-url: https://github.com/nodejs/node/pull/30768
403+
description: User defined prototype properties are inspected in case
404+
`showHidden` is `true`.
401405
- version: v12.0.0
402406
pr-url: https://github.com/nodejs/node/pull/27109
403407
description: The `compact` options default is changed to `3` and the
@@ -458,7 +462,8 @@ changes:
458462
* `options` {Object}
459463
* `showHidden` {boolean} If `true`, `object`'s non-enumerable symbols and
460464
properties are included in the formatted result. [`WeakMap`][] and
461-
[`WeakSet`][] entries are also included. **Default:** `false`.
465+
[`WeakSet`][] entries are also included as well as user defined prototype
466+
properties (excluding method properties). **Default:** `false`.
462467
* `depth` {number} Specifies the number of times to recurse while formatting
463468
`object`. This is useful for inspecting large objects. To recurse up to
464469
the maximum call stack size pass `Infinity` or `null`.

lib/internal/util/inspect.js

+100-16
Original file line numberDiff line numberDiff line change
@@ -450,14 +450,20 @@ function getEmptyFormatArray() {
450450
return [];
451451
}
452452

453-
function getConstructorName(obj, ctx, recurseTimes) {
453+
function getConstructorName(obj, ctx, recurseTimes, protoProps) {
454454
let firstProto;
455455
const tmp = obj;
456456
while (obj) {
457457
const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
458458
if (descriptor !== undefined &&
459459
typeof descriptor.value === 'function' &&
460460
descriptor.value.name !== '') {
461+
if (protoProps !== undefined &&
462+
!builtInObjects.has(descriptor.value.name)) {
463+
const isProto = firstProto !== undefined;
464+
addPrototypeProperties(
465+
ctx, tmp, obj, recurseTimes, isProto, protoProps);
466+
}
461467
return descriptor.value.name;
462468
}
463469

@@ -477,7 +483,8 @@ function getConstructorName(obj, ctx, recurseTimes) {
477483
return `${res} <Complex prototype>`;
478484
}
479485

480-
const protoConstr = getConstructorName(firstProto, ctx, recurseTimes + 1);
486+
const protoConstr = getConstructorName(
487+
firstProto, ctx, recurseTimes + 1, protoProps);
481488

482489
if (protoConstr === null) {
483490
return `${res} <${inspect(firstProto, {
@@ -490,6 +497,68 @@ function getConstructorName(obj, ctx, recurseTimes) {
490497
return `${res} <${protoConstr}>`;
491498
}
492499

500+
// This function has the side effect of adding prototype properties to the
501+
// `output` argument (which is an array). This is intended to highlight user
502+
// defined prototype properties.
503+
function addPrototypeProperties(ctx, main, obj, recurseTimes, isProto, output) {
504+
let depth = 0;
505+
let keys;
506+
let keySet;
507+
do {
508+
if (!isProto) {
509+
obj = ObjectGetPrototypeOf(obj);
510+
// Stop as soon as a null prototype is encountered.
511+
if (obj === null) {
512+
return;
513+
}
514+
// Stop as soon as a built-in object type is detected.
515+
const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
516+
if (descriptor !== undefined &&
517+
typeof descriptor.value === 'function' &&
518+
builtInObjects.has(descriptor.value.name)) {
519+
return;
520+
}
521+
} else {
522+
isProto = false;
523+
}
524+
525+
if (depth === 0) {
526+
keySet = new Set();
527+
} else {
528+
keys.forEach((key) => keySet.add(key));
529+
}
530+
// Get all own property names and symbols.
531+
keys = ObjectGetOwnPropertyNames(obj);
532+
const symbols = ObjectGetOwnPropertySymbols(obj);
533+
if (symbols.length !== 0) {
534+
keys.push(...symbols);
535+
}
536+
for (const key of keys) {
537+
// Ignore the `constructor` property and keys that exist on layers above.
538+
if (key === 'constructor' ||
539+
ObjectPrototypeHasOwnProperty(main, key) ||
540+
(depth !== 0 && keySet.has(key))) {
541+
continue;
542+
}
543+
const desc = ObjectGetOwnPropertyDescriptor(obj, key);
544+
if (typeof desc.value === 'function') {
545+
continue;
546+
}
547+
const value = formatProperty(
548+
ctx, obj, recurseTimes, key, kObjectType, desc);
549+
if (ctx.colors) {
550+
// Faint!
551+
output.push(`\u001b[2m${value}\u001b[22m`);
552+
} else {
553+
output.push(value);
554+
}
555+
}
556+
// Limit the inspection to up to three prototype layers. Using `recurseTimes`
557+
// is not a good choice here, because it's as if the properties are declared
558+
// on the current object from the users perspective.
559+
} while (++depth !== 3);
560+
}
561+
493562
function getPrefix(constructor, tag, fallback) {
494563
if (constructor === null) {
495564
if (tag !== '') {
@@ -693,8 +762,17 @@ function formatValue(ctx, value, recurseTimes, typedArray) {
693762

694763
function formatRaw(ctx, value, recurseTimes, typedArray) {
695764
let keys;
765+
let protoProps;
766+
if (ctx.showHidden && (recurseTimes <= ctx.depth || ctx.depth === null)) {
767+
protoProps = [];
768+
}
769+
770+
const constructor = getConstructorName(value, ctx, recurseTimes, protoProps);
771+
// Reset the variable to check for this later on.
772+
if (protoProps !== undefined && protoProps.length === 0) {
773+
protoProps = undefined;
774+
}
696775

697-
const constructor = getConstructorName(value, ctx, recurseTimes);
698776
let tag = value[SymbolToStringTag];
699777
// Only list the tag in case it's non-enumerable / not an own property.
700778
// Otherwise we'd print this twice.
@@ -724,21 +802,21 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
724802
// Only set the constructor for non ordinary ("Array [...]") arrays.
725803
const prefix = getPrefix(constructor, tag, 'Array');
726804
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
727-
if (value.length === 0 && keys.length === 0)
805+
if (value.length === 0 && keys.length === 0 && protoProps === undefined)
728806
return `${braces[0]}]`;
729807
extrasType = kArrayExtrasType;
730808
formatter = formatArray;
731809
} else if (isSet(value)) {
732810
keys = getKeys(value, ctx.showHidden);
733811
const prefix = getPrefix(constructor, tag, 'Set');
734-
if (value.size === 0 && keys.length === 0)
812+
if (value.size === 0 && keys.length === 0 && protoProps === undefined)
735813
return `${prefix}{}`;
736814
braces = [`${prefix}{`, '}'];
737815
formatter = formatSet;
738816
} else if (isMap(value)) {
739817
keys = getKeys(value, ctx.showHidden);
740818
const prefix = getPrefix(constructor, tag, 'Map');
741-
if (value.size === 0 && keys.length === 0)
819+
if (value.size === 0 && keys.length === 0 && protoProps === undefined)
742820
return `${prefix}{}`;
743821
braces = [`${prefix}{`, '}'];
744822
formatter = formatMap;
@@ -773,12 +851,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
773851
} else if (tag !== '') {
774852
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
775853
}
776-
if (keys.length === 0) {
854+
if (keys.length === 0 && protoProps === undefined) {
777855
return `${braces[0]}}`;
778856
}
779857
} else if (typeof value === 'function') {
780858
base = getFunctionBase(value, constructor, tag);
781-
if (keys.length === 0)
859+
if (keys.length === 0 && protoProps === undefined)
782860
return ctx.stylize(base, 'special');
783861
} else if (isRegExp(value)) {
784862
// Make RegExps say that they are RegExps
@@ -788,8 +866,10 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
788866
const prefix = getPrefix(constructor, tag, 'RegExp');
789867
if (prefix !== 'RegExp ')
790868
base = `${prefix}${base}`;
791-
if (keys.length === 0 || (recurseTimes > ctx.depth && ctx.depth !== null))
869+
if ((keys.length === 0 && protoProps === undefined) ||
870+
(recurseTimes > ctx.depth && ctx.depth !== null)) {
792871
return ctx.stylize(base, 'regexp');
872+
}
793873
} else if (isDate(value)) {
794874
// Make dates with properties first say the date
795875
base = NumberIsNaN(DatePrototypeGetTime(value)) ?
@@ -798,12 +878,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
798878
const prefix = getPrefix(constructor, tag, 'Date');
799879
if (prefix !== 'Date ')
800880
base = `${prefix}${base}`;
801-
if (keys.length === 0) {
881+
if (keys.length === 0 && protoProps === undefined) {
802882
return ctx.stylize(base, 'date');
803883
}
804884
} else if (isError(value)) {
805885
base = formatError(value, constructor, tag, ctx);
806-
if (keys.length === 0)
886+
if (keys.length === 0 && protoProps === undefined)
807887
return base;
808888
} else if (isAnyArrayBuffer(value)) {
809889
// Fast path for ArrayBuffer and SharedArrayBuffer.
@@ -814,7 +894,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
814894
const prefix = getPrefix(constructor, tag, arrayType);
815895
if (typedArray === undefined) {
816896
formatter = formatArrayBuffer;
817-
} else if (keys.length === 0) {
897+
} else if (keys.length === 0 && protoProps === undefined) {
818898
return prefix +
819899
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
820900
}
@@ -838,7 +918,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
838918
formatter = formatNamespaceObject;
839919
} else if (isBoxedPrimitive(value)) {
840920
base = getBoxedBase(value, ctx, keys, constructor, tag);
841-
if (keys.length === 0) {
921+
if (keys.length === 0 && protoProps === undefined) {
842922
return base;
843923
}
844924
} else {
@@ -858,7 +938,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
858938
formatter = formatIterator;
859939
// Handle other regular objects again.
860940
} else {
861-
if (keys.length === 0) {
941+
if (keys.length === 0 && protoProps === undefined) {
862942
if (isExternal(value))
863943
return ctx.stylize('[External]', 'special');
864944
return `${getCtxStyle(value, constructor, tag)}{}`;
@@ -886,6 +966,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
886966
output.push(
887967
formatProperty(ctx, value, recurseTimes, keys[i], extrasType));
888968
}
969+
if (protoProps !== undefined) {
970+
output.push(...protoProps);
971+
}
889972
} catch (err) {
890973
const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1);
891974
return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl);
@@ -1349,6 +1432,7 @@ function formatTypedArray(ctx, value, recurseTimes) {
13491432
}
13501433
if (ctx.showHidden) {
13511434
// .buffer goes last, it's not a primitive like the others.
1435+
// All besides `BYTES_PER_ELEMENT` are actually getters.
13521436
ctx.indentationLvl += 2;
13531437
for (const key of [
13541438
'BYTES_PER_ELEMENT',
@@ -1497,10 +1581,10 @@ function formatPromise(ctx, value, recurseTimes) {
14971581
return output;
14981582
}
14991583

1500-
function formatProperty(ctx, value, recurseTimes, key, type) {
1584+
function formatProperty(ctx, value, recurseTimes, key, type, desc) {
15011585
let name, str;
15021586
let extra = ' ';
1503-
const desc = ObjectGetOwnPropertyDescriptor(value, key) ||
1587+
desc = desc || ObjectGetOwnPropertyDescriptor(value, key) ||
15041588
{ value: value[key], enumerable: true };
15051589
if (desc.value !== undefined) {
15061590
const diff = (type !== kObjectType || ctx.compact !== true) ? 2 : 3;

test/parallel/test-util-inspect.js

+79-1
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,24 @@ assert.strictEqual(
391391
{
392392
class CustomArray extends Array {}
393393
CustomArray.prototype[5] = 'foo';
394+
CustomArray.prototype[49] = 'bar';
395+
CustomArray.prototype.foo = true;
394396
const arr = new CustomArray(50);
395-
assert.strictEqual(util.inspect(arr), 'CustomArray [ <50 empty items> ]');
397+
arr[49] = 'I win';
398+
assert.strictEqual(
399+
util.inspect(arr),
400+
"CustomArray [ <49 empty items>, 'I win' ]"
401+
);
402+
assert.strictEqual(
403+
util.inspect(arr, { showHidden: true }),
404+
'CustomArray [\n' +
405+
' <49 empty items>,\n' +
406+
" 'I win',\n" +
407+
' [length]: 50,\n' +
408+
" '5': 'foo',\n" +
409+
' foo: true\n' +
410+
']'
411+
);
396412
}
397413

398414
// Array with extra properties.
@@ -2585,3 +2601,65 @@ assert.strictEqual(
25852601
throw err;
25862602
}
25872603
}
2604+
2605+
// Inspect prototype properties.
2606+
{
2607+
class Foo extends Map {
2608+
prop = false;
2609+
prop2 = true;
2610+
get abc() {
2611+
return true;
2612+
}
2613+
get def() {
2614+
return false;
2615+
}
2616+
set def(v) {}
2617+
get xyz() {
2618+
return 'Should be ignored';
2619+
}
2620+
func(a) {}
2621+
[util.inspect.custom]() {
2622+
return this;
2623+
}
2624+
}
2625+
2626+
class Bar extends Foo {
2627+
abc = true;
2628+
prop = true;
2629+
get xyz() {
2630+
return 'YES!';
2631+
}
2632+
[util.inspect.custom]() {
2633+
return this;
2634+
}
2635+
}
2636+
2637+
const bar = new Bar();
2638+
2639+
assert.strictEqual(
2640+
inspect(bar),
2641+
'Bar [Map] { prop: true, prop2: true, abc: true }'
2642+
);
2643+
assert.strictEqual(
2644+
inspect(bar, { showHidden: true, getters: true, colors: false }),
2645+
'Bar [Map] {\n' +
2646+
' [size]: 0,\n' +
2647+
' prop: true,\n' +
2648+
' prop2: true,\n' +
2649+
' abc: true,\n' +
2650+
" [xyz]: [Getter: 'YES!'],\n" +
2651+
' [def]: [Getter/Setter: false]\n' +
2652+
'}'
2653+
);
2654+
assert.strictEqual(
2655+
inspect(bar, { showHidden: true, getters: false, colors: true }),
2656+
'Bar [Map] {\n' +
2657+
' [size]: \x1B[33m0\x1B[39m,\n' +
2658+
' prop: \x1B[33mtrue\x1B[39m,\n' +
2659+
' prop2: \x1B[33mtrue\x1B[39m,\n' +
2660+
' abc: \x1B[33mtrue\x1B[39m,\n' +
2661+
' \x1B[2m[xyz]: \x1B[36m[Getter]\x1B[39m\x1B[22m,\n' +
2662+
' \x1B[2m[def]: \x1B[36m[Getter/Setter]\x1B[39m\x1B[22m\n' +
2663+
'}'
2664+
);
2665+
}

test/parallel/test-whatwg-encoding-custom-textdecoder.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,19 @@ if (common.hasIntl) {
113113
} else {
114114
assert.strictEqual(
115115
util.inspect(dec, { showHidden: true }),
116-
"TextDecoder {\n encoding: 'utf-8',\n fatal: false,\n " +
117-
'ignoreBOM: true,\n [Symbol(flags)]: 4,\n [Symbol(handle)]: ' +
118-
"StringDecoder {\n encoding: 'utf8',\n " +
119-
'[Symbol(kNativeDecoder)]: <Buffer 00 00 00 00 00 00 01>\n }\n}'
116+
'TextDecoder {\n' +
117+
" encoding: 'utf-8',\n" +
118+
' fatal: false,\n' +
119+
' ignoreBOM: true,\n' +
120+
' [Symbol(flags)]: 4,\n' +
121+
' [Symbol(handle)]: StringDecoder {\n' +
122+
" encoding: 'utf8',\n" +
123+
' [Symbol(kNativeDecoder)]: <Buffer 00 00 00 00 00 00 01>,\n' +
124+
' lastChar: [Getter],\n' +
125+
' lastNeed: [Getter],\n' +
126+
' lastTotal: [Getter]\n' +
127+
' }\n' +
128+
'}'
120129
);
121130
}
122131
}

0 commit comments

Comments
 (0)