Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): [no-confusing-void-expression] add ignoreVoidInVoid option to void in void situation#8632
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
Thanks for the PR,@developer-bandi! typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community. The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately. Thanks again! 🙏Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently onhttps://opencollective.com/typescript-eslint. |
netlifybot commentedMar 9, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
✅ Deploy Preview fortypescript-eslint ready!
To edit notification comments on pull requests, go to yourNetlify site configuration. |
nx-cloudbot commentedMar 9, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
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 nice start, thank you for sending it in!
packages/eslint-plugin/docs/rules/no-confusing-void-expression.md OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
developer-bandi commentedMar 10, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Except for the comment left at the top, there were no objections to the comments you left, so i has reflected all of them. |
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.
Getting closer 🙌
I'm requesting changes on one blocking bug and two non-blocking style/refactor changes!
if (options.ignoreVoidInVoid) { | ||
if ( | ||
invalidAncestor.returnType?.typeAnnotation.type === | ||
AST_NODE_TYPES.TSVoidKeyword | ||
) { | ||
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.
if(options.ignoreVoidInVoid){ | |
if( | |
invalidAncestor.returnType?.typeAnnotation.type=== | |
AST_NODE_TYPES.TSVoidKeyword | |
){ | |
return; | |
} | |
} | |
if(options.ignoreVoidInVoid&&invalidAncestor.returnType?.typeAnnotation.type===AST_NODE_TYPES.TSVoidKeyword){ | |
return; | |
} | |
[Style] Totally non-blocking, I just noticed that we can inline these two conditions. Feel free to ignore this, if you find the current implementation more readable.
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.
thank you i agree this style
targetNodeAncestorHasVoidReturnType(invalidAncestor, [ | ||
AST_NODE_TYPES.FunctionDeclaration, | ||
AST_NODE_TYPES.ArrowFunctionExpression, | ||
AST_NODE_TYPES.FunctionExpression, | ||
]) |
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.
[Refactor] I see no reason why we would want to usetargetNodeAncestorHasVoidReturnType
with some other set oftargets
nodes. So I think we can just put those node types inside the function. WDYT?
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 agreed that there was no possibility of reuse, so i moved the targets inside the function and changed the function name to be more clear.
function targetNodeAncestorHasVoidReturnType( | ||
node: TSESTree.Node, | ||
targets: ( | ||
| AST_NODE_TYPES.FunctionDeclaration | ||
| AST_NODE_TYPES.ArrowFunctionExpression | ||
| AST_NODE_TYPES.FunctionExpression | ||
)[], | ||
): boolean { | ||
if (!node.parent) { | ||
return false; | ||
} | ||
const filter = targets.filter(target => { | ||
if (target === node.parent.type) { | ||
if ( | ||
node.parent.returnType?.typeAnnotation.type === | ||
AST_NODE_TYPES.TSVoidKeyword | ||
) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}); | ||
if (filter.length > 0) { | ||
return true; | ||
} | ||
return targetNodeAncestorHasVoidReturnType(node.parent, targets); | ||
} |
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.
[Bug] This function won't stop until it finds a matching parent, so the following case is false negative
functionfoo():void{constbar=()=>{returnconsole.log()}}
I think that this function should look +- like this:
- if
!node.parent
-> return false - if
node.parent
is one of theFunctionDeclaration
,ArrowFunctionExpression
,FunctionExpression
:- if
node.parent
hasvoid
in type annotation -> return true - if
node.parent
hasn'tvoid
in type annotation -> return false
- if
- return
targetNodeAncestorHasVoidReturnType(node.parent)
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.
in playground, this case seems false positive. anyway, this is bug, so i add test code and fixtargetNodeAncestorHasVoidReturnType
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.
in playground, this case seems false positive
As far as I know:
false negative === the ruleshould report error, butit doesn't do so
false positive === the ruleshould not report error, butit does so
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 not familiar with the testing, so I think I used the terminology the other way around. thank you for telling me!
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.
Progress 🚀
if (options.ignoreVoidInVoid) { | ||
if ( | ||
targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor) | ||
) { | ||
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.
[Style] Same as here:#8632 (comment)
if(options.ignoreVoidInVoid){ | |
if( | |
targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor) | |
){ | |
return; | |
} | |
} | |
if(options.ignoreVoidInVoid&&targetNodeFunctionAncestorNodeHasVoidReturnType(invalidAncestor)){ | |
return; | |
} |
WDYT about inlining this if 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.
same convention is make sense. thank you!
if ( | ||
targets[0] === node.parent.type || | ||
targets[1] === node.parent.type || | ||
targets[2] === node.parent.type | ||
) { |
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.
if( | |
targets[0]===node.parent.type|| | |
targets[1]===node.parent.type|| | |
targets[2]===node.parent.type | |
){ | |
if( | |
AST_NODE_TYPES.FunctionDeclaration===node.parent.type|| | |
AST_NODE_TYPES.ArrowFunctionExpression===node.parent.type|| | |
AST_NODE_TYPES.FunctionExpression===node.parent.type | |
){ |
[Refactor] Why do we need thetargets
tuple if we don't iterate over it? IMO if we directly comparenode.parent.type
to AST node types, it will be more readable. What do you think?
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.
Comparing variables directly seems much more readable!
if ( | ||
node.parent.returnType?.typeAnnotation.type === | ||
AST_NODE_TYPES.TSVoidKeyword | ||
) { | ||
return true; | ||
} | ||
return 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.
[Bug] I just noticed that we determine whether function signature hasvoid
return type or not by doing pure syntax check.
Thus, the following case is false positive (the rule shouldn't report error, since function returnsFoo
- an alias ofvoid
type):
typeFoo=voidfunctiontest():Foo{returnconsole.log()}
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 didn't even think about it. 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.
[Testing] Some more cases to add:
- the case mentionedhere -
return console.log('foo')
on the top-level - function without
void
in signature contains in its body function withvoid
in signature, and this nested function returnsreturn console.log('foo')
- Type aliases, like in the comment above ^ (in the signature of the parent function, in the signature of the returned function, in both signatures at the same time)
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 added a test case, but I may have misunderstood the last comment, so please check my new test code. 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.
I think it's fine.
Also, I think it would be nice to add the following cases:
functiontest():void&void{returnconsole.log('foo')}
typeFoo=voiddeclarefunctionfoo():Foofunctiontest():Foo{returnfoo()}
// se UPD below. there is a typotypeFoo=voidconsttest:Foo=()=>console.log('err')
UPD: there was a typo in the last code block! We shouldn't consider case with such a weird typing (the function is being assigned to variable withvoid
type). Instead, I meant the following code:
typeFoo=voidconsttest=():Foo=>console.log('err')
developer-bandiMar 11, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
thank you! add all test case and last case has ignoreArrowShorthand
option to pass, so addignoreArrowShorthand
option
i think last case situation is same tobelow comment
if ( | ||
options.ignoreVoidInVoid && | ||
invalidAncestor.returnType?.typeAnnotation.type === | ||
AST_NODE_TYPES.TSVoidKeyword | ||
) { | ||
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.
[Bug] Same as here#8632 (comment)...
typeFoo=voidconsttest=():Foo=>console.log()
The error shouldn't be reported, since arrow function has thevoid
return type
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 think this error is because of ignoreArrowShorthand. so ignoreArrowShorthand option to true or change code like:
type Foo = voidconst test = (): Foo => { return console.log(); }
I add test case with ignoreArrowShorthand option 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.
i think this error is because of ignoreArrowShorthand. so ignoreArrowShorthand option to true or change code like:
The rule reports error because it does pure syntax check instead of type-level check.
typeFoo=voidconsttest1=():void=>console.log()// not reported - okconsttest2=():Foo=>console.log()// reported - ok
ignoreArrowShorthand
ignores all arrow functions, but rule users may want to ignore only void <-> void cases as mentioned in the original issue.
developer-bandiMar 12, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
thank you i understand it! and since it seems to be valid even when declaring a type in a variable of a function expression, I modified the code and test case.
typeFoo=()=>void;consttest:Foo=function(){returnconsole.log('err');};
if ( | ||
node.parent.returnType && | ||
!tsutils.isTypeFlagSet( | ||
getConstrainedTypeAtLocation(services, node.parent.returnType), | ||
ts.TypeFlags.VoidLike, | ||
) | ||
) { | ||
return true; | ||
} | ||
return 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.
if( | |
node.parent.returnType&& | |
!tsutils.isTypeFlagSet( | |
getConstrainedTypeAtLocation(services,node.parent.returnType), | |
ts.TypeFlags.VoidLike, | |
) | |
){ | |
returntrue; | |
} | |
returnfalse; | |
return( | |
node.parent.returnType&& | |
!tsutils.isTypeFlagSet( | |
getConstrainedTypeAtLocation(services,node.parent.returnType), | |
ts.TypeFlags.VoidLike, | |
) | |
) |
[Refactor] This code may be converted into a single return statement. What do you think?
node.returnType.typeAnnotation.type !== | ||
AST_NODE_TYPES.TSAnyKeyword && | ||
node.returnType.typeAnnotation.type !== | ||
AST_NODE_TYPES.TSUnknownKeyword) || |
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.
[Bug] I somehow missed this in previous reviews: we can't rely on syntactic check here
():any=>console.log();// reported - nice!():unknown=>console.log();// reported - nice!typeFoo=any():Foo=>console.log();// not reported - bug!typeBar=unknown():Bar=>console.log();// not reported - bug!
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.
When specifying a type using a type alias, it is difficult to find a way to determine what type it is.
Could you please give me a hint?
type Bar = unknown(): Bar => console.log(); // how to know Bar is unknown type in create fn
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.
@auvred Can you help with the issue left in the comment above?
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 not 100% sure about this, but I'd try playing around with function signatures. There is nice utility function in thets-api-utils
package -getCallSignaturesOfType
.
Search with some examples of usage:https://github.com/search?q=repo%3Atypescript-eslint%2Ftypescript-eslint%20getCallSignaturesOfType&type=code
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 works to me very well. 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.
[Testing] I think it would be nice to add few tests after#8809 is merged. This PR fixes theancestorHasReturnType
behavior.
We can add something like this:
functionfoo():void{()=>()=>console.log();}functionfoo():any{()=>()=>console.log();}
(both of the above cases should be reported)
…eslint into feat/no-confusing-void-expression
codecovbot commentedApr 28, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@## main #8632 +/- ##==========================================- Coverage 87.40% 87.24% -0.16%========================================== Files 260 251 -9 Lines 12605 12336 -269 Branches 3937 3894 -43 ==========================================- Hits 11017 10763 -254+ Misses 1313 1303 -10+ Partials 275 270 -5
Flags with carried forward coverage won't be shown.Click here to find out 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.
Requesting changes on#8632 (comment). I will re-review this PR more thoroughly after this comment is resolved.
@@ -376,5 +427,55 @@ export default createRule<Options, MessageId>({ | |||
const type = getConstrainedTypeAtLocation(services, targetNode); | |||
return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike); | |||
} | |||
function hasValidReturnType( | |||
services: ParserServicesWithTypeInformation, |
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.
[Refactor] (optional)hasValidReturnType
function is declared in block wherecontext
is visible. What do you think about callingconst services = getParserServices(context);
insidehasValidReturnType
(likecanFix
function above does) instead of passingservices
as an argument?
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 too believe that using context is better than passing a service.
const functionType = | ||
(ts.isFunctionExpression(functionTSNode) || | ||
ts.isArrowFunction(functionTSNode) | ||
? getContextualType(checker, functionTSNode) | ||
: services.getTypeAtLocation(node)) ?? | ||
services.getTypeAtLocation(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.
[Bug] Why do we get a contextual type for function expressions and arrow functions?
The rule you took this code from has a detailed explanation of why it callsgetContextualType
for function expressions and arrow functions:
// function expressions will not have their return type modified based on receiver typing | |
// so we have to use the contextual typing in these cases, i.e. | |
// const foo1: () => Set<string> = () => new Set<any>(); | |
// the return type of the arrow function is Set<any> even though the variable is typed as Set<string> | |
letfunctionType= | |
ts.isFunctionExpression(functionTSNode)|| | |
ts.isArrowFunction(functionTSNode) | |
?getContextualType(checker,functionTSNode) | |
:services.getTypeAtLocation(functionNode); |
This means that the following case is reported:
consttest:()=>any=():void=>console.log()
(it should not be reported since(): void => console.log()
is allowed withignoreVoidInVoid: true
)
for (const signature of tsutils.getCallSignaturesOfType( | ||
functionType, | ||
)) { | ||
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.
[Bug] This code returns fromfor
loop on the first iteration. So if a function has several signatures, the rule will check only the first signature:
functiontest1():voidfunctiontest1(arg:string):anyfunctiontest1(arg?:string):any|void{if(arg){returnarg}returnconsole.log()// not reported - ok}functiontest2(arg:string):anyfunctiontest2():voidfunctiontest2(arg?:string):any|void{if(arg){returnarg}returnconsole.log()// reported - bug}
tsutils.isTypeFlagSet( | ||
signature.getReturnType(), | ||
ts.TypeFlags.Any, | ||
) || | ||
tsutils.isTypeFlagSet( | ||
signature.getReturnType(), | ||
ts.TypeFlags.Unknown, | ||
) |
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.
tsutils.isTypeFlagSet( | |
signature.getReturnType(), | |
ts.TypeFlags.Any, | |
)|| | |
tsutils.isTypeFlagSet( | |
signature.getReturnType(), | |
ts.TypeFlags.Unknown, | |
) | |
tsutils.isTypeFlagSet( | |
signature.getReturnType(), | |
ts.TypeFlags.Any|ts.TypeFlags.Unknown, | |
) |
[Refactor] Let's do a bitwise OR of these flags instead of callingisTypeFlagSet
twice?
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 did not understand that the isTypeFlagSet function utilizes bitwise operators, so I called it twice. thank you for telling me!
if ( | ||
((node.type === AST_NODE_TYPES.FunctionExpression || | ||
node.type === AST_NODE_TYPES.ArrowFunctionExpression) && | ||
isValidFunctionExpressionReturnType(node, { | ||
allowTypedFunctionExpressions: true, | ||
})) || | ||
ancestorHasReturnType(node) | ||
) { | ||
return 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.
If we check that function signature don't containany
andunknown
in the return type, we should do the same for typed functions/variable declarations. The following cases are not reported:
typeHigherOrderType=()=>any;():HigherOrderType=>()=>console.log();constx:HigherOrderType=()=>console.log();
return arg; | ||
} | ||
return console.log(); // reported - bug |
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 see my comment from the one of the previous reviews#8632 (comment). 🙂
The code samples I provide, which contain some error messages, are for reference only. They are not intended to be directly copied into real tests. I'm including comments on these code samples to better illustrate what the bug I found is. The same goes for variable names:test1
andtest2
are named to avoid name collisions in the playground. Having thetest2
function in a separate test case withouttest1
looks a bit confusing.
I ask you to pay attention to the reviews and not just blindly insert some stuff into the code, assuming that someone will notice it in one of the next reviews anyway.
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 apologize for making the same mistake by not checking the code thoroughly after writing it.
In the future, I will look more closely before submitting a review.
I tried several methods to correct the error, but it seems that I need to check if the return type is void, rather than excluding the current any and unknown types. However, there is currently not much time to work on this, so only the test code in this PR has been modified and reflected. If there is anyone who wants to continue this work or it has been too long, you can close the pr or provide a new pr. |
👍 thanks for the heads up, and of course all your work on this@developer-bandi! Closing the PR out now so if other folks want to tackle it, they can. If anybody wants to drive it forward, please do post your own PR - and if you use this as a start, consider adding |
PR Checklist
Overview
add ignoreVoidInVoid option in no-confusing-void-expression to resolve void in void situation. below example is avaliable