-
-
Notifications
You must be signed in to change notification settings - Fork 2.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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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 on https://opencollective.com/typescript-eslint. |
✅ Deploy Preview for typescript-eslint ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
packages/eslint-plugin/src/rules/no-confusing-void-expression.ts
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/src/rules/no-confusing-void-expression.ts
Outdated
Show resolved
Hide resolved
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
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/src/rules/no-confusing-void-expression.ts
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/src/rules/no-confusing-void-expression.ts
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts
Outdated
Show resolved
Hide resolved
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 use targetNodeAncestorHasVoidReturnType
with some other set of targets
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
function foo(): void {
const bar = () => {
return console.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 fix targetNodeAncestorHasVoidReturnType
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 rule should report error, but it doesn't do so
false positive === the rule should not report error, but it 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 the targets
tuple if we don't iterate over it? IMO if we directly compare node.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 has void
return type or not by doing pure syntax check.
Thus, the following case is false positive (the rule shouldn't report error, since function returns Foo
- an alias of void
type):
type Foo = void
function test(): Foo {
return console.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 mentioned here -
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:
-
function test(): void & void { return console.log('foo') }
-
type Foo = void declare function foo(): Foo function test(): Foo { return foo() }
-
// se UPD below. there is a typo type Foo = void const test: 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 with void
type). Instead, I meant the following code:
-
type Foo = void const test = (): Foo => console.log('err')
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 add ignoreArrowShorthand
option
i think last case situation is same to below 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)...
type Foo = void
const test = (): Foo => console.log()
The error shouldn't be reported, since arrow function has the void
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 = void
const 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.
type Foo = void
const test1 = (): void => console.log() // not reported - ok
const test2 = (): 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.
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.
type Foo = () => void;
const test: Foo = function () {
return console.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, | |
) | |
) { | |
return true; | |
} | |
return false; | |
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!
type Foo = any
(): Foo => console.log(); // not reported - bug!
type Bar = 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 the ts-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 the ancestorHasReturnType
behavior.
We can add something like this:
function foo(): void {
() => () => console.log();
}
function foo(): any {
() => () => console.log();
}
(both of the above cases should be reported)
…eslint into feat/no-confusing-void-expression
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 where context
is visible. What do you think about calling const services = getParserServices(context);
inside hasValidReturnType
(like canFix
function above does) instead of passing services
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 calls getContextualType
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> | |
let functionType = | |
ts.isFunctionExpression(functionTSNode) || | |
ts.isArrowFunction(functionTSNode) | |
? getContextualType(checker, functionTSNode) | |
: services.getTypeAtLocation(functionNode); |
This means that the following case is reported:
const test: () => any = (): void => console.log()
(it should not be reported since (): void => console.log()
is allowed with ignoreVoidInVoid: 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 from for
loop on the first iteration. So if a function has several signatures, the rule will check only the first signature:
function test1(): void
function test1(arg: string): any
function test1(arg?: string): any | void {
if (arg) {
return arg
}
return console.log() // not reported - ok
}
function test2(arg: string): any
function test2(): void
function test2(arg?: string): any | void {
if (arg) {
return arg
}
return console.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 calling isTypeFlagSet
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 contain any
and unknown
in the return type, we should do the same for typed functions/variable declarations. The following cases are not reported:
type HigherOrderType = () => any;
(): HigherOrderType => () => console.log();
const x: 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
and test2
are named to avoid name collisions in the playground. Having the test2
function in a separate test case without test1
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