Skip to content

Commit 7686865

Browse files
BridgeARBethGriggs
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: #30768Fixes: #30183 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent 0376e1c commit 7686865

4 files changed

Lines changed: 198 additions & 22 deletions

File tree

‎doc/api/util.md‎

Lines changed: 6 additions & 1 deletion
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‎

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

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

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

480-
constprotoConstr=getConstructorName(firstProto,ctx,recurseTimes+1);
486+
constprotoConstr=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+
functionaddPrototypeProperties(ctx,main,obj,recurseTimes,isProto,output){
504+
letdepth=0;
505+
letkeys;
506+
letkeySet;
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+
constdescriptor=ObjectGetOwnPropertyDescriptor(obj,'constructor');
516+
if(descriptor!==undefined&&
517+
typeofdescriptor.value==='function'&&
518+
builtInObjects.has(descriptor.value.name)){
519+
return;
520+
}
521+
}else{
522+
isProto=false;
523+
}
524+
525+
if(depth===0){
526+
keySet=newSet();
527+
}else{
528+
keys.forEach((key)=>keySet.add(key));
529+
}
530+
// Get all own property names and symbols.
531+
keys=ObjectGetOwnPropertyNames(obj);
532+
constsymbols=ObjectGetOwnPropertySymbols(obj);
533+
if(symbols.length!==0){
534+
keys.push(...symbols);
535+
}
536+
for(constkeyofkeys){
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+
constdesc=ObjectGetOwnPropertyDescriptor(obj,key);
544+
if(typeofdesc.value==='function'){
545+
continue;
546+
}
547+
constvalue=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
functiongetPrefix(constructor,tag,fallback){
494563
if(constructor===null){
495564
if(tag!==''){
@@ -693,8 +762,17 @@ function formatValue(ctx, value, recurseTimes, typedArray) {
693762

694763
functionformatRaw(ctx,value,recurseTimes,typedArray){
695764
letkeys;
765+
letprotoProps;
766+
if(ctx.showHidden&&(recurseTimes<=ctx.depth||ctx.depth===null)){
767+
protoProps=[];
768+
}
769+
770+
constconstructor=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-
constconstructor=getConstructorName(value,ctx,recurseTimes);
698776
lettag=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
constprefix=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
}elseif(isSet(value)){
732810
keys=getKeys(value,ctx.showHidden);
733811
constprefix=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
}elseif(isMap(value)){
739817
keys=getKeys(value,ctx.showHidden);
740818
constprefix=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
}elseif(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
}elseif(typeofvalue==='function'){
780858
base=getFunctionBase(value,constructor,tag);
781-
if(keys.length===0)
859+
if(keys.length===0&&protoProps===undefined)
782860
returnctx.stylize(base,'special');
783861
}elseif(isRegExp(value)){
784862
// Make RegExps say that they are RegExps
@@ -788,8 +866,10 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
788866
constprefix=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
returnctx.stylize(base,'regexp');
872+
}
793873
}elseif(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
constprefix=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
returnctx.stylize(base,'date');
803883
}
804884
}elseif(isError(value)){
805885
base=formatError(value,constructor,tag,ctx);
806-
if(keys.length===0)
886+
if(keys.length===0&&protoProps===undefined)
807887
returnbase;
808888
}elseif(isAnyArrayBuffer(value)){
809889
// Fast path for ArrayBuffer and SharedArrayBuffer.
@@ -814,7 +894,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
814894
constprefix=getPrefix(constructor,tag,arrayType);
815895
if(typedArray===undefined){
816896
formatter=formatArrayBuffer;
817-
}elseif(keys.length===0){
897+
}elseif(keys.length===0&&protoProps===undefined){
818898
returnprefix+
819899
`{ byteLength: ${formatNumber(ctx.stylize,value.byteLength)} }`;
820900
}
@@ -838,7 +918,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
838918
formatter=formatNamespaceObject;
839919
}elseif(isBoxedPrimitive(value)){
840920
base=getBoxedBase(value,ctx,keys,constructor,tag);
841-
if(keys.length===0){
921+
if(keys.length===0&&protoProps===undefined){
842922
returnbase;
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
returnctx.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
constconstructorName=getCtxStyle(value,constructor,tag).slice(0,-1);
891974
returnhandleMaxCallStackSize(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(constkeyof[
13541438
'BYTES_PER_ELEMENT',
@@ -1497,10 +1581,10 @@ function formatPromise(ctx, value, recurseTimes) {
14971581
returnoutput;
14981582
}
14991583

1500-
functionformatProperty(ctx,value,recurseTimes,key,type){
1584+
functionformatProperty(ctx,value,recurseTimes,key,type,desc){
15011585
letname,str;
15021586
letextra=' ';
1503-
constdesc=ObjectGetOwnPropertyDescriptor(value,key)||
1587+
desc=desc||ObjectGetOwnPropertyDescriptor(value,key)||
15041588
{value: value[key],enumerable: true};
15051589
if(desc.value!==undefined){
15061590
constdiff=(type!==kObjectType||ctx.compact!==true) ? 2 : 3;

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,24 @@ assert.strictEqual(
391391
{
392392
classCustomArrayextendsArray{}
393393
CustomArray.prototype[5]='foo';
394+
CustomArray.prototype[49]='bar';
395+
CustomArray.prototype.foo=true;
394396
constarr=newCustomArray(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
throwerr;
25862602
}
25872603
}
2604+
2605+
// Inspect prototype properties.
2606+
{
2607+
classFooextendsMap{
2608+
prop=false;
2609+
prop2=true;
2610+
getabc(){
2611+
returntrue;
2612+
}
2613+
getdef(){
2614+
returnfalse;
2615+
}
2616+
setdef(v){}
2617+
getxyz(){
2618+
return'Should be ignored';
2619+
}
2620+
func(a){}
2621+
[util.inspect.custom](){
2622+
returnthis;
2623+
}
2624+
}
2625+
2626+
classBarextendsFoo{
2627+
abc=true;
2628+
prop=true;
2629+
getxyz(){
2630+
return'YES!';
2631+
}
2632+
[util.inspect.custom](){
2633+
returnthis;
2634+
}
2635+
}
2636+
2637+
constbar=newBar();
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‎

Lines changed: 13 additions & 4 deletions
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)