Skip to content

Commit 31e0dbc

Browse files
devsnekaddaleax
authored andcommitted
util: use @@toStringTag
uses @@toStringTag when creating the "tag" for an inspected value PR-URL: #16956 Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Timothy Gu <[email protected]> Reviewed-By: Brian White <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent f6926d5 commit 31e0dbc

File tree

4 files changed

+147
-36
lines changed

4 files changed

+147
-36
lines changed

doc/api/util.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,24 @@ changes:
350350
The `util.inspect()` method returns a string representation of `object` that is
351351
primarily useful for debugging. Additional `options` may be passed that alter
352352
certain aspects of the formatted string.
353+
`util.inspect()` will use the constructor's name and/or `@@toStringTag` to make an
354+
identifiable tag for an inspected value.
355+
356+
```js
357+
class Foo {
358+
get [Symbol.toStringTag]() {
359+
return 'bar';
360+
}
361+
}
362+
363+
class Bar {}
364+
365+
const baz = Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } });
366+
367+
util.inspect(new Foo()); // 'Foo [bar] {}'
368+
util.inspect(new Bar()); // 'Bar {}'
369+
util.inspect(baz); // '[foo] {}'
370+
```
353371

354372
The following example inspects all properties of the `util` object:
355373

lib/internal/util.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,43 @@ function getConstructorOf(obj) {
220220
return null;
221221
}
222222

223+
// getConstructorOf is wrapped into this to save iterations
224+
function getIdentificationOf(obj) {
225+
const original = obj;
226+
let constructor = undefined;
227+
let tag = undefined;
228+
229+
while (obj) {
230+
if (constructor === undefined) {
231+
const desc = Object.getOwnPropertyDescriptor(obj, 'constructor');
232+
if (desc !== undefined &&
233+
typeof desc.value === 'function' &&
234+
desc.value.name !== '')
235+
constructor = desc.value.name;
236+
}
237+
238+
if (tag === undefined) {
239+
const desc = Object.getOwnPropertyDescriptor(obj, Symbol.toStringTag);
240+
if (desc !== undefined) {
241+
if (typeof desc.value === 'string') {
242+
tag = desc.value;
243+
} else if (desc.get !== undefined) {
244+
tag = desc.get.call(original);
245+
if (typeof tag !== 'string')
246+
tag = undefined;
247+
}
248+
}
249+
}
250+
251+
if (constructor !== undefined && tag !== undefined)
252+
break;
253+
254+
obj = Object.getPrototypeOf(obj);
255+
}
256+
257+
return { constructor, tag };
258+
}
259+
223260
const kCustomPromisifiedSymbol = Symbol('util.promisify.custom');
224261
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');
225262

@@ -310,6 +347,7 @@ module.exports = {
310347
emitExperimentalWarning,
311348
filterDuplicateStrings,
312349
getConstructorOf,
350+
getIdentificationOf,
313351
isError,
314352
join,
315353
normalizeEncoding,

lib/util.js

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
const {
5757
customInspectSymbol,
5858
deprecate,
59-
getConstructorOf,
59+
getIdentificationOf,
6060
isError,
6161
promisify,
6262
join
@@ -429,9 +429,15 @@ function formatValue(ctx, value, recurseTimes, ln) {
429429
}
430430

431431
const keyLength = keys.length + symbols.length;
432-
const constructor = getConstructorOf(value);
433-
const ctorName = constructor && constructor.name ?
434-
`${constructor.name} ` : '';
432+
433+
const { constructor, tag } = getIdentificationOf(value);
434+
var prefix = '';
435+
if (constructor && tag && constructor !== tag)
436+
prefix = `${constructor} [${tag}] `;
437+
else if (constructor)
438+
prefix = `${constructor} `;
439+
else if (tag)
440+
prefix = `[${tag}] `;
435441

436442
var base = '';
437443
var formatter = formatObject;
@@ -444,28 +450,28 @@ function formatValue(ctx, value, recurseTimes, ln) {
444450
noIterator = false;
445451
if (Array.isArray(value)) {
446452
// Only set the constructor for non ordinary ("Array [...]") arrays.
447-
braces = [`${ctorName === 'Array ' ? '' : ctorName}[`, ']'];
453+
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
448454
if (value.length === 0 && keyLength === 0)
449455
return `${braces[0]}]`;
450456
formatter = formatArray;
451457
} else if (isSet(value)) {
452458
if (value.size === 0 && keyLength === 0)
453-
return `${ctorName}{}`;
454-
braces = [`${ctorName}{`, '}'];
459+
return `${prefix}{}`;
460+
braces = [`${prefix}{`, '}'];
455461
formatter = formatSet;
456462
} else if (isMap(value)) {
457463
if (value.size === 0 && keyLength === 0)
458-
return `${ctorName}{}`;
459-
braces = [`${ctorName}{`, '}'];
464+
return `${prefix}{}`;
465+
braces = [`${prefix}{`, '}'];
460466
formatter = formatMap;
461467
} else if (isTypedArray(value)) {
462-
braces = [`${ctorName}[`, ']'];
468+
braces = [`${prefix}[`, ']'];
463469
formatter = formatTypedArray;
464470
} else if (isMapIterator(value)) {
465-
braces = ['MapIterator {', '}'];
471+
braces = [`[${tag}] {`, '}'];
466472
formatter = formatMapIterator;
467473
} else if (isSetIterator(value)) {
468-
braces = ['SetIterator {', '}'];
474+
braces = [`[${tag}] {`, '}'];
469475
formatter = formatSetIterator;
470476
} else {
471477
// Check for boxed strings with valueOf()
@@ -491,12 +497,13 @@ function formatValue(ctx, value, recurseTimes, ln) {
491497
}
492498
if (noIterator) {
493499
braces = ['{', '}'];
494-
if (ctorName === 'Object ') {
500+
if (prefix === 'Object ') {
495501
// Object fast path
496502
if (keyLength === 0)
497503
return '{}';
498504
} else if (typeof value === 'function') {
499-
const name = `${constructor.name}${value.name ? `: ${value.name}` : ''}`;
505+
const name =
506+
`${constructor || tag}${value.name ? `: ${value.name}` : ''}`;
500507
if (keyLength === 0)
501508
return ctx.stylize(`[${name}]`, 'special');
502509
base = ` [${name}]`;
@@ -523,16 +530,16 @@ function formatValue(ctx, value, recurseTimes, ln) {
523530
// Can't do the same for DataView because it has a non-primitive
524531
// .buffer property that we need to recurse for.
525532
if (keyLength === 0)
526-
return ctorName +
533+
return prefix +
527534
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
528-
braces[0] = `${ctorName}{`;
535+
braces[0] = `${prefix}{`;
529536
keys.unshift('byteLength');
530537
} else if (isDataView(value)) {
531-
braces[0] = `${ctorName}{`;
538+
braces[0] = `${prefix}{`;
532539
// .buffer goes last, it's not a primitive like the others.
533540
keys.unshift('byteLength', 'byteOffset', 'buffer');
534541
} else if (isPromise(value)) {
535-
braces[0] = `${ctorName}{`;
542+
braces[0] = `${prefix}{`;
536543
formatter = formatPromise;
537544
} else {
538545
// Check boxed primitives other than string with valueOf()
@@ -560,22 +567,21 @@ function formatValue(ctx, value, recurseTimes, ln) {
560567
} else if (keyLength === 0) {
561568
if (isExternal(value))
562569
return ctx.stylize('[External]', 'special');
563-
return `${ctorName}{}`;
570+
return `${prefix}{}`;
564571
} else {
565-
braces[0] = `${ctorName}{`;
572+
braces[0] = `${prefix}{`;
566573
}
567574
}
568575
}
569576

570577
// Using an array here is actually better for the average case than using
571-
// a Set. `seen` will only check for the depth and will never grow to large.
578+
// a Set. `seen` will only check for the depth and will never grow too large.
572579
if (ctx.seen.indexOf(value) !== -1)
573580
return ctx.stylize('[Circular]', 'special');
574581

575582
if (recurseTimes != null) {
576583
if (recurseTimes < 0)
577-
return ctx.stylize(`[${constructor ? constructor.name : 'Object'}]`,
578-
'special');
584+
return ctx.stylize(`[${constructor || tag || 'Object'}]`, 'special');
579585
recurseTimes -= 1;
580586
}
581587

test/parallel/test-util-inspect.js

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -923,27 +923,27 @@ if (typeof Symbol !== 'undefined') {
923923
// Test Map iterators
924924
{
925925
const map = new Map([['foo', 'bar']]);
926-
assert.strictEqual(util.inspect(map.keys()), 'MapIterator { \'foo\' }');
927-
assert.strictEqual(util.inspect(map.values()), 'MapIterator { \'bar\' }');
926+
assert.strictEqual(util.inspect(map.keys()), '[Map Iterator] { \'foo\' }');
927+
assert.strictEqual(util.inspect(map.values()), '[Map Iterator] { \'bar\' }');
928928
assert.strictEqual(util.inspect(map.entries()),
929-
'MapIterator { [ \'foo\', \'bar\' ] }');
929+
'[Map Iterator] { [ \'foo\', \'bar\' ] }');
930930
// make sure the iterator doesn't get consumed
931931
const keys = map.keys();
932-
assert.strictEqual(util.inspect(keys), 'MapIterator { \'foo\' }');
933-
assert.strictEqual(util.inspect(keys), 'MapIterator { \'foo\' }');
932+
assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }');
933+
assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }');
934934
}
935935

936936
// Test Set iterators
937937
{
938938
const aSet = new Set([1, 3]);
939-
assert.strictEqual(util.inspect(aSet.keys()), 'SetIterator { 1, 3 }');
940-
assert.strictEqual(util.inspect(aSet.values()), 'SetIterator { 1, 3 }');
939+
assert.strictEqual(util.inspect(aSet.keys()), '[Set Iterator] { 1, 3 }');
940+
assert.strictEqual(util.inspect(aSet.values()), '[Set Iterator] { 1, 3 }');
941941
assert.strictEqual(util.inspect(aSet.entries()),
942-
'SetIterator { [ 1, 1 ], [ 3, 3 ] }');
942+
'[Set Iterator] { [ 1, 1 ], [ 3, 3 ] }');
943943
// make sure the iterator doesn't get consumed
944944
const keys = aSet.keys();
945-
assert.strictEqual(util.inspect(keys), 'SetIterator { 1, 3 }');
946-
assert.strictEqual(util.inspect(keys), 'SetIterator { 1, 3 }');
945+
assert.strictEqual(util.inspect(keys), '[Set Iterator] { 1, 3 }');
946+
assert.strictEqual(util.inspect(keys), '[Set Iterator] { 1, 3 }');
947947
}
948948

949949
// Test alignment of items in container
@@ -996,11 +996,11 @@ if (typeof Symbol !== 'undefined') {
996996
assert.strictEqual(util.inspect(new ArraySubclass(1, 2, 3)),
997997
'ArraySubclass [ 1, 2, 3 ]');
998998
assert.strictEqual(util.inspect(new SetSubclass([1, 2, 3])),
999-
'SetSubclass { 1, 2, 3 }');
999+
'SetSubclass [Set] { 1, 2, 3 }');
10001000
assert.strictEqual(util.inspect(new MapSubclass([['foo', 42]])),
1001-
'MapSubclass { \'foo\' => 42 }');
1001+
'MapSubclass [Map] { \'foo\' => 42 }');
10021002
assert.strictEqual(util.inspect(new PromiseSubclass(() => {})),
1003-
'PromiseSubclass { <pending> }');
1003+
'PromiseSubclass [Promise] { <pending> }');
10041004
assert.strictEqual(
10051005
util.inspect({ a: { b: new ArraySubclass([1, [2], 3]) } }, { depth: 1 }),
10061006
'{ a: { b: [ArraySubclass] } }'
@@ -1162,3 +1162,52 @@ assert.doesNotThrow(() => util.inspect(process));
11621162
const obj = { inspect: 'fhqwhgads' };
11631163
assert.strictEqual(util.inspect(obj), "{ inspect: 'fhqwhgads' }");
11641164
}
1165+
1166+
{
1167+
// @@toStringTag
1168+
assert.strictEqual(util.inspect({ [Symbol.toStringTag]: 'a' }),
1169+
'Object [a] { [Symbol(Symbol.toStringTag)]: \'a\' }');
1170+
1171+
class Foo {
1172+
constructor() {
1173+
this.foo = 'bar';
1174+
}
1175+
1176+
get [Symbol.toStringTag]() {
1177+
return this.foo;
1178+
}
1179+
}
1180+
1181+
assert.strictEqual(util.inspect(
1182+
Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })),
1183+
'[foo] {}');
1184+
1185+
assert.strictEqual(util.inspect(new Foo()), 'Foo [bar] { foo: \'bar\' }');
1186+
1187+
assert.strictEqual(
1188+
util.inspect(new (class extends Foo {})()),
1189+
'Foo [bar] { foo: \'bar\' }');
1190+
1191+
assert.strictEqual(
1192+
util.inspect(Object.create(Object.create(Foo.prototype), {
1193+
foo: { value: 'bar', enumerable: true }
1194+
})),
1195+
'Foo [bar] { foo: \'bar\' }');
1196+
1197+
class ThrowingClass {
1198+
get [Symbol.toStringTag]() {
1199+
throw new Error('toStringTag error');
1200+
}
1201+
}
1202+
1203+
assert.throws(() => util.inspect(new ThrowingClass()), /toStringTag error/);
1204+
1205+
class NotStringClass {
1206+
get [Symbol.toStringTag]() {
1207+
return null;
1208+
}
1209+
}
1210+
1211+
assert.strictEqual(util.inspect(new NotStringClass()),
1212+
'NotStringClass {}');
1213+
}

0 commit comments

Comments
 (0)