Fix: Disable no-var autofixer in some incorrect cases in loops#7811
Merged
not-an-aardvark merged 1 commit intoeslint:masterfrom Dec 27, 2016
Merged
Conversation
|
@alangpierce, thanks for your PR! By analyzing the history of the files in this pull request, we identified @mysticatea, @kaicataldo and @vitorbal to be potential reviewers. |
|
LGTM |
not-an-aardvark
suggested changes
Dec 24, 2016
Member
not-an-aardvark
left a comment
There was a problem hiding this comment.
Thanks! This generally looks very good -- I just have one minor suggestion to add some tests for isInLoop.
| * | ||
| * @param {ASTNode} node - The node to check. | ||
| * @returns {boolean} `true` if the node is in a loop. | ||
| */ |
Member
There was a problem hiding this comment.
Since this has been moved to ast-utils.js, I think it might be good to add separate tests for it (in tests/lib/ast-utils.js).
Contributor
Author
There was a problem hiding this comment.
Ok, I just pushed an update that added some tests for this function.
The autofixer for the no-var rule was converting `var` to `let` within loops, but in some cases that can introduce incorrect behavior because `let` variables in loops only live for the lifetime of their loop iteration, while `var` variables within loops use the same variable across all iterations. We can still convert to `let` in typical cases as long as we check for cases that might cause a behavior difference: * If the variable is referenced from a closure, then that closure might be called after the current loop iteration ends. For `var` declarations, the closure refers to the shared variable across all iterations, and for `let` declarations, the closure refers to the variable just from that one iteration. * If the variable is used before its first assignment in the loop body, then for `var` declarations it will retain its value from the previous iteration, but for `let` declarations it will start as undefined. This change skips the autofixer for any variables referenced by any closure, and for any variables that are not initialized right when they are declared. Some additional static analysis can make both of these cases smarter, but this should handle most common cases.
427394a to
1b30fcb
Compare
|
LGTM |
gyandeeps
approved these changes
Dec 24, 2016
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the purpose of this pull request? (put an "X" next to item)
[ ] Documentation update
[X] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:
Tell us about your environment
What parser (default, Babel-ESLint, etc.) are you using?
default
Please show your full configuration:
What did you do? Please include the actual source code causing the issue.
I ran
eslint --fixon a file with this code:What did you expect to happen?
I expected the
varto remain, so that the output is:What actually happened? Please include the actual, raw output from ESLint.
No errors were reported, and the
varwas changed tolet, changing the behavior to have this output:What changes did you make? (Give an overview)
The autofixer for the no-var rule was converting
vartoletwithin loops, but in some cases that can introduce incorrect behavior becauseletvariables in loops only live for the lifetime of their loop iteration, whilevarvariables within loops use the same variable across all iterations.We can still convert to
letin typical cases as long as we check for cases that might cause a behavior difference:vardeclarations, the closure refers to the shared variable across all iterations, and forletdeclarations, the closure refers to the variable just from that one iteration, so changingvartoletcan change the behavior.vardeclarations it will retain its value from the previous iteration, but forletdeclarations it will start as undefined.This change skips the autofixer for any variables referenced by any closure, and for any variables that are not initialized right when they are declared. Some additional static analysis can make both of these cases smarter, but this should handle most common cases.
I also updated the docs to fix a typo and use the same phrasing as the prefer-const rule, since not all cases can be fixed.
Also, since
isInLoopwas already used in two places and I needed a third, I moved it into ast-utils, which required reworking things there a little.For a little more context, I recently implemented a similar fix for the same issue in the esnext project:
resugar/resugar#113
decaffeinate/decaffeinate#624
(The reason I have code that looks like that in the first place is that it's auto-generated from CoffeeScript using the decaffeinate project, so
vardeclarations get added in a way that's correct, but not always clean.)Is there anything you'd like reviewers to focus on?
I think the main thing is to sanity check that this change indeed fixes the correctness issues I mentioned, and that the autofixer's behavior is still reasonable after this change. Also, since this is my first eslint PR, there may be style issues or other things I've overlooked.
I also was a little unsure how to move
isInLoopintoast-utils. I ended up moving some other functions to be top-level helpers so I could call them, but let me know if I should have added it to themodule.exportsobject.Also, the
isLoopAssigneefunction checks forForInStatementandForOfStatement, and ideally it would also check forfor awaitstatements once those get standardized (currently they're stage 3). If there's a way to make sure that gets added later or a way to make the code more future-proof, would be nice.