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

Update: no-undefined handles properties/classes/modules (fixes #7964) #7966

Merged
merged 1 commit into from Feb 7, 2017

Conversation

platinumazure
Copy link
Member

@platinumazure platinumazure commented Jan 23, 2017

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?

@mention-bot
Copy link

@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.

@eslintbot
Copy link

LGTM

@platinumazure platinumazure added accepted There is consensus among the team that this change meets the criteria for inclusion bug ESLint is working incorrectly do not merge This pull request should not be merged yet rule Relates to ESLint's core rules labels Jan 23, 2017

// Only non-computed keys are valid here
return node === node.parent.key &&
!node.parent.computed;
Copy link
Member

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

{ code: "undefined = true", errors },
{ code: "var undefined = true", errors },
{ code: "({ bar: undefined })", errors },
{ code: "({ bar: undefined } = foo)", errors }
Copy link
Member

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

Copy link
Member Author

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.

return node === node.parent.key &&
!node.parent.computed;

default:
Copy link
Member

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

{ code: "undefined = true", errors },
{ code: "var undefined = true", errors },
{ code: "({ bar: undefined })", errors },
{ code: "({ bar: undefined } = foo)", errors }
Copy link
Sponsor Contributor

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?

Copy link
Member Author

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.

Copy link
Sponsor Contributor

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 :-)

@mysticatea
Copy link
Member

Could we simplify this by escope's references?

@platinumazure
Copy link
Member Author

platinumazure commented Jan 23, 2017 via email

@platinumazure
Copy link
Member Author

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.

cc @not-an-aardvark, @mysticatea

@eslintbot
Copy link

LGTM

1 similar comment
@eslintbot
Copy link

LGTM

@platinumazure
Copy link
Member Author

Okay, it's currently handling everything that's been requested, except for when undefined is used as a function identifier while being associated with a variable or object key of a different name.

For some reason, I thought var foo = function bar() {} would also put bar in the same scope as foo, but that doesn't seem to be the case in Chrome. Is bar in the inner function scope (same level as params or locals in bar)? Or is it not put in any scope at all and the name is pointless?

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 undefined that read from or write to the current scope; or we could say that it should forbid all references to undefined except object or method keys, and anything else (even if it doesn't try to read a variable or create a new variable) is forbidden.

Any thoughts?

@ljharb
Copy link
Sponsor Contributor

ljharb commented Jan 28, 2017

@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 foo could be reassigned, bar will always safely point to the function itself.

@platinumazure
Copy link
Member Author

@ljharb Thanks, that makes sense. In that case, I think the spirit of the rule says that a case like foo = function undefined() {} must be invalid, since it's not "merely" an object key or other string, do you agree?

@not-an-aardvark
Copy link
Member

I would say that the rule should ban anything that creates a variable called undefined in any scope. So for example, this should be disallowed:

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 undefined would be in-scope in the function body.)

@eslintbot
Copy link

LGTM

@platinumazure
Copy link
Member Author

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.)

@not-an-aardvark
Copy link
Member

The tests look good. The one thing I might add is

({ [undefined]: foo }); // invalid

@platinumazure
Copy link
Member Author

platinumazure commented Jan 28, 2017 via email

@eslintbot
Copy link

LGTM

@platinumazure
Copy link
Member Author

@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.

@eslintbot
Copy link

LGTM

@mysticatea
Copy link
Member

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

@ljharb
Copy link
Sponsor Contributor

ljharb commented Jan 30, 2017

Also any object destructuring/rest pattern that creates an undefined binding

@platinumazure platinumazure removed the do not merge This pull request should not be merged yet label Jan 30, 2017
@platinumazure
Copy link
Member Author

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.)

@platinumazure
Copy link
Member Author

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.

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

@platinumazure
Copy link
Member Author

@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.

@eslintbot
Copy link

LGTM

@platinumazure
Copy link
Member Author

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.

@mysticatea
Copy link
Member

@mysticatea I didn't implement the following test case as it seems to be experimental syntax:

Oops, I'm sorry, it's my copy-paste mistake.

Copy link
Member

@mysticatea mysticatea left a 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.

*/
function checkScope(scope) {
const undefinedVar = scope.variables
? scope.variables.filter(variable => variable.name === "undefined")[0]
Copy link
Member

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));
Copy link
Member

@mysticatea mysticatea Feb 3, 2017

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 Identifiers 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.

Copy link
Member Author

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?)

Copy link
Member

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 😉.

Copy link
Member Author

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.

*/
globalScope.through
.filter(isUndefinedReference)
.forEach(ref => report(ref.identifier));
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 6,
sourceType: "module"
Copy link
Member

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.

@eslintbot
Copy link

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);
Copy link
Member

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?

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member

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.

@platinumazure
Copy link
Member Author

platinumazure commented Feb 4, 2017 via email

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

@platinumazure platinumazure changed the title Fix: no-undefined warned on non-computed property keys (fixes #7964) Update: no-undefined handles properties/classes/modules (fixes #7964) Feb 7, 2017
@platinumazure
Copy link
Member Author

platinumazure commented Feb 7, 2017

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!

Copy link
Member

@mysticatea mysticatea left a 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:.

@alberto alberto merged commit e228d56 into master Feb 7, 2017
@platinumazure platinumazure deleted the no-undefined-false-positives branch February 8, 2017 00:38
@eslint-deprecated eslint-deprecated bot locked and limited conversation to collaborators Feb 6, 2018
@eslint-deprecated eslint-deprecated bot added the archived due to age This issue has been archived; please open a new issue for any further discussion label Feb 6, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepted There is consensus among the team that this change meets the criteria for inclusion archived due to age This issue has been archived; please open a new issue for any further discussion bug ESLint is working incorrectly rule Relates to ESLint's core rules
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants