-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): add rule [strict-void-return] #9707
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
Open
phaux
wants to merge
60
commits into
typescript-eslint:main
Choose a base branch
from
phaux:strict-void-return
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,842
−1
Open
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
76c295b
feat(eslint-plugin): add rule [strict-void-return]
phaux 5454727
chore: generate configs
phaux 38e2d18
better handling of base class/interfaces
phaux cb58ccc
update docs
phaux 75da13d
restructure some code
phaux db1bc42
Merge branch 'main' into strict-void-return
phaux e0ef02d
Merge branch 'main' into strict-void-return
phaux 2d82282
update to new RuleTester
phaux 8cea97b
more detailed messages
phaux c16c874
Merge branch 'main' into strict-void-return
phaux d8f2b03
more coverage
phaux b353dc9
lint fix
phaux 3de72ca
fix addEventListener test
phaux ae1fd10
Merge branch 'main' into strict-void-return
phaux cc7a23e
fix node imports
phaux fe7a62e
Merge branch 'main' into strict-void-return
phaux a335f3c
simplify options
phaux 4d8bf19
Apply suggestions from code review
31434c8
Merge branch 'main' into strict-void-return
4075db2
docs
phaux 4c9a620
Merge branch 'main' into strict-void-return
phaux f4f68f9
update snapshots
phaux f4bc0bc
fix lint errors
phaux 458ccf5
simplify messages
phaux 335a7cf
move option descriptions to schema
phaux 6981f67
update snapshots
phaux 3c0484f
Merge branch 'main' into strict-void-return
phaux 851f472
update messages
phaux 21b4020
don't use node:assert
phaux 8687130
simplify examples
phaux 3f9d977
update snapshots
phaux 172548c
remove autofixes
phaux a484413
Merge branch 'main' into strict-void-return
phaux bb4c90b
remove unused function
phaux 5d76db0
Merge branch 'main' into strict-void-return
phaux 5c56d72
Merge branch 'main' into strict-void-return
phaux 55b6378
recreate from main
phaux 600422e
Merge branch 'main' into strict-void-return
phaux f72849a
take ESTree nodes in utils
phaux b43a125
add tests
phaux 76c3b30
add test
phaux 75ac458
remove dynamic messages
phaux 2f0326d
remove unnecessary code
phaux 8b20c4b
remove allowReturnPromiseIfTryCatch
phaux 74dfcba
update docs
phaux ede0145
Merge branch 'main' into strict-void-return
JoshuaKGoldberg 47bfe75
remove unnecessary condition
phaux 2137c1c
test weird jsx
phaux a061962
fix deprecation
phaux 337694a
Update docs
phaux 1143df9
Update docs
phaux 1b1ed16
Update docs
phaux f63c609
remove comment
phaux f491103
Update docs
phaux 9ba717f
Update docs
phaux 83cb8a7
Update docs
phaux 5d71ef5
simplify void check
phaux 9b2f2f4
assert object prop type
phaux 3cad6b9
test empty and accessor methods
phaux aad8977
update docs
phaux File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
321 changes: 321 additions & 0 deletions
321
packages/eslint-plugin/docs/rules/strict-void-return.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
--- | ||
description: 'Disallow passing a value-returning function in a position accepting a void function.' | ||
--- | ||
|
||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
|
||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/strict-void-return** for documentation. | ||
|
||
## Rule Details | ||
|
||
TypeScript considers functions returning a value to be assignable to a function returning void. | ||
Using this feature of TypeScript can lead to bugs or confusing code. | ||
|
||
## Examples | ||
|
||
### Return type unsafety | ||
|
||
Passing a value-returning function in a place expecting a void function can be unsound. | ||
phaux marked this conversation as resolved.
Show resolved
Hide resolved
|
||
TypeScript generally treats the `void` type as though it has the same runtime behavior as `undefined`, | ||
so this pattern will cause a mismatch between the runtime behavior and the types. | ||
|
||
```ts | ||
// TypeScript errors on overtly wrong ways of populating the `void` type... | ||
function returnsVoid(): void { | ||
return 1234; // TS Error: Type 'number' is not assignable to type 'void'. | ||
} | ||
|
||
// ...but allows more subtle ones | ||
const returnsVoid: () => void = () => 1234; | ||
|
||
// Likewise, TypeScript errors on overtly wrong usages of `void` as a runtime value... | ||
declare const v: void; | ||
if (v) { | ||
// TS Error: An expression of type 'void' cannot be tested for truthiness. | ||
// ... do something | ||
} | ||
|
||
// ...but silently treats `void` as `undefined` in more subtle scenarios | ||
declare const voidOrString: void | string; | ||
if (voidOrString) { | ||
// voidOrString is narrowed to string in this branch, so this is allowed. | ||
console.log(voidOrString.toUpperCase()); | ||
} | ||
``` | ||
|
||
Between these two behaviors, examples like the following will throw at runtime, despite not reporting a type error: | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts | ||
const getNothing: () => void = () => 2137; | ||
const getString: () => string = () => 'Hello'; | ||
const maybeString = Math.random() > 0.1 ? getNothing() : getString(); | ||
if (maybeString) console.log(maybeString.toUpperCase()); // ❌ Crash if getNothing was called | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
const getNothing: () => void = () => {}; | ||
const getString: () => string = () => 'Hello'; | ||
const maybeString = Math.random() > 0.1 ? getNothing() : getString(); | ||
if (maybeString) console.log(maybeString.toUpperCase()); // ✅ No crash | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
### Unhandled returned promises | ||
|
||
If a callback is meant to return void, values returned from functions are likely ignored. | ||
Ignoring a returned Promise means any Promise rejection will be silently ignored | ||
or crash the process depending on runtime. | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts | ||
declare function takesCallback(cb: () => void): void; | ||
|
||
takesCallback(async () => { | ||
const response = await fetch('https://api.example.com/'); | ||
const data = await response.json(); | ||
console.log(data); | ||
}); | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
declare function takesCallback(cb: () => void): void; | ||
|
||
takesCallback(() => { | ||
(async () => { | ||
const response = await fetch('https://api.example.com/'); | ||
const data = await response.json(); | ||
console.log(data); | ||
})().catch(console.error); | ||
}); | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
:::info | ||
If you only care about promises, | ||
you can use the [`no-misused-promises`](no-misused-promises.mdx) rule instead. | ||
::: | ||
|
||
:::tip | ||
Use [`no-floating-promises`](no-floating-promises.mdx) | ||
to also enforce error handling of non-awaited promises in statement positions. | ||
::: | ||
|
||
### Ignored returned generators | ||
|
||
If a generator is returned from a void function it won't even be started. | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts | ||
declare function takesCallback(cb: () => void): void; | ||
|
||
takesCallback(function* () { | ||
console.log('Hello'); | ||
yield; | ||
console.log('World'); | ||
}); | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
declare function takesCallback(cb: () => void): void; | ||
|
||
takesCallback(() => { | ||
function* gen() { | ||
console.log('Hello'); | ||
yield; | ||
console.log('World'); | ||
} | ||
for (const _ of gen()); | ||
}); | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
### Accidental dead code | ||
|
||
Returning a value from a void function often is an indication of incorrect assumptions about APIs. | ||
Those incorrect assumptions can often lead to unnecessary code. | ||
|
||
The following `forEach` loop is a common mistake: its author likely either meant to add `console.log` or meant to use `.map` instead. | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts | ||
['Kazik', 'Zenek'].forEach(name => `Hello, ${name}!`); | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
['Kazik', 'Zenek'].forEach(name => console.log(`Hello, ${name}!`)); | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
### Void context from extended classes | ||
|
||
This rule enforces class methods which override a void method to also be void. | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts | ||
class Foo { | ||
cb() { | ||
console.log('foo'); | ||
} | ||
} | ||
|
||
class Bar extends Foo { | ||
cb() { | ||
super.cb(); | ||
return 'bar'; | ||
} | ||
} | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
class Foo { | ||
cb() { | ||
console.log('foo'); | ||
} | ||
} | ||
|
||
class Bar extends Foo { | ||
cb() { | ||
super.cb(); | ||
console.log('bar'); | ||
} | ||
} | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
### Void context from implemented interfaces | ||
|
||
This rule enforces class methods which implement a void method to also be void. | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts | ||
interface Foo { | ||
cb(): void; | ||
} | ||
|
||
class Bar implements Foo { | ||
cb() { | ||
return 'cb'; | ||
} | ||
} | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
interface Foo { | ||
cb(): void; | ||
} | ||
|
||
class Bar implements Foo { | ||
cb() { | ||
console.log('cb'); | ||
} | ||
} | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
## Options | ||
|
||
### `allowReturnAny` | ||
|
||
{/* insert option description */} | ||
|
||
Additional incorrect code when the option is **disabled**: | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts option='{ "allowReturnAny": false }' | ||
declare function fn(cb: () => void): void; | ||
|
||
fn(() => JSON.parse('{}')); | ||
|
||
fn(() => { | ||
return someUntypedApi(); | ||
}); | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts option='{ "allowReturnAny": false }' | ||
declare function fn(cb: () => void): void; | ||
|
||
fn(() => void JSON.parse('{}')); | ||
|
||
fn(() => { | ||
someUntypedApi(); | ||
}); | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
## When Not To Use It | ||
|
||
Some projects are architected so that values returned from synchronous void functions are generally safe. | ||
If you only want to check for misused voids with asynchronous functions then you can use [`no-misused-promises`](./no-misused-promises.mdx) instead. | ||
|
||
In browser context, an unhandled promise will be reported as an error in the console. | ||
It's generally a good idea to also show some kind of indicator on the page that something went wrong, | ||
but if you are just prototyping or don't care about that, the default behavior might be acceptable. | ||
In such case, instead of handling the promises and `console.error`ing them anyways, you can just disable this rule. | ||
|
||
Similarly, the default behavior of crashing the process on unhandled promise rejection | ||
might be acceptable when developing, for example, a CLI tool. | ||
If your promise handlers simply call `process.exit(1)` on rejection, | ||
you may prefer to avoid this rule and rely on the default behavior. | ||
|
||
## Related To | ||
|
||
- [`no-misused-promises`](./no-misused-promises.mdx) - A subset of this rule which only cares about promises. | ||
- [`no-floating-promises`](./no-floating-promises.mdx) - Warns about unhandled promises in _statement_ positions. | ||
- [`no-confusing-void-expression`](./no-confusing-void-expression.mdx) - Disallows returning _void_ values. | ||
|
||
## Further Reading | ||
|
||
- [TypeScript FAQ - Void function assignability](https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-returning-non-void-assignable-to-function-returning-void) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 is a great question.
no-misused-promises
was already largely overlapped byno-unnecessary-condition
. This newstrict-void-return
pretty much takes on the rest ofno-misused-promises
, makingno-misused-promises
redundant if you have bothno-unnecessary-condition
andstrict-void-return
...I'd be in favor of deprecated
no-misused-promises
in favor of usingno-unnecessary-condition
+strict-void-return
. The only benefit I can think of forno-misused-promises
would be projects that want to only apply the checks for Promises... Maybe these two rules could each be given some kind of "only check Promises" option?Also of note is that
no-misused-promises
'scheckVoidReturns
is pretty configurable. Maybe, if this rule is to replaceno-misused-promises
, it'd be useful to have each of those configurable options? Or, on the other hand, maybe those options are holdovers that real-world don't generally use? Investigation needed. I think those options can be a followup & shouldn't block this PR.What do you think?
Also cc: @typescript-eslint/triage-team in general, and @kirkwaiblinger + @alythobani from #8765.
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.
Yeah the only thing left I think would be the
checksSpreads
option:I do agree it could make sense to replace
checksVoidReturn
withstrict-void-return
. Although there may be tradeoffs in terms of eng effort and/or UX complexity if we wanted to retain all the configurability on top of having anonlyChecksPromises
option.As for
checksConditionals
, I actually just found microsoft/TypeScript#34717 and microsoft/TypeScript#39175—looks likecheckConditionals
has been covered by TypeScript for a couple years now :)Yeah e.g. one example I've seen when looking into this topic (void function assignability), is using
push
withforEach
:It's possible some users would prefer to only check Promises so they can still use shorthands like the above without linter errors (and/or just mainly care about forgetting to
await
Promises), in which case anonlyChecksPromises
option would be useful if we did replacechecksVoidReturn
withstrict-void-return
.It looks like #4619 was originally the impetus for adding the options (#4623); and based on the thread it looks like there are at least a few people who find the configurability you added very helpful!
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.
Maybe a lame response, but is there a compelling reason not to land this first, then consider the no-misused-promises deprecation, and which options we might need to port or create in order to do so, afterwards?
Just thinking, deprecating no-misused-promises might have some strings attached, such as some nontrivial updating of the docs in no-floating-promises that explain how to lint against promise antipatterns outside of ExpressionStatements.
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.
Agreed with landing this first, then considering a deprecation as a followup.
In fact, this rule is pretty big and scary. We don't really have a process for declaring rules as "canary" or "experimental". #8676 is the closest we have to a feature request. Maybe we should set a precedent?
(I don't think this PR should be blocked on that)