Skip to content

Commit 61e6385

Browse files
princjefbradzacher
authored andcommitted
feat(eslint-plugin): add new rule no-floating-promises (typescript-eslint#495)
1 parent 2c557f1 commit 61e6385

File tree

6 files changed

+770
-2
lines changed

6 files changed

+770
-2
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
146146
| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | | |
147147
| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | |
148148
| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | |
149+
| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately. | | | :thought_balloon: |
149150
| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | | | :thought_balloon: |
150151
| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | |
151152
| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallows magic numbers | | | |

packages/eslint-plugin/ROADMAP.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Roadmap
1+
# Roadmap
22

33
✅ = done<br>
44
🌟 = in ESLint core<br>
@@ -60,7 +60,7 @@
6060
| [`no-dynamic-delete`] | 🛑 | N/A |
6161
| [`no-empty`] | 🌟 | [`no-empty`][no-empty] |
6262
| [`no-eval`] | 🌟 | [`no-eval`][no-eval] |
63-
| [`no-floating-promises`] | 🛑 | N/A ([relevant plugin][plugin:promise]) |
63+
| [`no-floating-promises`] | | [`@typescript-eslint/no-floating-promises`] |
6464
| [`no-for-in-array`] || [`@typescript-eslint/no-for-in-array`] |
6565
| [`no-implicit-dependencies`] | 🔌 | [`import/no-extraneous-dependencies`] |
6666
| [`no-inferred-empty-object-type`] | 🛑 | N/A |
@@ -612,6 +612,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
612612
[`@typescript-eslint/no-for-in-array`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-for-in-array.md
613613
[`@typescript-eslint/no-unnecessary-qualifier`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md
614614
[`@typescript-eslint/semi`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/semi.md
615+
[`@typescript-eslint/no-floating-promises`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-floating-promises.md
615616

616617
<!-- eslint-plugin-import -->
617618

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Requires Promise-like values to be handled appropriately (no-floating-promises)
2+
3+
This rule forbids usage of Promise-like values in statements without handling
4+
their errors appropriately. Unhandled promises can cause several issues, such
5+
as improperly sequenced operations, ignored Promise rejections and more. Valid
6+
ways of handling a Promise-valued statement include `await`ing, returning, and
7+
either calling `.then()` with two arguments or `.catch()` with one argument.
8+
9+
## Rule Details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```ts
14+
const promise = new Promise((resolve, reject) => resolve('value'));
15+
promise;
16+
17+
async function returnsPromise() {
18+
return 'value';
19+
}
20+
returnsPromise().then(() => {});
21+
22+
Promise.reject('value').catch();
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```ts
28+
const promise = new Promise((resolve, reject) => resolve('value'));
29+
await promise;
30+
31+
async function returnsPromise() {
32+
return 'value';
33+
}
34+
returnsPromise().then(() => {}, () => {});
35+
36+
Promise.reject('value').catch(() => {});
37+
```
38+
39+
## When Not To Use It
40+
41+
If you do not use Promise-like values in your codebase or want to allow them to
42+
remain unhandled.
43+
44+
## Related to
45+
46+
- Tslint: ['no-floating-promises'](https://palantir.github.io/tslint/rules/no-floating-promises/)

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import noEmptyInterface from './no-empty-interface';
2020
import noExplicitAny from './no-explicit-any';
2121
import noExtraParens from './no-extra-parens';
2222
import noExtraneousClass from './no-extraneous-class';
23+
import noFloatingPromises from './no-floating-promises';
2324
import noForInArray from './no-for-in-array';
2425
import noInferrableTypes from './no-inferrable-types';
2526
import noMagicNumbers from './no-magic-numbers';
@@ -76,6 +77,7 @@ export default {
7677
'no-explicit-any': noExplicitAny,
7778
'no-extra-parens': noExtraParens,
7879
'no-extraneous-class': noExtraneousClass,
80+
'no-floating-promises': noFloatingPromises,
7981
'no-for-in-array': noForInArray,
8082
'no-inferrable-types': noInferrableTypes,
8183
'no-magic-numbers': noMagicNumbers,
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as tsutils from 'tsutils';
2+
import * as ts from 'typescript';
3+
4+
import * as util from '../util';
5+
6+
export default util.createRule({
7+
name: 'no-floating-promises',
8+
meta: {
9+
docs: {
10+
description: 'Requires Promise-like values to be handled appropriately.',
11+
category: 'Best Practices',
12+
recommended: false,
13+
},
14+
messages: {
15+
floating: 'Promises must be handled appropriately',
16+
},
17+
schema: [],
18+
type: 'problem',
19+
},
20+
defaultOptions: [],
21+
22+
create(context) {
23+
const parserServices = util.getParserServices(context);
24+
const checker = parserServices.program.getTypeChecker();
25+
26+
return {
27+
ExpressionStatement(node) {
28+
const { expression } = parserServices.esTreeNodeToTSNodeMap.get(
29+
node,
30+
) as ts.ExpressionStatement;
31+
32+
if (isUnhandledPromise(checker, expression)) {
33+
context.report({
34+
messageId: 'floating',
35+
node,
36+
});
37+
}
38+
},
39+
};
40+
},
41+
});
42+
43+
function isUnhandledPromise(checker: ts.TypeChecker, node: ts.Node): boolean {
44+
// First, check expressions whose resulting types may not be promise-like
45+
if (
46+
ts.isBinaryExpression(node) &&
47+
node.operatorToken.kind === ts.SyntaxKind.CommaToken
48+
) {
49+
// Any child in a comma expression could return a potentially unhandled
50+
// promise, so we check them all regardless of whether the final returned
51+
// value is promise-like.
52+
return (
53+
isUnhandledPromise(checker, node.left) ||
54+
isUnhandledPromise(checker, node.right)
55+
);
56+
} else if (ts.isVoidExpression(node)) {
57+
// Similarly, a `void` expression always returns undefined, so we need to
58+
// see what's inside it without checking the type of the overall expression.
59+
return isUnhandledPromise(checker, node.expression);
60+
}
61+
62+
// Check the type. At this point it can't be unhandled if it isn't a promise
63+
if (!isPromiseLike(checker, node)) {
64+
return false;
65+
}
66+
67+
if (ts.isCallExpression(node)) {
68+
// If the outer expression is a call, it must be either a `.then()` or
69+
// `.catch()` that handles the promise.
70+
return (
71+
!isPromiseCatchCallWithHandler(node) &&
72+
!isPromiseThenCallWithRejectionHandler(node)
73+
);
74+
} else if (ts.isConditionalExpression(node)) {
75+
// We must be getting the promise-like value from one of the branches of the
76+
// ternary. Check them directly.
77+
return (
78+
isUnhandledPromise(checker, node.whenFalse) ||
79+
isUnhandledPromise(checker, node.whenTrue)
80+
);
81+
} else if (
82+
ts.isPropertyAccessExpression(node) ||
83+
ts.isIdentifier(node) ||
84+
ts.isNewExpression(node)
85+
) {
86+
// If it is just a property access chain or a `new` call (e.g. `foo.bar` or
87+
// `new Promise()`), the promise is not handled because it doesn't have the
88+
// necessary then/catch call at the end of the chain.
89+
return true;
90+
}
91+
92+
// We conservatively return false for all other types of expressions because
93+
// we don't want to accidentally fail if the promise is handled internally but
94+
// we just can't tell.
95+
return false;
96+
}
97+
98+
// Modified from tsutils.isThenable() to only consider thenables which can be
99+
// rejected/caught via a second parameter. Original source (MIT licensed):
100+
//
101+
// https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125
102+
function isPromiseLike(checker: ts.TypeChecker, node: ts.Node): boolean {
103+
const type = checker.getTypeAtLocation(node);
104+
for (const ty of tsutils.unionTypeParts(checker.getApparentType(type))) {
105+
const then = ty.getProperty('then');
106+
if (then === undefined) {
107+
continue;
108+
}
109+
110+
const thenType = checker.getTypeOfSymbolAtLocation(then, node);
111+
if (
112+
hasMatchingSignature(
113+
thenType,
114+
signature =>
115+
signature.parameters.length >= 2 &&
116+
isFunctionParam(checker, signature.parameters[0], node) &&
117+
isFunctionParam(checker, signature.parameters[1], node),
118+
)
119+
) {
120+
return true;
121+
}
122+
}
123+
return false;
124+
}
125+
126+
function hasMatchingSignature(
127+
type: ts.Type,
128+
matcher: (signature: ts.Signature) => boolean,
129+
): boolean {
130+
for (const t of tsutils.unionTypeParts(type)) {
131+
if (t.getCallSignatures().some(matcher)) {
132+
return true;
133+
}
134+
}
135+
136+
return false;
137+
}
138+
139+
function isFunctionParam(
140+
checker: ts.TypeChecker,
141+
param: ts.Symbol,
142+
node: ts.Node,
143+
): boolean {
144+
const type: ts.Type | undefined = checker.getApparentType(
145+
checker.getTypeOfSymbolAtLocation(param, node),
146+
);
147+
for (const t of tsutils.unionTypeParts(type)) {
148+
if (t.getCallSignatures().length !== 0) {
149+
return true;
150+
}
151+
}
152+
return false;
153+
}
154+
155+
function isPromiseCatchCallWithHandler(expression: ts.CallExpression): boolean {
156+
return (
157+
tsutils.isPropertyAccessExpression(expression.expression) &&
158+
expression.expression.name.text === 'catch' &&
159+
expression.arguments.length >= 1
160+
);
161+
}
162+
163+
function isPromiseThenCallWithRejectionHandler(
164+
expression: ts.CallExpression,
165+
): boolean {
166+
return (
167+
tsutils.isPropertyAccessExpression(expression.expression) &&
168+
expression.expression.name.text === 'then' &&
169+
expression.arguments.length >= 2
170+
);
171+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy