Skip to content

Commit de1e5ce

Browse files
authored
feat(eslint-plugin): [promise-function-async] check for promises in implicit return types (#6330)
* [promise-function-async] Only allow unions in explicit return types When we return a union containing a promise from a function implicitly, it's often a mistake. This commit makes it so if the return type is explicit, any `Promise` in the return type (whether it's part of a union or not) will flag the function. If it is intentional, make the return type explicit. Fixes #6329 * Refrain from renaming the type-util, instead add an optional fourth param.
1 parent 9b06e1a commit de1e5ce

File tree

4 files changed

+68
-7
lines changed

4 files changed

+68
-7
lines changed

packages/eslint-plugin/docs/rules/promise-function-async.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ Ensures that each function is only capable of:
1111
- returning a rejected promise, or
1212
- throwing an Error object.
1313

14-
In contrast, non-`async` `Promise` - returning functions are technically capable of either.
14+
In contrast, non-`async`, `Promise`-returning functions are technically capable of either.
1515
Code that handles the results of those functions will often need to handle both cases, which can get complex.
1616
This rule's practice removes a requirement for creating code to handle both cases.
1717

18+
> When functions return unions of `Promise` and non-`Promise` types implicitly, it is usually a mistake—this rule flags those cases. If it is intentional, make the return type explicitly to allow the rule to pass.
19+
1820
## Examples
1921

2022
Examples of code for this rule
@@ -29,6 +31,10 @@ const arrowFunctionReturnsPromise = () => Promise.resolve('value');
2931
function functionReturnsPromise() {
3032
return Promise.resolve('value');
3133
}
34+
35+
function functionReturnsUnionWithPromiseImplicitly(p: boolean) {
36+
return p ? 'value' : Promise.resolve('value');
37+
}
3238
```
3339

3440
### ✅ Correct
@@ -39,4 +45,15 @@ const arrowFunctionReturnsPromise = async () => Promise.resolve('value');
3945
async function functionReturnsPromise() {
4046
return Promise.resolve('value');
4147
}
48+
49+
// An explicit return type that is not Promise means this function cannot be made async, so it is ignored by the rule
50+
function functionReturnsUnionWithPromiseExplicitly(
51+
p: boolean,
52+
): string | Promise<string> {
53+
return p ? 'value' : Promise.resolve('value');
54+
}
55+
56+
async function functionReturnsUnionWithPromiseImplicitly(p: boolean) {
57+
return p ? 'value' : Promise.resolve('value');
58+
}
4259
```

packages/eslint-plugin/src/rules/promise-function-async.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ export default util.createRule<Options, MessageIds>({
115115
returnType,
116116
allowAny!,
117117
allAllowedPromiseNames,
118+
// If no return type is explicitly set, we check if any parts of the return type match a Promise (instead of requiring all to match).
119+
node.returnType == null,
118120
)
119121
) {
120122
// Return type is not a promise

packages/eslint-plugin/tests/rules/promise-function-async.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,23 @@ abstract class Test {
165165
}
166166
`,
167167
},
168+
`
169+
function promiseInUnionWithExplicitReturnType(
170+
p: boolean,
171+
): Promise<number> | number {
172+
return p ? Promise.resolve(5) : 5;
173+
}
174+
`,
175+
`
176+
function explicitReturnWithPromiseInUnion(): Promise<number> | number {
177+
return 5;
178+
}
179+
`,
180+
`
181+
async function asyncFunctionReturningUnion(p: boolean) {
182+
return p ? Promise.resolve(5) : 5;
183+
}
184+
`,
168185
],
169186
invalid: [
170187
{
@@ -752,5 +769,22 @@ const foo = {
752769
},
753770
],
754771
},
772+
{
773+
code: `
774+
function promiseInUnionWithoutExplicitReturnType(p: boolean) {
775+
return p ? Promise.resolve(5) : 5;
776+
}
777+
`,
778+
errors: [
779+
{
780+
messageId,
781+
},
782+
],
783+
output: `
784+
async function promiseInUnionWithoutExplicitReturnType(p: boolean) {
785+
return p ? Promise.resolve(5) : 5;
786+
}
787+
`,
788+
},
755789
],
756790
});

packages/type-utils/src/containsAllTypesByName.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { isTypeFlagSet } from './typeFlagUtils';
55

66
/**
77
* @param type Type being checked by name.
8+
* @param allowAny Whether to consider `any` and `unknown` to match.
89
* @param allowedNames Symbol names checking on the type.
9-
* @returns Whether the type is, extends, or contains all of the allowed names.
10+
* @param matchAnyInstead Whether to instead just check if any parts match, rather than all parts.
11+
* @returns Whether the type is, extends, or contains the allowed names (or all matches the allowed names, if mustMatchAll is true).
1012
*/
1113
export function containsAllTypesByName(
1214
type: ts.Type,
1315
allowAny: boolean,
1416
allowedNames: Set<string>,
17+
matchAnyInstead = false,
1518
): boolean {
1619
if (isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
1720
return !allowAny;
@@ -26,16 +29,21 @@ export function containsAllTypesByName(
2629
return true;
2730
}
2831

32+
const predicate = (t: ts.Type): boolean =>
33+
containsAllTypesByName(t, allowAny, allowedNames, matchAnyInstead);
34+
2935
if (isUnionOrIntersectionType(type)) {
30-
return type.types.every(t =>
31-
containsAllTypesByName(t, allowAny, allowedNames),
32-
);
36+
return matchAnyInstead
37+
? type.types.some(predicate)
38+
: type.types.every(predicate);
3339
}
3440

3541
const bases = type.getBaseTypes();
42+
3643
return (
3744
typeof bases !== 'undefined' &&
38-
bases.length > 0 &&
39-
bases.every(t => containsAllTypesByName(t, allowAny, allowedNames))
45+
(matchAnyInstead
46+
? bases.some(predicate)
47+
: bases.length > 0 && bases.every(predicate))
4048
);
4149
}

0 commit comments

Comments
 (0)