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
Conversation
7f6912c
to
cc00be7
Compare
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 |
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.
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:
302d094
to
1945162
Compare
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 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 😄
@platinumazure done |
@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; } } }" |
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 |
I meant exactly what @not-an-aardvark wrote. Thank you for clarifying. |
@ajafff @not-an-aardvark P.S. After two fixes, my IDE (with my code) return next result: |
@SerhiiBilyk I think this is happening because the autofixer for the |
@not-an-aardvark @ajafff Is it will be ok? |
@@ -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; } }", |
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.
why this case cannot be fixed? IMHO, it seems safe to do the fix.
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.
@aladdin-add Does my test code correct?
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 case should be fixable because the declaration in the else
block doesn't shadow any other variable.
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 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.
@platinumazure |
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'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.
lib/rules/no-else-return.js
Outdated
* 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; |
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.
Nit: You could use .some
here to simplify
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.
@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
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.
@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
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.
@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; } }", |
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 case should be fixable because the declaration in the else
block doesn't shadow any other variable.
tests/lib/rules/no-else-return.js
Outdated
errors: [{ messageId: "unexpected", type: "BlockStatement" }] | ||
}, | ||
{ | ||
code: "function foo() { let bar = true; if (baz) { return; } else { { let bar = false; } return; } }", |
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 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.
Also, I apologize for losing track of this. |
@platinumazure can you check my last commit? |
lib/rules/no-else-return.js
Outdated
|
||
/** | ||
* If else block includes block scope local variable declaration [let or const], | ||
* then it is not safe to remove else keyword [issue 11069] |
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's safe to remove the else
keyword. it's not safe to remove the Block of the else
branch
lib/rules/no-else-return.js
Outdated
const shadowed = shadowedVariables(context.getScope()); | ||
console.log('test', shadowed) | ||
|
||
if (isDeclarationInside) { |
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 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
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.
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.
@ajafff Does it mean, that if else
contain block scoped declarations , fix
method has to return null (abort)?
Can you please provide an example?
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.
"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);
}
}
}
lib/rules/no-else-return.js
Outdated
const result = childScopeVars(scope.childScopes); | ||
return scope.variables.filter(elem => result[0].findIndex(vars => vars.name === elem.name) > -1) | ||
}; | ||
const shadowed = shadowedVariables(context.getScope()); |
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.
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)
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.
@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!
lib/rules/no-else-return.js
Outdated
|
||
const shadowedVariables = scope => { | ||
const result = childScopeVars(scope.childScopes); | ||
return scope.variables.filter(elem => result[0].findIndex(vars => vars.name === elem.name) > -1) |
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.
consider using some
instead of findIndex
if you're only interested in a match and don't use the index
@ajafff Thank you very much for your comments!!!!!!!!! |
lib/rules/no-else-return.js
Outdated
const elseBranch = | ||
start === startToken.start && end === endToken.end; | ||
|
||
return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false |
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.
seems like there's a missing semicolon at the end of the line
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.
Please fix the missing semicolon on this line, as well as any other lint errors. Thanks!
lib/rules/no-else-return.js
Outdated
const elseBranch = | ||
start === startToken.start && end === endToken.end; | ||
|
||
return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false |
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 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.
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 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).
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'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!
lib/rules/no-else-return.js
Outdated
const elseBranch = | ||
start === startToken.start && end === endToken.end; | ||
|
||
return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false |
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.
Please fix the missing semicolon on this line, as well as any other lint errors. Thanks!
lib/rules/no-else-return.js
Outdated
const elseBranch = | ||
start === startToken.start && end === endToken.end; | ||
|
||
return elseBranch ? childScope.variables.some(variable => BLOCK_SCOPED_DECLARATION.test(variable.defs[0].node.type)) : false |
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 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).
@platinumazure I have fixed eslint errors.
Do you mean, that I should try this (0b3a3a4) approach? |
@platinumazure What's the status of this PR? Anything else needs to be done here? |
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.
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) { |
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'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.
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 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 |
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.
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; } }", |
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 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, |
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 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.
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.
Hi @platinumazure , I'm sorry for the delay. I have started to work with this issue.
What the expected output here ?
Hi @platinumazure , |
Friendly ping @SerhiiBilyk: Are you still planning to work on this? Is there anything I can do to help? Thanks! |
@platinumazure I'm sorry, but unfortunately I have no time to work on this bug. Sorry for the delay |
@SerhiiBilyk no worries, thanks for the contributing regardless! @eslint/eslint-team shall we close it? |
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! |
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?