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

Fix: prevent fixing to incorrect code (fixes #11069) #11076

Closed
wants to merge 11 commits into from

Conversation

captain-yossarian
Copy link

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:

#11069

What changes did you make? (Give an overview)
I've add the function which checks if ELSE block includes block scope local variable declaration [let or const]

Is there anything you'd like reviewers to focus on?

@jsf-clabot
Copy link

jsf-clabot commented Nov 11, 2018

CLA assistant check
All committers have signed the CLA.

@eslint-deprecated eslint-deprecated bot added the triage An ESLint team member will look at this issue soon label Nov 11, 2018
@captain-yossarian
Copy link
Author

I had wrong email on my git account, that's why CI couldn't not to approve. How to properly remove all of this previous commits? Thank you

@platinumazure platinumazure added bug ESLint is working incorrectly rule Relates to ESLint's core rules and removed triage An ESLint team member will look at this issue soon labels Nov 11, 2018
Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

Hi @SerhiiBilyk, thanks for contributing!

As the PR template notes, we require unit tests for all changes (where unit tests are applicable). Could you please add some tests?

Rule unit test resources:

Copy link
Member

@platinumazure platinumazure 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 adding tests!

I think the important test case in the issue is when there is a shadowing of an existing variable in the else clause. For example:

function foo() {
    let bar = true;
    if (baz) {
        return;
    } else {
        let bar = false;    // Shadows "bar" from top of function
        return;
    ]

Could you please add this test case? Then I can review the general approach 😄

@captain-yossarian
Copy link
Author

@platinumazure done

@captain-yossarian
Copy link
Author

captain-yossarian commented Nov 13, 2018

@ajafff @platinumazure what I'm doing wrong?

{
     code: "function foo() { let bar = true; if (baz) { return; } else { { let bar = false; }  return; } }",
            output: null,
            parserOptions: { ecmaVersion: 6 },
            errors: [
                { messageId: "unexpected", type: "BlockStatement" }
            ]
  }

this test is not failing. Like I understand, [output] must be:

output : "function foo() { let bar = true; if (baz) {  } else { { let bar = false; }  } }"

@not-an-aardvark
Copy link
Member

I think @ajafff meant that the output should be

function foo() {
  let bar = true;
  if (baz) {
    return;
  }
    {
      let bar = false; 
    }
}

...since it would be safe to remove the else block in that case because all the variables declared the let block would be scoped off, and there would be no risk of having the variables redeclared elsewhere.

@ajafff
Copy link
Contributor

ajafff commented Nov 18, 2018

I meant exactly what @not-an-aardvark wrote. Thank you for clarifying.

@captain-yossarian
Copy link
Author

@ajafff @not-an-aardvark
This is the inital code:
initial
When I saving it (trigger auto fix), I get the next version , without first [return] keyword:
firstfix
But! When I trying to test it:
test
I'm getting the next error:
testresult
My question:
Why do my test code does not trigger autofix, just like my IDE?

P.S. After two fixes, my IDE (with my code) return next result:
secondfix
So, it is mean that my code has not any side effects.

@not-an-aardvark
Copy link
Member

@SerhiiBilyk I think this is happening because the autofixer for the no-useless-return rule is also getting applied at the same time, which causes the no-else-return rule autofixer to temporarily not get applied because the VSCode integration sometimes doesn't fix all problems. I wouldn't worry about this.

@captain-yossarian
Copy link
Author

@not-an-aardvark @ajafff Is it will be ok?

@aladdin-add aladdin-add added the accepted There is consensus among the team that this change meets the criteria for inclusion label Nov 19, 2018
@@ -182,6 +182,32 @@ ruleTester.run("no-else-return", rule, {
output: "function foo21() { var x = true; if (x) { return x; } if (x === false) { return false; } }",
options: [{ allowElseIf: false }],
errors: [{ messageId: "unexpected", type: "IfStatement" }]
},
{
code: "function foo() { if (true) { return bar; } else { const baz = 1; return baz; } }",
Copy link
Member

Choose a reason for hiding this comment

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

why this case cannot be fixed? IMHO, it seems safe to do the fix.

Copy link
Author

Choose a reason for hiding this comment

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

@aladdin-add Does my test code correct?

Copy link
Member

Choose a reason for hiding this comment

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

This case should be fixable because the declaration in the else block doesn't shadow any other variable.

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 okay to just not autofix any block scope declarations to be safe, but I think this one could be autofixed since none of the declarations share a name with the variable in the immediate upper scope.

@captain-yossarian
Copy link
Author

@platinumazure
Hi, my PR still have 'requested changes' status
Can you please check what else I have to do?

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

I've left some comments. Let me know if you have any questions.

Also, your current approach does not take into account any nested scopes that may be introduced within an else block. It might be worth using context.getScope on the BlockStatement node in the IfStatement's alternate property, getting that scope's upper scope, and comparing the declared variables of both scopes. If the else scope declares a new variable with the same name as a variable in the upper scope, then that variable is being shadowed in the pre-autofix code and the fix isn't safe. Using this approach should also allow for the nested scope declarations (i.e., the last test cases) to be fixed safely.

* If else block includes block scope local variable declaration [let or const],
* then it is not safe to remove else keyword [issue 11069]
*/
const isDeclarationInside = sourceCode.getTokensBetween(startToken, endToken).findIndex(isVariableDeclaration) > -1;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: You could use .some here to simplify

Copy link
Author

Choose a reason for hiding this comment

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

@platinumazure I've used .findIndex, because it will stop iteration if it will find an element and .some method will iterate through whole array, even it will find an element

Copy link
Member

Choose a reason for hiding this comment

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

@SerhiiBilyk Not right. The some method also stops iteration once the callback returns truthy value. You can check MDN for detail: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some

Copy link
Author

Choose a reason for hiding this comment

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

@g-plane you are right, thank you. I will change it!

@@ -182,6 +182,32 @@ ruleTester.run("no-else-return", rule, {
output: "function foo21() { var x = true; if (x) { return x; } if (x === false) { return false; } }",
options: [{ allowElseIf: false }],
errors: [{ messageId: "unexpected", type: "IfStatement" }]
},
{
code: "function foo() { if (true) { return bar; } else { const baz = 1; return baz; } }",
Copy link
Member

Choose a reason for hiding this comment

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

This case should be fixable because the declaration in the else block doesn't shadow any other variable.

errors: [{ messageId: "unexpected", type: "BlockStatement" }]
},
{
code: "function foo() { let bar = true; if (baz) { return; } else { { let bar = false; } return; } }",
Copy link
Member

Choose a reason for hiding this comment

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

This test case could be fixed as well. After removing the else and associated braces, let bar = false is in its own scope due to the extra braces. So this fix is safe.

@platinumazure
Copy link
Member

Also, I apologize for losing track of this.

@captain-yossarian
Copy link
Author

@platinumazure can you check my last commit?
I've created [ childScopeVars ] function which gather all variables from child scopes of [else] block, and then [ shadowedVariables ] checks is there any shadows.


/**
* If else block includes block scope local variable declaration [let or const],
* then it is not safe to remove else keyword [issue 11069]
Copy link
Contributor

Choose a reason for hiding this comment

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

it's safe to remove the else keyword. it's not safe to remove the Block of the else branch

const shadowed = shadowedVariables(context.getScope());
console.log('test', shadowed)

if (isDeclarationInside) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this will evaluate to true if there's a let or const somewhere in a nested child scope. instead it should only abort here if the else-block contains block scoped declarations

Copy link
Author

Choose a reason for hiding this comment

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

@ajafff
Do you mean

else {
    {
      const x = false;
    }
    return null;
  }

by "block scoped declarations" ?

Copy link
Author

Choose a reason for hiding this comment

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

@ajafff Does it mean, that if else contain block scoped declarations , fix method has to return null (abort)?
Can you please provide an example?

Copy link
Contributor

Choose a reason for hiding this comment

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

"Block scoped declaration" means const, let and class as they are not hoisted to the containing function.

let foo = 1;
function canNotBeFixed() {
  if (condition) {
    return foo;
  } else {
    let foo = 2; // else-block contains block scoped declaration, can not be fixed
    console.log(foo);
  }
}

function canBeFixed() {
  if (condition) {
    return foo;
  } else {
    {
      let foo = 2; // block scoped declaration is not directly in else-block, but in a nested block
      console.log(foo);
    }
  }
}

const result = childScopeVars(scope.childScopes);
return scope.variables.filter(elem => result[0].findIndex(vars => vars.name === elem.name) > -1)
};
const shadowed = shadowedVariables(context.getScope());
Copy link
Contributor

Choose a reason for hiding this comment

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

you probably don't need to compute the shadowed names. Consider the following example, where the shadowed declaration is in a parent scope:

let foo = 1;
function test() {
  if (condition) {
    return foo;
  } else {
    let foo = 2;
    console.log(foo)
  }
}

removing the else-block changes the reference of foo in the then-branch (which will throw as the use is in the temporal dead zone)

Copy link
Author

Choose a reason for hiding this comment

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

@ajafff Thank you very much for your example and patience. I made another one commit. Can you check it?
Still have problem with variable shadowing,Don't know how to handle it.
P.S. Happy holidays!


const shadowedVariables = scope => {
const result = childScopeVars(scope.childScopes);
return scope.variables.filter(elem => result[0].findIndex(vars => vars.name === elem.name) > -1)
Copy link
Contributor

Choose a reason for hiding this comment

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

consider using some instead of findIndex if you're only interested in a match and don't use the index

@captain-yossarian
Copy link
Author

@ajafff Thank you very much for your comments!!!!!!!!!

const elseBranch =
start === startToken.start && end === endToken.end;

return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like there's a missing semicolon at the end of the line

Copy link
Member

Choose a reason for hiding this comment

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

Please fix the missing semicolon on this line, as well as any other lint errors. Thanks!

const elseBranch =
start === startToken.start && end === endToken.end;

return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know exactly how ESLint's scope handling works, but it should work if you only check if childScope.variables.length !== 0. That's because function scoped declarations like var or function are hoisted to the containing function scope.

Copy link
Member

Choose a reason for hiding this comment

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

I agree that it's worth trying out this simplification, although I think the real problem is that we need to track all variables available in the containing scope and look for declarations with the same name (which are shadowing before the autofix, but redeclaring after).

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

I've left a comment about fixing lint errors. Additionally, I tried to explain more about what I think the logic should look like, but I expect we might need to go back and forth a bit to figure out the best way to handle it.

Let's start by fixing the lint errors first... Thanks!

const elseBranch =
start === startToken.start && end === endToken.end;

return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false
Copy link
Member

Choose a reason for hiding this comment

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

Please fix the missing semicolon on this line, as well as any other lint errors. Thanks!

const elseBranch =
start === startToken.start && end === endToken.end;

return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false
Copy link
Member

Choose a reason for hiding this comment

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

I agree that it's worth trying out this simplification, although I think the real problem is that we need to track all variables available in the containing scope and look for declarations with the same name (which are shadowing before the autofix, but redeclaring after).

@captain-yossarian
Copy link
Author

captain-yossarian commented Feb 10, 2019

@platinumazure I have fixed eslint errors.
Thank you for your help.

I agree that it's worth trying out this simplification, although I think the real problem is that we need to track all variables available in the containing scope and look for declarations with the same name (which are shadowing before the autofix, but redeclaring after).

Do you mean, that I should try this (0b3a3a4) approach?

@ilyavolodin
Copy link
Member

@platinumazure What's the status of this PR? Anything else needs to be done here?

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

Hi @SerhiiBilyk, I apologize for having let this slip.

I have a few simple changes, and then I was hoping we could look at the 2 test cases that could potentially be autofixable. It might be okay to merge this as is since it's better for our autofixer to be cautious than overeager, but I wanted to see what you thought about trying to make those 2 cases fixable.

Thanks so much for your patience and all of your effort so far. On behalf of the ESLint team, I greatly appreciate the hard work!

* @param {Token} endToken s
* @returns {boolean} s
*/
function blockScopeDeclaration(scope, startToken, endToken) {
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 a little confused by this function name. What are we checking here? It looks like we might be trying to check if an else block has any block scope declarations?

I would suggest prefixing the function name with "is" or "has", if it is returning a boolean.

Copy link
Author

Choose a reason for hiding this comment

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

I have renamed it.
Yes, I'm checking here if else block has any scope declarations,

* @param {*} scope current scope
* @param {Token} startToken s
* @param {Token} endToken s
* @returns {boolean} s
Copy link
Member

Choose a reason for hiding this comment

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

Could you please fill in these comments (this line, and the two lines above it)?

@@ -182,6 +182,32 @@ ruleTester.run("no-else-return", rule, {
output: "function foo21() { var x = true; if (x) { return x; } if (x === false) { return false; } }",
options: [{ allowElseIf: false }],
errors: [{ messageId: "unexpected", type: "IfStatement" }]
},
{
code: "function foo() { if (true) { return bar; } else { const baz = 1; return baz; } }",
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 okay to just not autofix any block scope declarations to be safe, but I think this one could be autofixed since none of the declarations share a name with the variable in the immediate upper scope.

},
{
code: "function foo() { if (true) { return bar; } else { let baz = 1; return baz; } }",
output: 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 okay to just not autofix any block scope declarations to be safe, but I think this one could be autofixed since none of the declarations share a name with the variable in the immediate upper scope.

Copy link
Author

Choose a reason for hiding this comment

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

Hi @platinumazure , I'm sorry for the delay. I have started to work with this issue.
What the expected output here ?

@captain-yossarian
Copy link
Author

Hi @platinumazure ,
Thank you for your answer, I will check all your comment tomorrow.

@platinumazure
Copy link
Member

Friendly ping @SerhiiBilyk: Are you still planning to work on this? Is there anything I can do to help? Thanks!

@captain-yossarian
Copy link
Author

@platinumazure I'm sorry, but unfortunately I have no time to work on this bug. Sorry for the delay

@aladdin-add
Copy link
Member

@SerhiiBilyk no worries, thanks for the contributing regardless!

@eslint/eslint-team shall we close it?

@ilyavolodin
Copy link
Member

I'm going to close this PR, since it's been open for a while, and original owner doesn't have time to work on it anymore. Thanks again for the PR!

@eslint-deprecated eslint-deprecated bot locked and limited conversation to collaborators Nov 22, 2019
@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 Nov 22, 2019
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

9 participants