From 31e0dbc0c700e7bb8fa453258ba0233975ece575 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 11 Nov 2017 10:35:01 -0600 Subject: [PATCH] util: use @@toStringTag uses @@toStringTag when creating the "tag" for an inspected value PR-URL: https://github.com/nodejs/node/pull/16956 Reviewed-By: Refael Ackermann Reviewed-By: Timothy Gu Reviewed-By: Brian White Reviewed-By: Anna Henningsen Reviewed-By: James M Snell --- doc/api/util.md | 18 +++++++ lib/internal/util.js | 38 +++++++++++++++ lib/util.js | 52 ++++++++++++--------- test/parallel/test-util-inspect.js | 75 ++++++++++++++++++++++++------ 4 files changed, 147 insertions(+), 36 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 8c73242a5e6dab..2d0f7fe365c5a2 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -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: diff --git a/lib/internal/util.js b/lib/internal/util.js index 4b5fa21e7ac474..e5a6803d4e5ee2 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -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'); @@ -310,6 +347,7 @@ module.exports = { emitExperimentalWarning, filterDuplicateStrings, getConstructorOf, + getIdentificationOf, isError, join, normalizeEncoding, diff --git a/lib/util.js b/lib/util.js index 058b5ed6bde145..da8d1f0f9b7169 100644 --- a/lib/util.js +++ b/lib/util.js @@ -56,7 +56,7 @@ const { const { customInspectSymbol, deprecate, - getConstructorOf, + getIdentificationOf, isError, promisify, join @@ -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; @@ -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() @@ -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}]`; @@ -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() @@ -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; } diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 78aa75779ca4a0..f522abf7c29afd 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -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 @@ -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 { }'); + 'PromiseSubclass [Promise] { }'); assert.strictEqual( util.inspect({ a: { b: new ArraySubclass([1, [2], 3]) } }, { depth: 1 }), '{ a: { b: [ArraySubclass] } }' @@ -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 {}'); +}