Skip to content

Commit fb8b483

Browse files
committed
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 <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 28ee032 commit fb8b483

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
@@ -453,14 +453,20 @@ function getEmptyFormatArray() {
453453
return [];
454454
}
455455

456-
function getConstructorName(obj, ctx, recurseTimes) {
456+
function getConstructorName(obj, ctx, recurseTimes, protoProps) {
457457
let firstProto;
458458
const tmp = obj;
459459
while (obj) {
460460
const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
461461
if (descriptor !== undefined &&
462462
typeof descriptor.value === 'function' &&
463463
descriptor.value.name !== '') {
464+
if (protoProps !== undefined &&
465+
!builtInObjects.has(descriptor.value.name)) {
466+
const isProto = firstProto !== undefined;
467+
addPrototypeProperties(
468+
ctx, tmp, obj, recurseTimes, isProto, protoProps);
469+
}
464470
return descriptor.value.name;
465471
}
466472

@@ -480,7 +486,8 @@ function getConstructorName(obj, ctx, recurseTimes) {
480486
return `${res} <Complex prototype>`;
481487
}
482488

483-
const protoConstr = getConstructorName(firstProto, ctx, recurseTimes + 1);
489+
const protoConstr = getConstructorName(
490+
firstProto, ctx, recurseTimes + 1, protoProps);
484491

485492
if (protoConstr === null) {
486493
return `${res} <${inspect(firstProto, {
@@ -493,6 +500,68 @@ function getConstructorName(obj, ctx, recurseTimes) {
493500
return `${res} <${protoConstr}>`;
494501
}
495502

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

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

700-
const constructor = getConstructorName(value, ctx, recurseTimes);
701779
let tag = value[SymbolToStringTag];
702780
// Only list the tag in case it's non-enumerable / not an own property.
703781
// Otherwise we'd print this twice.
@@ -727,21 +805,21 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
727805
// Only set the constructor for non ordinary ("Array [...]") arrays.
728806
const prefix = getPrefix(constructor, tag, 'Array');
729807
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
730-
if (value.length === 0 && keys.length === 0)
808+
if (value.length === 0 && keys.length === 0 && protoProps === undefined)
731809
return `${braces[0]}]`;
732810
extrasType = kArrayExtrasType;
733811
formatter = formatArray;
734812
} else if (isSet(value)) {
735813
keys = getKeys(value, ctx.showHidden);
736814
const prefix = getPrefix(constructor, tag, 'Set');
737-
if (value.size === 0 && keys.length === 0)
815+
if (value.size === 0 && keys.length === 0 && protoProps === undefined)
738816
return `${prefix}{}`;
739817
braces = [`${prefix}{`, '}'];
740818
formatter = formatSet;
741819
} else if (isMap(value)) {
742820
keys = getKeys(value, ctx.showHidden);
743821
const prefix = getPrefix(constructor, tag, 'Map');
744-
if (value.size === 0 && keys.length === 0)
822+
if (value.size === 0 && keys.length === 0 && protoProps === undefined)
745823
return `${prefix}{}`;
746824
braces = [`${prefix}{`, '}'];
747825
formatter = formatMap;
@@ -776,12 +854,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
776854
} else if (tag !== '') {
777855
braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
778856
}
779-
if (keys.length === 0) {
857+
if (keys.length === 0 && protoProps === undefined) {
780858
return `${braces[0]}}`;
781859
}
782860
} else if (typeof value === 'function') {
783861
base = getFunctionBase(value, constructor, tag);
784-
if (keys.length === 0)
862+
if (keys.length === 0 && protoProps === undefined)
785863
return ctx.stylize(base, 'special');
786864
} else if (isRegExp(value)) {
787865
// Make RegExps say that they are RegExps
@@ -791,8 +869,10 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
791869
const prefix = getPrefix(constructor, tag, 'RegExp');
792870
if (prefix !== 'RegExp ')
793871
base = `${prefix}${base}`;
794-
if (keys.length === 0 || (recurseTimes > ctx.depth && ctx.depth !== null))
872+
if ((keys.length === 0 && protoProps === undefined) ||
873+
(recurseTimes > ctx.depth && ctx.depth !== null)) {
795874
return ctx.stylize(base, 'regexp');
875+
}
796876
} else if (isDate(value)) {
797877
// Make dates with properties first say the date
798878
base = NumberIsNaN(DatePrototypeGetTime(value)) ?
@@ -801,12 +881,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
801881
const prefix = getPrefix(constructor, tag, 'Date');
802882
if (prefix !== 'Date ')
803883
base = `${prefix}${base}`;
804-
if (keys.length === 0) {
884+
if (keys.length === 0 && protoProps === undefined) {
805885
return ctx.stylize(base, 'date');
806886
}
807887
} else if (isError(value)) {
808888
base = formatError(value, constructor, tag, ctx);
809-
if (keys.length === 0)
889+
if (keys.length === 0 && protoProps === undefined)
810890
return base;
811891
} else if (isAnyArrayBuffer(value)) {
812892
// Fast path for ArrayBuffer and SharedArrayBuffer.
@@ -817,7 +897,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
817897
const prefix = getPrefix(constructor, tag, arrayType);
818898
if (typedArray === undefined) {
819899
formatter = formatArrayBuffer;
820-
} else if (keys.length === 0) {
900+
} else if (keys.length === 0 && protoProps === undefined) {
821901
return prefix +
822902
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
823903
}
@@ -841,7 +921,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
841921
formatter = formatNamespaceObject;
842922
} else if (isBoxedPrimitive(value)) {
843923
base = getBoxedBase(value, ctx, keys, constructor, tag);
844-
if (keys.length === 0) {
924+
if (keys.length === 0 && protoProps === undefined) {
845925
return base;
846926
}
847927
} else {
@@ -861,7 +941,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
861941
formatter = formatIterator;
862942
// Handle other regular objects again.
863943
} else {
864-
if (keys.length === 0) {
944+
if (keys.length === 0 && protoProps === undefined) {
865945
if (isExternal(value))
866946
return ctx.stylize('[External]', 'special');
867947
return `${getCtxStyle(value, constructor, tag)}{}`;
@@ -889,6 +969,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
889969
output.push(
890970
formatProperty(ctx, value, recurseTimes, keys[i], extrasType));
891971
}
972+
if (protoProps !== undefined) {
973+
output.push(...protoProps);
974+
}
892975
} catch (err) {
893976
const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1);
894977
return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl);
@@ -1355,6 +1438,7 @@ function formatTypedArray(ctx, value, recurseTimes) {
13551438
}
13561439
if (ctx.showHidden) {
13571440
// .buffer goes last, it's not a primitive like the others.
1441+
// All besides `BYTES_PER_ELEMENT` are actually getters.
13581442
ctx.indentationLvl += 2;
13591443
for (const key of [
13601444
'BYTES_PER_ELEMENT',
@@ -1503,10 +1587,10 @@ function formatPromise(ctx, value, recurseTimes) {
15031587
return output;
15041588
}
15051589

1506-
function formatProperty(ctx, value, recurseTimes, key, type) {
1590+
function formatProperty(ctx, value, recurseTimes, key, type, desc) {
15071591
let name, str;
15081592
let extra = ' ';
1509-
const desc = ObjectGetOwnPropertyDescriptor(value, key) ||
1593+
desc = desc || ObjectGetOwnPropertyDescriptor(value, key) ||
15101594
{ value: value[key], enumerable: true };
15111595
if (desc.value !== undefined) {
15121596
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
@@ -392,8 +392,24 @@ assert.strictEqual(
392392
{
393393
class CustomArray extends Array {}
394394
CustomArray.prototype[5] = 'foo';
395+
CustomArray.prototype[49] = 'bar';
396+
CustomArray.prototype.foo = true;
395397
const arr = new CustomArray(50);
396-
assert.strictEqual(util.inspect(arr), 'CustomArray [ <50 empty items> ]');
398+
arr[49] = 'I win';
399+
assert.strictEqual(
400+
util.inspect(arr),
401+
"CustomArray [ <49 empty items>, 'I win' ]"
402+
);
403+
assert.strictEqual(
404+
util.inspect(arr, { showHidden: true }),
405+
'CustomArray [\n' +
406+
' <49 empty items>,\n' +
407+
" 'I win',\n" +
408+
' [length]: 50,\n' +
409+
" '5': 'foo',\n" +
410+
' foo: true\n' +
411+
']'
412+
);
397413
}
398414

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

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)