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
Update: no-unused-vars to account for rest property omissions #7968
Update: no-unused-vars to account for rest property omissions #7968
Conversation
@zackargyle, thanks for your PR! By analyzing the history of the files in this pull request, we identified @mysticatea, @not-an-aardvark and @vitorbal to be potential reviewers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the pull request.
This seems like a reasonable use-case for having unused variables. Personally, though, I think it would be better to put it behind an option rather than changing the default behavior of the rule.
Previous discussion on this issue can be found here: #6563
@@ -333,6 +345,21 @@ ruleTester.run("no-unused-vars", rule, { | |||
] | |||
}, | |||
|
|||
// Using object rest for variable omission | |||
{ | |||
code: "const data = { type: 'coords', x: 1, y: 2 };\nconst { type } = data;", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by this testcase -- the comment indicates that it's using object rest, but it doesn't appear to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I wasn't sure how necessary this was, but thought it would be nice to have a destructuring case without the rest sibling just for sanity. Need to change the comment for sure tho. Copy pasta.
.some(def => ( | ||
def.node.id && | ||
def.node.id.type === "ObjectPattern" && | ||
def.node.id.properties.some(property => property.type === "ExperimentalRestProperty") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also include a check for RestProperty
in addition to ExperimentalRestProperty
? I think parsers like babel-eslint output RestProperty
, and we will too after it reaches stage 4.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can do.
.some(def => ( | ||
def.node.id && | ||
def.node.id.type === "ObjectPattern" && | ||
def.node.id.properties.some(property => property.type === "ExperimentalRestProperty") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's only necessary to check the last property of the declaration here rather than iterating through all the properties, because rest properties are always the last property of a declaration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point.
Thanks for the pull request, @zackargyle! I took a look to make sure it's ready for merging and found some changes are needed:
Can you please update the pull request to address these? (More information can be found in our pull request guide.) |
f3a4d48
to
ccdd96a
Compare
Thanks for the pull request, @zackargyle! I took a look to make sure it's ready for merging and found some changes are needed:
Can you please update the pull request to address these? (More information can be found in our pull request guide.) |
I think eslintbot is complaining because it's checking the message for the first commit of this PR (since that's the message that gets used when the PR is merged). |
ccdd96a
to
09fce25
Compare
LGTM |
@not-an-aardvark all fixed up :) Was missing the colon 😄👍🏻 |
Agree. I don't think this should be the default behavior, but I think an option is fine. |
lastProperty.node.id.properties.some(property => ( | ||
property.type === "ExperimentalRestProperty" || | ||
property.type === "RestProperty" | ||
)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you might have removed the wrong iteration here -- it's still necessary to keep the filter
statement from before (if a variable has multiple declarations, the rule should check all of them), but the node.id.properties.some()
iteration is unnecessary because a rest property will always be the last property of a given node.
I think it should look roughly like this:
function hasRestSpreadSibling(variable) {
return variable.defs
.filter(def => def.name.type === "Identifier")
.some(def => (
def.node.id &&
def.node.id.type === "ObjectPattern" &&
def.node.id.properties.length &&
new Set(["ExperimentalRestProperty", "RestProperty"]).has(def.node.id.properties[def.node.id.properties.length - 1].type)
));
}
Looking at the status of the proposal:
|
@not-an-aardvark I edited the comment. I'm 👍 only if the enhancement is behind an option, 👎 for default behavior change. Thanks for following up. |
Thanks for clarifying. So we now have a champion and 3 👍s for putting the behavior behind an option, which means we can accept the issue if @mysticatea's 👎 was just for changing the default behavior. (I'll wait for him to clarify, though.) |
09fce25
to
5aa0048
Compare
LGTM |
5aa0048
to
c4efcec
Compare
LGTM |
Fixed it up and put it behind a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mostly looks good to me, I just have some recommendations for additional tests.
@@ -195,6 +195,16 @@ Examples of **correct** code for the `{ "args": "none" }` option: | |||
})(); | |||
``` | |||
|
|||
### ignoreRestSiblings | |||
|
|||
The `ignoreRestSiblings` option is a boolean (default: `false`). With modern JavaScript it is possible to "omit" properties from an object by using a rest parameter, but by default the sibling properties are marked as "unused". With this option enabled the omitted properties are considered "used", as their existence has a side-effect. In the example, `type` is considered "used". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few minor nitpicks:
-
I feel like "modern JavaScript" is kind of a vague term, especially since rest/spread properties aren't part of the official spec yet. Maybe it would be better to link to the rest/spread property spec instead.
-
I think this is technically referring to rest properties, not rest parameters.
-
It might be clearer to say that the unused variables are ignored by the rule, rather than saying that the variables are considered "used" because their existence has a side-effect. For example, this variable's existence also has a side-effect, but it's not considered "used":
var foo = doSomethingWithSideEffects();
|
||
The `ignoreRestSiblings` option is a boolean (default: `false`). With modern JavaScript it is possible to "omit" properties from an object by using a rest parameter, but by default the sibling properties are marked as "unused". With this option enabled the omitted properties are considered "used", as their existence has a side-effect. In the example, `type` is considered "used". | ||
|
||
```js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make sure the code box ends up colored green on the site to indicate "correct" code, you should put something like this before the code box:
Examples of **correct** code for the `{ "ignoreRestSiblings": true }` option:
```js
// ...example code
```
* @private | ||
*/ | ||
function hasRestSpreadSibling(variable) { | ||
if (config.ignoreRestSiblings === true) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: This can be replaced with if (config.ignoreRestSiblings) {
(explicitly comparing to true
isn't necessary).
experimentalObjectRestSpread: true | ||
} | ||
} | ||
}, |
There was a problem hiding this comment.
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 that has code like this, but without the ignoreRestSiblings
option enabled? It would be useful to verify that an error is reported in that case.
errors: [ | ||
{ line: 2, column: 9, message: "'type' is assigned a value but never used." } | ||
] | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a couple tests where the unused variable is the one with the rest property?
var { ...unused } = bar;
var { foo, ...unused } = baz;
doSomethingWith(foo);
Both of these should result in an error getting reported.
errors: [ | ||
{ line: 2, column: 9, message: "'type' is assigned a value but never used." } | ||
] | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a couple tests where an unused variable is in the same declarator as a rest property, but it's not a sibling?
var { foo: [unused], ...bar } = baz;
doSomethingWith(bar);
var [unused, { ...bar }] = baz;
doSomethingWith(bar);
Both of these should result in an error getting reported.
c4efcec
to
55961cb
Compare
LGTM |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one small correction, otherwise this LGTM.
def.node.id.type === "ObjectPattern" && | ||
def.node.id.properties.length && | ||
restProperties.has(def.node.id.properties[def.node.id.properties.length - 1].type) && // last property is a rest property | ||
def.node.id.properties.some(prop => prop.type === "Property" && prop.key.name === variable.name) // variable is sibling of the rest property |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think comparing prop.key.name
is correct, because the property names and the variable names are independent from each other:
// This will get reported, because prop.key.name is "foo" and variable.name is "_"
var { foo: _, ...bar } = baz;
doSomethingWith(bar);
I think the easiest way to fix this would be to replace the "variable is sibling of the rest property" line with something using def.name
property, which is a reference to the Identifier
node for the given variable. That way, we also wouldn't have to iterate over all the properties:
// verify that this definition is not part of a RestProperty
!restProperties.has(def.name.parent.type)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to change it, but I actually had purposefully wanted that example to be an error. My thinking was that you shouldn't rename a variable that's only used for omission. It would be slightly faster though to change it to check the parent type. Easy change, let me know :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see, interesting point. I still think it would be better to check the parent type though -- renaming the variable might be necessary if a different variable is already defined with the given name. For example, in the code below, the variable has to be renamed to avoid shadowing the existing window
variable:
var { window: _, ...otherProps } = unrelatedObj;
// later...
window.addEventListener(/* ... */);
Also, in some cases the property name is computed, so a name
check won't work:
function omit(obj, key) {
const { [key]: _, ...remainingProps } = obj;
return remainingProps;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Too many awkward possibilities. Just switched to use the parent type.
No. I will not oppose this proposal strongly if it's an option. However, "Rest/Spread Properties" proposal is still on stage 3, so I wonder if this is proper in core for right now (https://github.com/eslint/eslint/#what-about-experimental-features). Because we have |
Update docs Update: use RestProperty and only check last property Temp temp temp Use parent type
55961cb
to
31a1617
Compare
LGTM |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks!
@mysticatea I suppose another option would be to mark the option as experimental, e.g. something along the lines of |
Anything else y'all need from me? I'd love to get this merged so we can remove a bunch of disabled linter statements here at Pinterest! 😁 |
@not-an-aardvark I'd rather not have an option name with "experimental" because it means we'd have to change it (and do a major release) sometime after the proposal hits stage 4. As far as I'm concerned, even though our parser option labels it "experimental", we're basically supporting it, so we should use a normal option name. Just my 2 cents. |
My thinking was that we would eventually introduce a new non-experimental flag when the feature reaches stage 4 (e.g. That said, I'm also fine with keeping a normal option name. |
All good with option naming? |
Fine by me. I'll leave this open for one more day in case anyone else ends up reviewing it, but unless something unexpected comes up this will get merged soon and make it into the next release. 👍 |
Thanks for contributing! |
What is the purpose of this pull request? (put an "X" next to item)
[X] Changes an existing rule ([template]
What rule do you want to change?
no-unused-vars
Does this change cause the rule to produce more or fewer warnings?
fewer
How will the change be implemented? (New option, new default behavior, etc.)?
new default behavior
Please provide some example code that this change will affect:
What does the rule currently do for this code?
Errors that
type
is unused.What will the rule do after it's changed?
type
will be considered to be passively used. Meaning that although it is not used afterwards, it's existence effects the declaration ofcoordinates
. Much like_.omit(data, ['type']);
What changes did you make? (Give an overview)
The change is pretty simple. If the variable is unused, but has a sibling rest parameter, we now consider it to be used. The logic is found in the
hasRestSpreadSibling
function.Is there anything you'd like reviewers to focus on?
Although the rest property is still considered experimental, it is stage-3 and widely adopted in the community. The ability to use it as a means of property omission is also becoming increasingly popular.