Skip to content

Commit

Permalink
util: use @@toStringTag
Browse files Browse the repository at this point in the history
uses @@toStringTag when creating the "tag" for an inspected value

PR-URL: #16956
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
Reviewed-By: Brian White <mscdex@mscdex.net>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
devsnek authored and addaleax committed Dec 1, 2017
1 parent f6926d5 commit 31e0dbc
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 36 deletions.
18 changes: 18 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,24 @@ changes:
The `util.inspect()` method returns a string representation of `object` that is
primarily useful for debugging. Additional `options` may be passed that alter
certain aspects of the formatted string.
`util.inspect()` will use the constructor's name and/or `@@toStringTag` to make an
identifiable tag for an inspected value.

```js
class Foo {
get [Symbol.toStringTag]() {
return 'bar';
}
}

class Bar {}

const baz = Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } });

util.inspect(new Foo()); // 'Foo [bar] {}'
util.inspect(new Bar()); // 'Bar {}'
util.inspect(baz); // '[foo] {}'
```

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

Expand Down
38 changes: 38 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,43 @@ function getConstructorOf(obj) {
return null;
}

// getConstructorOf is wrapped into this to save iterations
function getIdentificationOf(obj) {
const original = obj;
let constructor = undefined;
let tag = undefined;

while (obj) {
if (constructor === undefined) {
const desc = Object.getOwnPropertyDescriptor(obj, 'constructor');
if (desc !== undefined &&
typeof desc.value === 'function' &&
desc.value.name !== '')
constructor = desc.value.name;
}

if (tag === undefined) {
const desc = Object.getOwnPropertyDescriptor(obj, Symbol.toStringTag);
if (desc !== undefined) {
if (typeof desc.value === 'string') {
tag = desc.value;
} else if (desc.get !== undefined) {
tag = desc.get.call(original);
if (typeof tag !== 'string')
tag = undefined;
}
}
}

if (constructor !== undefined && tag !== undefined)
break;

obj = Object.getPrototypeOf(obj);
}

return { constructor, tag };
}

const kCustomPromisifiedSymbol = Symbol('util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');

Expand Down Expand Up @@ -310,6 +347,7 @@ module.exports = {
emitExperimentalWarning,
filterDuplicateStrings,
getConstructorOf,
getIdentificationOf,
isError,
join,
normalizeEncoding,
Expand Down
52 changes: 29 additions & 23 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const {
const {
customInspectSymbol,
deprecate,
getConstructorOf,
getIdentificationOf,
isError,
promisify,
join
Expand Down Expand Up @@ -429,9 +429,15 @@ function formatValue(ctx, value, recurseTimes, ln) {
}

const keyLength = keys.length + symbols.length;
const constructor = getConstructorOf(value);
const ctorName = constructor && constructor.name ?
`${constructor.name} ` : '';

const { constructor, tag } = getIdentificationOf(value);
var prefix = '';
if (constructor && tag && constructor !== tag)
prefix = `${constructor} [${tag}] `;
else if (constructor)
prefix = `${constructor} `;
else if (tag)
prefix = `[${tag}] `;

var base = '';
var formatter = formatObject;
Expand All @@ -444,28 +450,28 @@ function formatValue(ctx, value, recurseTimes, ln) {
noIterator = false;
if (Array.isArray(value)) {
// Only set the constructor for non ordinary ("Array [...]") arrays.
braces = [`${ctorName === 'Array ' ? '' : ctorName}[`, ']'];
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
if (value.length === 0 && keyLength === 0)
return `${braces[0]}]`;
formatter = formatArray;
} else if (isSet(value)) {
if (value.size === 0 && keyLength === 0)
return `${ctorName}{}`;
braces = [`${ctorName}{`, '}'];
return `${prefix}{}`;
braces = [`${prefix}{`, '}'];
formatter = formatSet;
} else if (isMap(value)) {
if (value.size === 0 && keyLength === 0)
return `${ctorName}{}`;
braces = [`${ctorName}{`, '}'];
return `${prefix}{}`;
braces = [`${prefix}{`, '}'];
formatter = formatMap;
} else if (isTypedArray(value)) {
braces = [`${ctorName}[`, ']'];
braces = [`${prefix}[`, ']'];
formatter = formatTypedArray;
} else if (isMapIterator(value)) {
braces = ['MapIterator {', '}'];
braces = [`[${tag}] {`, '}'];
formatter = formatMapIterator;
} else if (isSetIterator(value)) {
braces = ['SetIterator {', '}'];
braces = [`[${tag}] {`, '}'];
formatter = formatSetIterator;
} else {
// Check for boxed strings with valueOf()
Expand All @@ -491,12 +497,13 @@ function formatValue(ctx, value, recurseTimes, ln) {
}
if (noIterator) {
braces = ['{', '}'];
if (ctorName === 'Object ') {
if (prefix === 'Object ') {
// Object fast path
if (keyLength === 0)
return '{}';
} else if (typeof value === 'function') {
const name = `${constructor.name}${value.name ? `: ${value.name}` : ''}`;
const name =
`${constructor || tag}${value.name ? `: ${value.name}` : ''}`;
if (keyLength === 0)
return ctx.stylize(`[${name}]`, 'special');
base = ` [${name}]`;
Expand All @@ -523,16 +530,16 @@ function formatValue(ctx, value, recurseTimes, ln) {
// Can't do the same for DataView because it has a non-primitive
// .buffer property that we need to recurse for.
if (keyLength === 0)
return ctorName +
return prefix +
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
braces[0] = `${ctorName}{`;
braces[0] = `${prefix}{`;
keys.unshift('byteLength');
} else if (isDataView(value)) {
braces[0] = `${ctorName}{`;
braces[0] = `${prefix}{`;
// .buffer goes last, it's not a primitive like the others.
keys.unshift('byteLength', 'byteOffset', 'buffer');
} else if (isPromise(value)) {
braces[0] = `${ctorName}{`;
braces[0] = `${prefix}{`;
formatter = formatPromise;
} else {
// Check boxed primitives other than string with valueOf()
Expand Down Expand Up @@ -560,22 +567,21 @@ function formatValue(ctx, value, recurseTimes, ln) {
} else if (keyLength === 0) {
if (isExternal(value))
return ctx.stylize('[External]', 'special');
return `${ctorName}{}`;
return `${prefix}{}`;
} else {
braces[0] = `${ctorName}{`;
braces[0] = `${prefix}{`;
}
}
}

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

if (recurseTimes != null) {
if (recurseTimes < 0)
return ctx.stylize(`[${constructor ? constructor.name : 'Object'}]`,
'special');
return ctx.stylize(`[${constructor || tag || 'Object'}]`, 'special');
recurseTimes -= 1;
}

Expand Down
75 changes: 62 additions & 13 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -923,27 +923,27 @@ if (typeof Symbol !== 'undefined') {
// Test Map iterators
{
const map = new Map([['foo', 'bar']]);
assert.strictEqual(util.inspect(map.keys()), 'MapIterator { \'foo\' }');
assert.strictEqual(util.inspect(map.values()), 'MapIterator { \'bar\' }');
assert.strictEqual(util.inspect(map.keys()), '[Map Iterator] { \'foo\' }');
assert.strictEqual(util.inspect(map.values()), '[Map Iterator] { \'bar\' }');
assert.strictEqual(util.inspect(map.entries()),
'MapIterator { [ \'foo\', \'bar\' ] }');
'[Map Iterator] { [ \'foo\', \'bar\' ] }');
// make sure the iterator doesn't get consumed
const keys = map.keys();
assert.strictEqual(util.inspect(keys), 'MapIterator { \'foo\' }');
assert.strictEqual(util.inspect(keys), 'MapIterator { \'foo\' }');
assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }');
assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }');
}

// Test Set iterators
{
const aSet = new Set([1, 3]);
assert.strictEqual(util.inspect(aSet.keys()), 'SetIterator { 1, 3 }');
assert.strictEqual(util.inspect(aSet.values()), 'SetIterator { 1, 3 }');
assert.strictEqual(util.inspect(aSet.keys()), '[Set Iterator] { 1, 3 }');
assert.strictEqual(util.inspect(aSet.values()), '[Set Iterator] { 1, 3 }');
assert.strictEqual(util.inspect(aSet.entries()),
'SetIterator { [ 1, 1 ], [ 3, 3 ] }');
'[Set Iterator] { [ 1, 1 ], [ 3, 3 ] }');
// make sure the iterator doesn't get consumed
const keys = aSet.keys();
assert.strictEqual(util.inspect(keys), 'SetIterator { 1, 3 }');
assert.strictEqual(util.inspect(keys), 'SetIterator { 1, 3 }');
assert.strictEqual(util.inspect(keys), '[Set Iterator] { 1, 3 }');
assert.strictEqual(util.inspect(keys), '[Set Iterator] { 1, 3 }');
}

// Test alignment of items in container
Expand Down Expand Up @@ -996,11 +996,11 @@ if (typeof Symbol !== 'undefined') {
assert.strictEqual(util.inspect(new ArraySubclass(1, 2, 3)),
'ArraySubclass [ 1, 2, 3 ]');
assert.strictEqual(util.inspect(new SetSubclass([1, 2, 3])),
'SetSubclass { 1, 2, 3 }');
'SetSubclass [Set] { 1, 2, 3 }');
assert.strictEqual(util.inspect(new MapSubclass([['foo', 42]])),
'MapSubclass { \'foo\' => 42 }');
'MapSubclass [Map] { \'foo\' => 42 }');
assert.strictEqual(util.inspect(new PromiseSubclass(() => {})),
'PromiseSubclass { <pending> }');
'PromiseSubclass [Promise] { <pending> }');
assert.strictEqual(
util.inspect({ a: { b: new ArraySubclass([1, [2], 3]) } }, { depth: 1 }),
'{ a: { b: [ArraySubclass] } }'
Expand Down Expand Up @@ -1162,3 +1162,52 @@ assert.doesNotThrow(() => util.inspect(process));
const obj = { inspect: 'fhqwhgads' };
assert.strictEqual(util.inspect(obj), "{ inspect: 'fhqwhgads' }");
}

{
// @@toStringTag
assert.strictEqual(util.inspect({ [Symbol.toStringTag]: 'a' }),
'Object [a] { [Symbol(Symbol.toStringTag)]: \'a\' }');

class Foo {
constructor() {
this.foo = 'bar';
}

get [Symbol.toStringTag]() {
return this.foo;
}
}

assert.strictEqual(util.inspect(
Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })),
'[foo] {}');

assert.strictEqual(util.inspect(new Foo()), 'Foo [bar] { foo: \'bar\' }');

assert.strictEqual(
util.inspect(new (class extends Foo {})()),
'Foo [bar] { foo: \'bar\' }');

assert.strictEqual(
util.inspect(Object.create(Object.create(Foo.prototype), {
foo: { value: 'bar', enumerable: true }
})),
'Foo [bar] { foo: \'bar\' }');

class ThrowingClass {
get [Symbol.toStringTag]() {
throw new Error('toStringTag error');
}
}

assert.throws(() => util.inspect(new ThrowingClass()), /toStringTag error/);

class NotStringClass {
get [Symbol.toStringTag]() {
return null;
}
}

assert.strictEqual(util.inspect(new NotStringClass()),
'NotStringClass {}');
}

0 comments on commit 31e0dbc

Please sign in to comment.