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

Conversation

devsnek
Copy link
Member

@devsnek devsnek commented Nov 11, 2017

Uses @@toStringTag before constructor.name which yields names where there previously weren't or more correct names.

closes #16952

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • docs changes included
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

util

@nodejs-github-bot nodejs-github-bot added the util Issues and PRs related to the built-in util module. label Nov 11, 2017
@devsnek devsnek changed the title util: use @@toStringTag util: use @@toStringTag in util.inspect Nov 11, 2017
Copy link
Contributor

@refack refack left a comment

Choose a reason for hiding this comment

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

Code LGTM but this needs a test.

@refack
Copy link
Contributor

refack commented Nov 11, 2017

@devsnek thank you for the contribution!

lib/util.js Outdated

// try to use @@toStringTag, fall back to constructor name, empty string
let tag = '';
if (hasOwnProperty(value, Symbol.toStringTag)) {
Copy link
Member

Choose a reason for hiding this comment

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

@@toStringTag is inherited. E.g. Object.prototype.toString.call(new Date()) returns [object Date], even though new Date().hasOwnProperty(Symbol.toStringTag) is false. IMO the check for @@toStringTag should be folded into a part of getConstructorOf and searched at the same time by walking the prototype chain (of course, then getConstructorOf would have to be modified to return a string instead).

Copy link
Member Author

Choose a reason for hiding this comment

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

how about getIdentifiableTagOf

Copy link
Member

Choose a reason for hiding this comment

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

Sure.

@addaleax
Copy link
Member

I'm pretty sure I would prefer the constructor name to take precedence over @@toStringTag .. the constructor is at least as specific as the tag, plus this works well with subclasses

Copy link
Contributor

@mscdex mscdex left a comment

Choose a reason for hiding this comment

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

-1 The name before the braces should be a class/constructor name, not anything else, for consistency.

@addaleax
Copy link
Member

@mscdex if you're worried about consistency, would it help to modify how the tag I'd displayed? Eg in braces or sth like that?

@TimothyGu
Copy link
Member

@mscdex

-1 The name before the braces should be a class/constructor name, not anything else, for consistency.

Symbol.toStringTag is used as class/constructor name by the ECMAScript spec itself, for consistency and compatibility with earlier versions of ES through Object.prototype.toString. So should we.

@jasnell
Copy link
Member

jasnell commented Nov 12, 2017

It would be semver-major, but perhaps there is a way to show both.

Given the example

class Foo {
  constructor() {}
  get [Symbol.toStringTag]() { return 'bar' }
}

const f = new Foo();
util.inspect(f)

The current result is Foo { } ... we could make it ... [Foo bar] { } or Foo [bar] { } to include the @@toStringTag value.

@devsnek
Copy link
Member Author

devsnek commented Nov 12, 2017

Foo:bar { }? [Foo bar] would he the most consistent with how Object.prototype.toString works

@addaleax
Copy link
Member

I like Foo [bar] {}, with [bar] {} being used when no constructor name is present and Foo {} when no toStringTag is present?

@jasnell
Copy link
Member

jasnell commented Nov 12, 2017

Works for me.

@devsnek
Copy link
Member Author

devsnek commented Nov 12, 2017

will do 👍

@devsnek
Copy link
Member Author

devsnek commented Nov 12, 2017

are people ok with this producing things like Object [WebAssembly]?

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

Mostly LGTM, thanks for the work!

@@ -203,6 +203,41 @@ function getConstructorOf(obj) {
return null;
}

function getIdentifiableTagOf(obj) {
let namespace = '';
Copy link
Member

Choose a reason for hiding this comment

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

namespace? Why not call it constructor?

if (desc.value !== undefined)
tag = desc.value;
else if (desc.get !== undefined)
tag = desc.get();
Copy link
Member

Choose a reason for hiding this comment

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

get() should be called with the this object being set to the original value of obj… right now this would refer to the descriptor object

Copy link
Member

Choose a reason for hiding this comment

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

Oh, and also maybe wrap this in a try { … } catch, we don’t really know what the getter will be doing

Copy link
Member Author

Choose a reason for hiding this comment

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

if it throws why wouldn't we want that to bubble up to the user?

if (namespace === tag)
return namespace;

return `${namespace} ${tag ? `[${tag}]` : ''}`.trim();
Copy link
Member

Choose a reason for hiding this comment

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

Why the .trim()?

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

Copy link
Member

Choose a reason for hiding this comment

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

Can you add a test like this?

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

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

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

Copy link
Member

Choose a reason for hiding this comment

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

To be clear, I don’t think this is a good way to write a class, but it should work :)

@joyeecheung
Copy link
Member

If we are adding a new syntax, maybe we should explain it in the docs because one might not be able to immediately tell what Foo and bar in Foo [bar] {} represent just by looking at it..

@addaleax
Copy link
Member

@refack @TimothyGu @mscdex PTAL

doc/api/util.md Outdated
@@ -330,6 +330,18 @@ 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 `@@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.

Nit: blank line before this to start the new paragraph.

doc/api/util.md Outdated
}

util.inspect(new Foo); // 'Foo [bar] {}'
```
Copy link
Member

Choose a reason for hiding this comment

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

I would also add some detail on what happens if @@toStringTag and construtor name are not present... e.g.

`util.inspect()` will use the constructor's name and `@@toStringTag` (if present) to make
an identifiable tag for an inspected value:

Then the examples:

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

util.inspect(new Foo()); // 'Foo [bar] {}`
function foo() {
  return new (class {
    get [Symbol.toStringTag]() { return 'bar'; }
  });
}
util.inspect(foo()); // '[bar] {}`
class Foo {}

util.inspect(new Foo()); // 'Foo {}`

Copy link
Member Author

@devsnek devsnek Nov 12, 2017

Choose a reason for hiding this comment

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

that second one would actually be Object [bar] {}, i'm not sure that constructor name will ever be absent.

Object.create(null, { [Symbol.toStringTag]: { value: 'hi' } })

Copy link
Member

Choose a reason for hiding this comment

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

> function m() {
... return new (class {})
... }
undefined
> m()
{}
> m().constructor.name
''
>

Copy link
Member Author

@devsnek devsnek Nov 12, 2017

Choose a reason for hiding this comment

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

> new (class {})
{}
> new (class { get [Symbol.toStringTag]() { return 'tst'; } })
Object [tst] {}

not exactly sure whats going on here but it looks like if the class is empty the inheritance from Object is optimised away?

In any case i think Object.create is a really good way to show that the inspected value has no constructor

doc/api/util.md Outdated

class Bar {}

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

Choose a reason for hiding this comment

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

s/Baz/baz since it's not a constructor

doc/api/util.md Outdated

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

util.inspect(new Foo); // 'Foo [bar] {}'
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's better to keep the parentheses after the constructor name, here and on the line below.

@mscdex
Copy link
Contributor

mscdex commented Nov 12, 2017

As far as the current layout/display goes, I guess it's ok for now, definitely much better than the original.

@addaleax addaleax added the semver-minor PRs that contain new features and should be released in the next minor version. label Nov 12, 2017
if (constructor === tag)
return constructor;

return `${constructor} ${tag ? `[${tag}]` : ''}`.trim();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we could instead just branch on tag to avoid the trim(). Something like:

if (tag)
  return `${constructor} [${tag}]`;

return constructor;

lib/util.js Outdated
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(`[${tag.trim() || 'Object'}]`, 'special');
Copy link
Contributor

@mscdex mscdex Nov 13, 2017

Choose a reason for hiding this comment

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

We could avoid trim() here if we saved the original getIdentifiableTagOf() result. Maybe change the current tag variable to prefix or something to differentiate between the two?

@refack
Copy link
Contributor

refack commented Nov 15, 2017

I agree that if this was only util.inspect that this would have been an exception, but since console.log is wired to use util.inspect IMHO it escalates the impact of this change. Using console.log is ubiquitous, and it's not just debugging.

(Simplest use-case I can think of is a test case that checks process output against a static fixture. If somewhere it outputs an iterator, after this change it will fail.)

BTW: this is actually diverging from chrome:
image
node after this change:

> class subm extends Map {}
undefined
> console.log(new subm().keys())
[Map Iterator] {  }
undefined
> console.log(new subm())
subm [Map] {}
undefined

@refack
Copy link
Contributor

refack commented Nov 15, 2017

P.S. if we flag util.inspect to keep the old behavior when called from console.log I agree this is minor.

@devsnek
Copy link
Member Author

devsnek commented Nov 15, 2017

I would agree that console.log doesn't need all this. i just wanted this for repl

@addaleax
Copy link
Member

I don’t think letting console.log and util.inspect diverge is a good idea. I’d rather have this be semver-major than jumping to that…

@TimothyGu
Copy link
Member

@refack Using console.log on anything but a primitive is ambiguous by default, and in my opinion leaves significant leeway to what we can output for objects. For a standardized, structured, and parsable output one should always use something like JSON.stringify. And for the comparison to DevTools -- as long as we are showing at least as much information as DevTools, I would be entirely fine with not showing exactly what DevTools does.


Thought: if constructor.name is not available, use @@toStringTag as the constructor name without any additional indication. This would make new Map().keys() emit Map Iterator {} rather than [Map Iterator] {}, similar to Firefox. MapIterator (what DevTools shows) has no basis in the ES spec.

@addaleax
Copy link
Member

@TimothyGu As I understand it, both @mscdex and me are -1 on that – showing something that may or may not be a constructor name seems to introduce inconsistency without any need for it…

@devsnek
Copy link
Member Author

devsnek commented Nov 15, 2017

as long as we are showing at least as much information as DevTools, I would be entirely fine with not showing exactly what DevTools does.

+1

@TimothyGu

if constructor.name is not available, use @@toStringTag as the constructor name without any additional indication.

I was considering this but i think keeping it distinct is the best option so that when debugging people know where the names are coming from.

@refack
Copy link
Contributor

refack commented Nov 15, 2017

Using console.log on anything but a primitive is ambiguous by default, and in my opinion leaves significant leeway to what we can output for objects. For a standardized, structured, and parsable output one should always use something like JSON.stringify. And for the comparison to DevTools -- as long as we are showing at least as much information as DevTools, I would be entirely fine with not showing exactly what DevTools does.

I agree with both statements (log ambiguity, and DevTools!=standard), but in terms of risk/reward IMHO this feature is not worth the risk. It's just my gut feeling, as I don't know what kind of code is out there. I'm not strongly opposed to this landing as non-major, but I'd advise against it.
BTW If we land this as major it'll be available in the nightly build for use as an exploratory REPL tool.

@evanlucas
Copy link
Contributor

I think this should be semver-major. If we are going to be able to change the output of a stable api in a minor/patch version, we should explicitly state "Do not rely on the output of this. It can and will change." in the documentation. Or at least something along those lines. Just because we state that the primary purpose of the api is for debugging doesn't mean it's used that way in practice.

@addaleax
Copy link
Member

@devsnek Could you put 6cc4b5e in a separate PR? We might need more discussion around that change on its own, and I would really like the original changes in here to get merged.

Also, I'm not sure what that last commit does? We can probably just drop it? We don't usually do merge commits, they break CI because that rebases automatically..

@devsnek
Copy link
Member Author

devsnek commented Nov 28, 2017

er its just a regular merge? i can squash my commits if you want, i had to merge it because there was a conflict

@devsnek devsnek force-pushed the feature/util-inspect-tostringtag branch from b48d0df to a107aaf Compare November 28, 2017 17:32
@TimothyGu
Copy link
Member

i had to merge it because there was a conflict

Please rebase instead.

uses @@toStringTag when creating the "tag" for an inspected value
@devsnek devsnek force-pushed the feature/util-inspect-tostringtag branch from a107aaf to 46eef42 Compare November 28, 2017 18:27
@addaleax addaleax added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Nov 30, 2017
@addaleax
Copy link
Member

@addaleax
Copy link
Member

addaleax commented Dec 1, 2017

Landed in 31e0dbc

@addaleax addaleax closed this Dec 1, 2017
@addaleax addaleax removed the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Dec 1, 2017
addaleax pushed a commit that referenced this pull request Dec 1, 2017
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>
@devsnek devsnek deleted the feature/util-inspect-tostringtag branch December 1, 2017 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-major PRs that contain breaking changes and should be released in the next major version. util Issues and PRs related to the built-in util module.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

WebAssembly support not easy to find
9 participants