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
Fixes #7863; Add fixer for no-else-return #7864
Conversation
@xdumaine, thanks for your PR! By analyzing the history of the files in this pull request, we identified @not-an-aardvark, @elwayman02 and @jrvidal to be potential reviewers. |
Thanks for the pull request, @xdumaine! 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.) |
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. I think adding autofix support for this rule is a good idea, but there are a few places where this implementation will accidentally change the behavior of the code.
let fixedSource; | ||
|
||
if (startToken.type === "Punctuator") { | ||
fixedSource = source.substr(1, source.length - 2); |
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 will cause an error if the else
statement doesn't have a block:
if (foo) bar()
else return baz
// gets fixed to
if (foo) bar()
return ba
if (foo) bar()
else return
// gets fixed to
if (foo) bar()
retur
Also, it will cause an error if there are more statements on the same line as the else
block.
if (foo) { bar() }
else { return baz } qux()
// gets fixed to:
if (foo) { bar() }
return baz qux()
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.
@not-an-aardvark I'll check out your third example, but are you sure the first two are true? The tests have these examples in them, and pass, without making those corrections. They're handled by the check on L48 that the start token is a punctuator.
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.
By the way, thanks for the quick responses and feedback. I'll give this another go and update. :)
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.
Ok, took a longer look at these:
- Your first example:
if (foo) bar()
else return baz
Is not an error according to the rule, so this one won't get fixed at all. You can't remove the else in this case, as it changes the meaning of the code.
Your second example is the same. It's not an error according to the rule because the else is not extra, it's necessary to the meaning of the code.
However, if you change both of these examples to replace bar()
with return bar()
that would make them error states. In doing that, your assertion about how it gets fixed is incorrect. The else case in my code (L51) handles it, and it fixes the code correctly.
Your third example is also not applicable unless you change bar()
to return bar()
. If that's the case you end up with this:
if (foo) { return bar() }
else { return baz } qux()
Where qux()
is unreachable code. So this would cause an unexpected token
error if 1. you have unreachable code and 2. don't use semicolons.
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.
Looks like you're right about these, my mistake. I missed the Punctuator
check.
However, I noticed a few other errors (and double-checked them with the fixer this time):
- At the moment the
Punctuator
check is too loose -- it will treat anyPunctuator
node as if it was a block, even if it's not an opening curly. As a result, the following code causes a syntax error when fixed:
function foo() {
if (foo) return bar;
else (foo).bar();
}
// gets fixed to:
function foo() {
if (foo) return bar;
foo).bar()
}
To fix this, I would recommend modifying the check to
if (startToken.type === "Punctuator" && startToken.value === "{")
- Also, this will cause ASI issues if the statement in the
if
statement does not end in a semicolon. For example, the following code will be fixed incorrectly:
function foo() {
if (foo) return bar
else { [1, 2, 3].map(foo) }
}
// gets fixed to
function foo() {
if (foo) return bar
[1, 2, 3].map(foo) // this is parsed as a computed property access rather than an array
}
To avoid this problem, I would recommend that the fixer avoid creating a fix when all of the following are true:
- The
if
statement does not have a block - The body of the
if
statement does not end in a semicolon - The first character of the
else
statement is an unsafe character for starting a line ((
,[
,/
,`
,+
, or-
)
A similar check can be found in the no-lonely-if
fixer.
- In the third example from the previous comment, I think we should still use a workaround to avoid causing a syntax error, even if it would only occur when the user has very strange code.
function foo() {
if (foo) return bar
else { baz } qux
}
// gets fixed to
function foo() {
if (foo) return bar
baz qux
}
Similarly to the example above, this will also cause an ASI issue if the statement after the else
starts with a character like [
:
function foo() {
if (foo) return bar
else { baz() }
[1, 2, 3].map(foo)
}
// gets fixed to
function foo() {
if (foo) return bar
baz()
[1, 2, 3].map(foo) // this gets parsed as a computed property access rather than an array
}
To avoid this problem, I would recommend that the fixer avoid creating a fix when all of the following are true:
- The last statement in the
else
block does not end in a semicolon - There is a statement after the
else
block, and it either (a) starts with an unsafe character ((
,[
,/
,`
,+
, or-
), or (b) is on the same line as the last statement in theelse
block.
As an aside, hopefully it doesn't seem like I'm coming up with exceedingly unlikely code examples that no one would actually use. In general, we try to uphold the guarantee that an autofixer should never change the behavior of working code under any circumstances, so sometimes there are unfortunately a lot of edge cases for fixers like this (particularly when a fix involves removing a block statement).
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 definitely appreciate the feedback. This type of semantic parsing type of code, and code modification is new to me, and I'm not familiar with all the ins and outs so I'm grateful for your detailed feedback. I'll give it another go later and update. Thanks again for your time!
Thanks for the pull request, @xdumaine! 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.) |
Thanks for the pull request, @xdumaine! 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.) |
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.
This mostly looks good, I just have a few more suggestions.
if (lastIfToken.value !== "}" && | ||
lastIfToken.value !== ";" && | ||
/^[([/+`-]/.test(firstTokenOfElseBlock.value)) { | ||
return null; |
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 helpful to future contributors to add a comment describing the case that this logic is trying to catch.
if (lastTokenOfElseBlock.value !== ";") { | ||
const nextToken = sourceCode.getTokenAfter(endToken); | ||
|
||
if (/^[([/+`-]/.test(nextToken.value) || |
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 will throw an error if nextToken
doesn't exist.
{
code: "if (foo) return bar; else { baz }",
parserOptions: { ecmaFeatures: { globalReturn: 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.
Easy enough to add a check, but Is that actually possible? Since return
must always be inside a function, so there will always be at least one more token: }
closing the function
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.
@xdumaine The globalReturn
option on espree begs to differ.
message: "Unnecessary 'else' after 'return'.", | ||
fix: fixer => { | ||
const sourceCode = context.getSourceCode(); | ||
const startToken = sourceCode.getTokenByRangeStart(node.start); |
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 clearer to use sourceCode.getFirstToken(node)
here instead.
if (startToken.type === "Punctuator" && startToken.value === "{") { | ||
const firstTokenOfElseBlock = sourceCode.getTokenAfter(startToken); | ||
|
||
if (lastIfToken.value !== "}" && |
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 check will work incorrectly here:
if (foo) return function() {} // note: the last token is '}'
else [1, 2, 3].map(bar)
// gets fixed to
if (foo) return function() {}
[1, 2, 3].map(bar) // this is parsed as a property access instead of an array
To fix this, I would replace the lastIfToken.value !== "}"
check with
node.parent.consequent.type !== "BlockStatement"
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.
Hmm makes sense, but node.parent
is not defined here - could you recommend a better way to do that check?
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.
Good point -- two potential workarounds I can think of:
- In this call to
displayReport
, passnode
instead ofalternate
(and then access the property accordingly within thedisplayReport
function to make sure everything else still works). That way,node
will refer to theIfStatement
, so you could checknode.consequent.type
. - Change this listener to
"IfStatement:exit"
instead ofIfStatement
.
Explanation: The parent
property is added to each node as ESLint enters it while traversing. Since this listener is fired when the IfStatement
node is entered, the parent
property hasn't been added to the else
node yet. If the listener is fired when the IfStatement
node is exited instead, the else
node will have its parent property.
} | ||
} | ||
|
||
fixedSource = source.substr(1, source.length - 2); |
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: Can you replace this with source.slice(1, -1)
?
{ | ||
code: "function foo() { if (foo) return bar \nelse { baz() } \n[1, 2, 3].map(foo) }", | ||
output: "function foo() { if (foo) return bar \nelse { baz() } \n[1, 2, 3].map(foo) }", | ||
errors: [{ message: "Unnecessary 'else' after 'return'.", type: "BlockStatement" }] |
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 test cases like the two above, but with a semicolon?
"function foo() { if (foo) return bar; \nelse { [1, 2, 3].map(foo) } }"
"function foo() { if (foo) return bar \nelse { baz(); } \n[1, 2, 3].map(foo) }"
Both of these cases should result in a fix being applied.
}, | ||
{ | ||
code: "function foo() { if (foo) return bar \nelse { baz() } \nqaz() }", | ||
output: "function foo() { if (foo) return bar \n baz() \nqaz() }", | ||
errors: [{ message: "Unnecessary 'else' after 'return'.", type: "BlockStatement" }] | ||
} |
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 case where a semicolon is missing and the following statement is on the same line?
"function foo() { if (foo) return bar; else { baz } qux }"
This should cause the fix to get skipped.
LGTM |
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 for contributing!
I have two final nitpicks, but they're very minor, so they shouldn't block this from being merged.
const sourceCode = context.getSourceCode(); | ||
const alternateStartToken = sourceCode.getFirstToken(node.parent.alternate); | ||
const elseToken = sourceCode.getTokenBefore(alternateStartToken); | ||
const source = sourceCode.getText(node.parent.alternate); |
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 line (and the alternateStartToken
line) is a bit redundant because node.parent.alternate === node
.
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.
Good call. Confused myself when trying out returning the original node instead of the alternate. Fixed and simplified.
const nextTokenUnsafe = nextToken && /^[([/+`-]/.test(nextToken.value); | ||
const nextTokenOnSameLine = nextToken && nextToken.loc.start.line === lastTokenOfElseBlock.loc.start.line; | ||
|
||
if (nextTokenUnsafe || (nextTokenOnSameLine && nextToken.value !== "}")) { |
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 comment here would also be useful.
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.
Comment added.
LGTM |
Note to whoever ends up merging this: make sure to use the |
What is the purpose of this pull request? (put an "X" next to item)
[ ] Documentation update
[ ] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[x] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:
What changes did you make? (Give an overview)
Added a fixer for the
no-else-return
rule, updated documentation, and updated tests.Is there anything you'd like reviewers to focus on?