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-undefined handles properties/classes/modules (fixes #7964) #7966
Conversation
@platinumazure, thanks for your PR! By analyzing the history of the files in this pull request, we identified @michaelficarra, @vitorbal and @kaicataldo to be potential reviewers. |
LGTM |
lib/rules/no-undefined.js
Outdated
|
||
// Only non-computed keys are valid here | ||
return node === node.parent.key && | ||
!node.parent.computed; |
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 this needs to include a && !node.parent.shorthand
check, otherwise the rule will have a false negative with
({ undefined });
// this should get reported, because it's equivalent to
({ undefined: undefined });
tests/lib/rules/no-undefined.js
Outdated
{ code: "undefined = true", errors }, | ||
{ code: "var undefined = true", errors }, | ||
{ code: "({ bar: undefined })", errors }, | ||
{ code: "({ bar: undefined } = foo)", errors } |
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 might be useful to have tests for
({ undefined }); // invalid
var { undefined } = foo; // invalid
({ undefined() {} }); // valid
({ undefined: function undefined() {} }); // invalid (EDITED)
var foo = function undefined() {}; // invalid (EDITED)
function undefined() {} // invalid, I think? It does create a variable
class Foo { undefined() {} } // valid
class Foo { [undefined]() {} } // invalid
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.
Agreed. I will see how many of those pass before this PR, as well.
lib/rules/no-undefined.js
Outdated
return node === node.parent.key && | ||
!node.parent.computed; | ||
|
||
default: |
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.
There should also be a MethodDefinition
case here, to prevent reporting an error for
class Foo { undefined() {} } // valid
tests/lib/rules/no-undefined.js
Outdated
{ code: "undefined = true", errors }, | ||
{ code: "var undefined = true", errors }, | ||
{ code: "({ bar: undefined })", errors }, | ||
{ code: "({ bar: undefined } = foo)", errors } |
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.
what about both of these examples with var
?
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.
Seemed unimportant, but couldn't hurt.
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.
One creates a new variable, the other assigns to an existing one (possibly a global one) - they're different :-)
Could we simplify this by escope's references? |
I can look into that possibility, sure.
…On Jan 22, 2017 10:46 PM, "Toru Nagashima" ***@***.***> wrote:
Could we simplify this by escope's references?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#7966 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AARWerzVbD7BZM9Gg6LIgx8vKzqu_A5Jks5rVDCKgaJpZM4LqpRl>
.
|
What I think I'm going to do is: Add a commit with missing test cases and logic; and then try to refactor to use escope references, while making sure the existing and new test cases pass. (Before all that, I'll rebase, of course.) It would be awesome to just handle this via escope references, since presumably the string keys are not references. I just want to make sure all the test cases are covered first. |
04bae5f
to
2794cf6
Compare
LGTM |
1 similar comment
LGTM |
Okay, it's currently handling everything that's been requested, except for when For some reason, I thought I'm also wondering how we really want the rule to behave as a whole with regard to extra identifiers. We could say that it should forbid all references to Any thoughts? |
@platinumazure the lexical binding created by a function's syntactical name is only available inside the function - the point is that it's useful for recursion, and since |
@ljharb Thanks, that makes sense. In that case, I think the spirit of the rule says that a case like |
I would say that the rule should ban anything that creates a variable called var foo = function undefined() {
// undefined is a function here
};
({
foo: function undefined() {
// undefined is a function here
}
}) (This contradicts what I said here because I'd forgotten that |
LGTM |
Awesome. I've just pushed a commit that should address those points. @not-an-aardvark Can you please review for whether the test cases are sufficient? (I'm not liking the way the code is currently laid out, so I will refactor it, possibly to try using escope references as @mysticatea suggested. But as I mentioned before, I wanted to get the test cases correct first.) |
The tests look good. The one thing I might add is ({ [undefined]: foo }); // invalid |
Good call, I'll add that. Thanks!
…On Jan 28, 2017 1:20 PM, "Teddy Katz" ***@***.***> wrote:
The tests look good. The one thing I might add is
({ [undefined]: foo }); // invalid
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#7966 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AARWeiPBS6-D2DisbSvXIFzaphlwZMW6ks5rW5UQgaJpZM4LqpRl>
.
|
LGTM |
@not-an-aardvark Good catch, that was not behaving correctly. Hopefully all is well now with regard to test cases. I'll refactor the code later this weekend. |
LGTM |
I have some ideas: // valid
import undefined as a from "foo"
import {undefined as a} from "foo"
export {undefined} from "foo"
export {undefined as a} from "foo"
export {a as undefined} from "foo"
// invalid
import undefined from "foo"
import * as undefined from "foo"
import {undefined} from "foo"
import {a as undefined} from "foo"
export {undefined}
let a = [b, ...undefined] // SpreadElement
;[a, ...undefined] = b // RestElement
;[a = undefined] = b // AssignmentPattern |
Also any object destructuring/rest pattern that creates an |
Removing "do not merge" since the dependent PR has been merged and this one has been rebased. (Still isn't ready to go, of course.) |
I also want to note that the original issue focused specifically on simple object keys. On some level, I'm wondering if it's worth fixing that and opening a separate issue for ensuring import/export and other syntax is well supported. In the meanwhile, I'm halfway down the rabbit hole for escope. I got many test cases to work by warning on variable references, but it's not sufficient for cases where there is merely a declaration at play, so I'm playing around some more trying to figure that out. |
LGTM |
2c71a78
to
0595124
Compare
LGTM |
@not-an-aardvark @mysticatea Just pushed some commits to switch to escope and to add the missing test cases. Would you mind giving this another review, please? @mysticatea I didn't implement the following test case as it seems to be experimental syntax: // valid
import undefined as a from "foo" Otherwise I think I've covered everything you suggested. |
0595124
to
a792889
Compare
LGTM |
Oh-- thinking this might need to be Update since some of the new import/export tests might not have been covered by the old code. I'll try to verify that in the next few days, but if anyone already knows the answer and wants to save some time, I would welcome that. |
Oops, I'm sorry, it's my copy-paste mistake. |
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.
Thank you, almost LGTM.
There are some suggestions.
lib/rules/no-undefined.js
Outdated
*/ | ||
function checkScope(scope) { | ||
const undefinedVar = scope.variables | ||
? scope.variables.filter(variable => variable.name === "undefined")[0] |
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.
scope
has ES6 Map
object which is mapping variable names to each variable. It would be more efficient.
const undefinedVar = scope.set.get("undefined")
if (!undefinedVar) {
return
}
(I'm not sure why the name of the Map
is set
.)
.filter(ref => !ref.init) | ||
.forEach(ref => report(ref.identifier)); | ||
|
||
defs.forEach(def => report(def.name)); |
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 felt that making the array of Identifier
s is nice. Only a bit. Feel free ignore this comment.
// All Identifiers of declarations and references.
const identifiers = new Set([
...undefinedVar.defs.map(def => def.name),
...undefinedVar.references.map(ref => ref.identifier)
])
identifiers.forEach(report) // ← or for-of loop.
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 hoping to avoid using sets of ASTNodes because that depends on escope maintaining referential equality between their definition names and their reference identifiers in this case. (Unless I'm wrong about how Sets work and they actually do deep compares of their members?)
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.
We can expect escope to not clone AST nodes.
We have already depended on it; for example, some rules are using reference.Identifier.parent
property ESLint added after scope analysis 😉.
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.
At this point, I'm not implementing this change yet since what I have seems pretty good. @mysticatea If you really want to see this in, please leave a review requesting changes and I'll do it.
lib/rules/no-undefined.js
Outdated
*/ | ||
globalScope.through | ||
.filter(isUndefinedReference) | ||
.forEach(ref => report(ref.identifier)); |
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 guess this fallback is unnecessary since the undefined
variable exists always.
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'll check into this.
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.
@mysticatea You're right. It seems that scope.through
is for when you want to catch variables that were assigned in a lower scope but which have percolated into the current scope without having been originally defined there. Since I'm iterating over all scopes anyway, the variable will be iterated over once I get to its immediate containing scope. I've just removed this logic and added a test case that was missing, and all is well.
tests/lib/rules/no-undefined.js
Outdated
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
ecmaVersion: 6, | ||
sourceType: "module" |
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.
sourceType: "module"
creates extra scope for modules, so I think it should not apply to all test cases.
LGTM |
if (!parent || parent.type !== "MemberExpression" || node !== parent.property || parent.computed) { | ||
context.report({ node, message: "Unexpected use of undefined." }); | ||
} | ||
stack.push.apply(stack, scope.childScopes); |
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 slightly concerned about performance here, since this requires another traversal of the scope tree. Would it be better to check the scopes during the initial AST traversal?
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.
Actually, there are several rules which are traversing variables by the same way. This is a tail call optimized recursive calls.
Probably we can make events to traverse every variable in another PR; e.g. onVariable.
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 ran this command on my branch and on its merge-base:
TIMING=1 node ./bin/eslint.js --no-eslintrc --parser-options "ecmaVersion: 6" --rule "no-undefined: error" lib tests
On master, the rule ran for 280-330ms. On my branch, the rule ran for 44-48ms. I'm open to suggestions on how I can improve my testing in case I'm doing something wrong there, but I assume that the switch from "Identifier" visitor to "Program:exit" helps quite a bit.
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 confirmed it. For our codebase, this implementation is x5 faster than original implementation.
https://gist.github.com/mysticatea/65b6e7a3298b6dc255389e79725f9c70
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 guess, it's because there are lots of unrelated Identifier
nodes. Relatively the number of scopes is not much.
The main/only reason I did it this way was so I wouldn't have to bother
with whether a BlockStatement counts as a new scope or not (yes in ES2015+,
no in ES5). If we had events for entering and leaving scope without having
to worry about node type and ecmaVersion, I would be happy to refactor in
that direction.
@mysticatea Can code path analysis be used for this purpose?
…On Feb 3, 2017 9:41 PM, "Teddy Katz" ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In lib/rules/no-undefined.js
<#7966 (review)>:
>
- if (!parent || parent.type !== "MemberExpression" || node !== parent.property || parent.computed) {
- context.report({ node, message: "Unexpected use of undefined." });
- }
+ stack.push.apply(stack, scope.childScopes);
I'm slightly concerned about performance here, since this requires another
traversal of the scope tree. Would it be better to check the scopes during
the initial AST traversal?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#7966 (review)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AARWesjr34obdKKqgx3-P5dekLRW2rGgks5rY_NPgaJpZM4LqpRl>
.
|
e782156
to
ab7310c
Compare
LGTM |
ab7310c
to
ec86b3f
Compare
LGTM |
ec86b3f
to
58512ac
Compare
LGTM |
Ran the new tests against the old code. A few cases where 2 errors would have been reported at the same location (which I've since fixed down to one), and other cases where they were valid but saw reports. I never saw an increase in reports. So I could have left this at "Fix", but with all the refactoring (and performance improvements), it feels right to make this an "Update". @mysticatea I think this might be ready for review once more-- if you have time. Thanks! |
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, thank you!
I'm OK if it's Fix:
.
What is the purpose of this pull request? (put an "X" next to item)
[x] Bug fix
See #7964.
What changes did you make? (Give an overview)
Fixed no-undefined to ensure that property keys (non-computed) were not incorrectly flagged. Although property keys are Identifier nodes, non-computed property keys are really strings at the end of the day.
Well, that's where we started, but then we decided to ensure that destructuring and import/export were handled correctly. In order to do that more reliably and (hopefully) ensure the rule keeps up with no syntax, I decided to refactor to use escope variables. This ended up improving performance by about 80% (at least when the rule is run on our codebase), so that's a huge win. (Identifer visitor was hit a lot, so even the performance penalty of iterating over scopes again still made this rule much faster overall.)
Labeling as "Update" in case more warnings are introduced in destructuring or import/export cases.
Is there anything you'd like reviewers to focus on?
Does this need a documentation update?