Skip to content

Commit 8dec909

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. PR-URL: #30768 Fixes: #30183 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent 453be95 commit 8dec909

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: v13.0.0
402406
pr-url: https://github.com/nodejs/node/pull/27685
403407
description: Circular references now include a marker to the reference.
@@ -461,7 +465,8 @@ changes:
461465
* `options` {Object}
462466
* `showHidden` {boolean} If `true`, `object`'s non-enumerable symbols and
463467
properties are included in the formatted result. [`WeakMap`][] and
464-
[`WeakSet`][] entries are also included. **Default:** `false`.
468+
[`WeakSet`][] entries are also included as well as user defined prototype
469+
properties (excluding method properties). **Default:** `false`.
465470
* `depth` {number} Specifies the number of times to recurse while formatting
466471
`object`. This is useful for inspecting large objects. To recurse up to
467472
the maximum call stack size pass `Infinity` or `null`.

lib/internal/util/inspect.js

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

451-
function getConstructorName(obj, ctx, recurseTimes) {
451+
function getConstructorName(obj, ctx, recurseTimes, protoProps) {
452452
let firstProto;
453453
const tmp = obj;
454454
while (obj) {
455455
const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
456456
if (descriptor !== undefined &&
457457
typeof descriptor.value === 'function' &&
458458
descriptor.value.name !== '') {
459+
if (protoProps !== undefined &&
460+
!builtInObjects.has(descriptor.value.name)) {
461+
const isProto = firstProto !== undefined;
462+
addPrototypeProperties(
463+
ctx, tmp, obj, recurseTimes, isProto, protoProps);
464+
}
459465
return descriptor.value.name;
460466
}
461467

@@ -475,7 +481,8 @@ function getConstructorName(obj, ctx, recurseTimes) {
475481
return `${res} <Complex prototype>`;
476482
}
477483

478-
const protoConstr = getConstructorName(firstProto, ctx, recurseTimes + 1);
484+
const protoConstr = getConstructorName(
485+
firstProto, ctx, recurseTimes + 1, protoProps);
479486

480487
if (protoConstr === null) {
481488
return `${res} <${inspect(firstProto, {
@@ -488,6 +495,68 @@ function getConstructorName(obj, ctx, recurseTimes) {
488495
return `${res} <${protoConstr}>`;
489496
}
490497

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

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

695-
const constructor = getConstructorName(value, ctx, recurseTimes);
696774
let tag = value[SymbolToStringTag];
697775
// Only list the tag in case it's non-enumerable / not an own property.
698776
// Otherwise we'd print this twice.
@@ -722,21 +800,21 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
722800
// Only set the constructor for non ordinary ("Array [...]") arrays.
723801
const prefix = getPrefix(constructor, tag, 'Array');
724802
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
725-
if (value.length === 0 && keys.length === 0)
803+
if (value.length === 0 && keys.length === 0 && protoProps === undefined)
726804
return `${braces[0]}]`;
727805
extrasType = kArrayExtrasType;
728806
formatter = formatArray;
729807
} else if (isSet(value)) {
730808
keys = getKeys(value, ctx.showHidden);
731809
const prefix = getPrefix(constructor, tag, 'Set');
732-
if (value.size === 0 && keys.length === 0)
810+
if (value.size === 0 && keys.length === 0 && protoProps === undefined)
733811
return `${prefix}{}`;
734812
braces = [`${prefix}{`, '}'];
735813
formatter = formatSet;
736814
} else if (isMap(value)) {
737815
keys = getKeys(value, ctx.showHidden);
738816
const prefix = getPrefix(constructor, tag, 'Map');
739-
if (value.size === 0 && keys.length === 0)
817+
if (value.size === 0 && keys.length === 0 && protoProps === undefined)
740818
return `${prefix}{}`;
741819
braces = [`${prefix}{`, '}'];
742820
formatter = formatMap;
@@ -771,12 +849,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
771849
} else if (tag !== '') {
772850
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
773851
}
774-
if (keys.length === 0) {
852+
if (keys.length === 0 && protoProps === undefined) {
775853
return `${braces[0]}}`;
776854
}
777855
} else if (typeof value === 'function') {
778856
base = getFunctionBase(value, constructor, tag);
779-
if (keys.length === 0)
857+
if (keys.length === 0 && protoProps === undefined)
780858
return ctx.stylize(base, 'special');
781859
} else if (isRegExp(value)) {
782860
// Make RegExps say that they are RegExps
@@ -786,8 +864,10 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
786864
const prefix = getPrefix(constructor, tag, 'RegExp');
787865
if (prefix !== 'RegExp ')
788866
base = `${prefix}${base}`;
789-
if (keys.length === 0 || (recurseTimes > ctx.depth && ctx.depth !== null))
867+
if ((keys.length === 0 && protoProps === undefined) ||
868+
(recurseTimes > ctx.depth && ctx.depth !== null)) {
790869
return ctx.stylize(base, 'regexp');
870+
}
791871
} else if (isDate(value)) {
792872
// Make dates with properties first say the date
793873
base = NumberIsNaN(DatePrototypeGetTime(value)) ?
@@ -796,12 +876,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
796876
const prefix = getPrefix(constructor, tag, 'Date');
797877
if (prefix !== 'Date ')
798878
base = `${prefix}${base}`;
799-
if (keys.length === 0) {
879+
if (keys.length === 0 && protoProps === undefined) {
800880
return ctx.stylize(base, 'date');
801881
}
802882
} else if (isError(value)) {
803883
base = formatError(value, constructor, tag, ctx);
804-
if (keys.length === 0)
884+
if (keys.length === 0 && protoProps === undefined)
805885
return base;
806886
} else if (isAnyArrayBuffer(value)) {
807887
// Fast path for ArrayBuffer and SharedArrayBuffer.
@@ -812,7 +892,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
812892
const prefix = getPrefix(constructor, tag, arrayType);
813893
if (typedArray === undefined) {
814894
formatter = formatArrayBuffer;
815-
} else if (keys.length === 0) {
895+
} else if (keys.length === 0 && protoProps === undefined) {
816896
return prefix +
817897
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
818898
}
@@ -836,7 +916,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
836916
formatter = formatNamespaceObject;
837917
} else if (isBoxedPrimitive(value)) {
838918
base = getBoxedBase(value, ctx, keys, constructor, tag);
839-
if (keys.length === 0) {
919+
if (keys.length === 0 && protoProps === undefined) {
840920
return base;
841921
}
842922
} else {
@@ -856,7 +936,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
856936
formatter = formatIterator;
857937
// Handle other regular objects again.
858938
} else {
859-
if (keys.length === 0) {
939+
if (keys.length === 0 && protoProps === undefined) {
860940
if (isExternal(value))
861941
return ctx.stylize('[External]', 'special');
862942
return `${getCtxStyle(value, constructor, tag)}{}`;
@@ -884,6 +964,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
884964
output.push(
885965
formatProperty(ctx, value, recurseTimes, keys[i], extrasType));
886966
}
967+
if (protoProps !== undefined) {
968+
output.push(...protoProps);
969+
}
887970
} catch (err) {
888971
const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1);
889972
return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl);
@@ -1350,6 +1433,7 @@ function formatTypedArray(ctx, value, recurseTimes) {
13501433
}
13511434
if (ctx.showHidden) {
13521435
// .buffer goes last, it's not a primitive like the others.
1436+
// All besides `BYTES_PER_ELEMENT` are actually getters.
13531437
ctx.indentationLvl += 2;
13541438
for (const key of [
13551439
'BYTES_PER_ELEMENT',
@@ -1498,10 +1582,10 @@ function formatPromise(ctx, value, recurseTimes) {
14981582
return output;
14991583
}
15001584

1501-
function formatProperty(ctx, value, recurseTimes, key, type) {
1585+
function formatProperty(ctx, value, recurseTimes, key, type, desc) {
15021586
let name, str;
15031587
let extra = ' ';
1504-
const desc = ObjectGetOwnPropertyDescriptor(value, key) ||
1588+
desc = desc || ObjectGetOwnPropertyDescriptor(value, key) ||
15051589
{ value: value[key], enumerable: true };
15061590
if (desc.value !== undefined) {
15071591
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.
@@ -2588,3 +2604,65 @@ assert.strictEqual(
25882604
throw err;
25892605
}
25902606
}
2607+
2608+
// Inspect prototype properties.
2609+
{
2610+
class Foo extends Map {
2611+
prop = false;
2612+
prop2 = true;
2613+
get abc() {
2614+
return true;
2615+
}
2616+
get def() {
2617+
return false;
2618+
}
2619+
set def(v) {}
2620+
get xyz() {
2621+
return 'Should be ignored';
2622+
}
2623+
func(a) {}
2624+
[util.inspect.custom]() {
2625+
return this;
2626+
}
2627+
}
2628+
2629+
class Bar extends Foo {
2630+
abc = true;
2631+
prop = true;
2632+
get xyz() {
2633+
return 'YES!';
2634+
}
2635+
[util.inspect.custom]() {
2636+
return this;
2637+
}
2638+
}
2639+
2640+
const bar = new Bar();
2641+
2642+
assert.strictEqual(
2643+
inspect(bar),
2644+
'Bar [Map] { prop: true, prop2: true, abc: true }'
2645+
);
2646+
assert.strictEqual(
2647+
inspect(bar, { showHidden: true, getters: true, colors: false }),
2648+
'Bar [Map] {\n' +
2649+
' [size]: 0,\n' +
2650+
' prop: true,\n' +
2651+
' prop2: true,\n' +
2652+
' abc: true,\n' +
2653+
" [xyz]: [Getter: 'YES!'],\n" +
2654+
' [def]: [Getter/Setter: false]\n' +
2655+
'}'
2656+
);
2657+
assert.strictEqual(
2658+
inspect(bar, { showHidden: true, getters: false, colors: true }),
2659+
'Bar [Map] {\n' +
2660+
' [size]: \x1B[33m0\x1B[39m,\n' +
2661+
' prop: \x1B[33mtrue\x1B[39m,\n' +
2662+
' prop2: \x1B[33mtrue\x1B[39m,\n' +
2663+
' abc: \x1B[33mtrue\x1B[39m,\n' +
2664+
' \x1B[2m[xyz]: \x1B[36m[Getter]\x1B[39m\x1B[22m,\n' +
2665+
' \x1B[2m[def]: \x1B[36m[Getter/Setter]\x1B[39m\x1B[22m\n' +
2666+
'}'
2667+
);
2668+
}

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)