Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: use @@toStringTag in util.inspect #16956

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line before this sentence to make this a new paragraph.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not meant to be a new paragraph, newline is just for readability while editing

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] {}'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test for this? Unless I'm mistaken in my reading of the code, baz would be inspected as ' [foo] {}' rather than '[foo] {}' with the current iteration.

Copy link
Member

@joyeecheung joyeecheung Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think util.inspect will also display the Symbol property if it's not defined as enumerable: false, which is the case in this example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

symbols aren't shown unless you pass true for the second arg

```

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 {}');
}