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

Fixes #7863; Add fixer for no-else-return #7864

Merged
merged 1 commit into from Jan 20, 2017

Conversation

xdumaine
Copy link
Contributor

@xdumaine xdumaine commented Jan 5, 2017

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?

@mention-bot
Copy link

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

@jsf-clabot
Copy link

jsf-clabot commented Jan 5, 2017

CLA assistant check
All committers have signed the CLA.

@eslintbot
Copy link

Thanks for the pull request, @xdumaine! I took a look to make sure it's ready for merging and found some changes are needed:

  • The commit summary needs to begin with a tag (such as Fix: or Update:). Please check out our guide for how to properly format your commit summary and update it on this pull request.

Can you please update the pull request to address these?

(More information can be found in our pull request guide.)

Copy link
Member

@not-an-aardvark not-an-aardvark left a 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);
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

@xdumaine xdumaine Jan 6, 2017

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:

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

Copy link
Member

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 any Punctuator 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 the else 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).

Copy link
Contributor Author

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!

@ilyavolodin ilyavolodin added enhancement This change enhances an existing feature of ESLint evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion rule Relates to ESLint's core rules labels Jan 6, 2017
@eslintbot
Copy link

Thanks for the pull request, @xdumaine! I took a look to make sure it's ready for merging and found some changes are needed:

  • The commit summary needs to begin with a tag (such as Fix: or Update:). Please check out our guide for how to properly format your commit summary and update it on this pull request.

Can you please update the pull request to address these?

(More information can be found in our pull request guide.)

@eslintbot
Copy link

Thanks for the pull request, @xdumaine! I took a look to make sure it's ready for merging and found some changes are needed:

  • The commit summary needs to begin with a tag (such as Fix: or Update:). Please check out our guide for how to properly format your commit summary and update it on this pull request.

Can you please update the pull request to address these?

(More information can be found in our pull request guide.)

@eslintbot
Copy link

LGTM

Copy link
Member

@not-an-aardvark not-an-aardvark left a 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;
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 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) ||
Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Member

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);
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 clearer to use sourceCode.getFirstToken(node) here instead.

if (startToken.type === "Punctuator" && startToken.value === "{") {
const firstTokenOfElseBlock = sourceCode.getTokenAfter(startToken);

if (lastIfToken.value !== "}" &&
Copy link
Member

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"

Copy link
Contributor Author

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?

Copy link
Member

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, pass node instead of alternate (and then access the property accordingly within the displayReport function to make sure everything else still works). That way, node will refer to the IfStatement, so you could check node.consequent.type.
  • Change this listener to "IfStatement:exit" instead of IfStatement.

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

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" }]
Copy link
Member

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" }]
}
Copy link
Member

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.

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

Copy link
Member

@not-an-aardvark not-an-aardvark left a 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);
Copy link
Member

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.

Copy link
Contributor Author

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 !== "}")) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added.

@eslintbot
Copy link

LGTM

@not-an-aardvark not-an-aardvark added accepted There is consensus among the team that this change meets the criteria for inclusion and removed evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion labels Jan 20, 2017
@not-an-aardvark
Copy link
Member

Note to whoever ends up merging this: make sure to use the Update: commit prefix since it's a semver-minor change.

@gyandeeps gyandeeps merged commit 243e47d into eslint:master Jan 20, 2017
@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 enhancement This change enhances an existing feature of ESLint rule Relates to ESLint's core rules
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants