Skip to content

Commit 606a52c

Browse files
fix(eslint-plugin): [no-floating-promises] false negative calling .then with second argument undefined (typescript-eslint#6881)
* fix(eslint-plugin): [no-floating-promises] False negative calling .then with second argument undefined (Issue typescript-eslint#6850) * use getTypeAtLocation correctly * clean up duplicate * correct mistake in IIFE abbreviation * purge the word invocated from the codebase
1 parent d740de3 commit 606a52c

File tree

4 files changed

+274
-48
lines changed

4 files changed

+274
-48
lines changed

packages/eslint-plugin/docs/rules/no-floating-promises.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ With this option set to `true`, and if you are using `no-void`, you should turn
8383

8484
### `ignoreIIFE`
8585

86-
This allows you to skip checking of async IIFEs (Immediately Invocated function Expressions).
86+
This allows you to skip checking of async IIFEs (Immediately Invoked function Expressions).
8787

8888
Examples of **correct** code for this rule with `{ ignoreIIFE: true }`:
8989

packages/eslint-plugin/src/rules/no-floating-promises.ts

Lines changed: 105 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@ type Options = [
1515

1616
type MessageId =
1717
| 'floating'
18+
| 'floatingVoid'
19+
| 'floatingUselessRejectionHandler'
20+
| 'floatingUselessRejectionHandlerVoid'
1821
| 'floatingFixAwait'
19-
| 'floatingFixVoid'
20-
| 'floatingVoid';
22+
| 'floatingFixVoid';
23+
24+
const messageBase =
25+
'Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.';
26+
27+
const messageBaseVoid =
28+
'Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler' +
29+
' or be explicitly marked as ignored with the `void` operator.';
30+
31+
const messageRejectionHandler =
32+
'A rejection handler that is not a function will be ignored.';
2133

2234
export default util.createRule<Options, MessageId>({
2335
name: 'no-floating-promises',
@@ -30,13 +42,14 @@ export default util.createRule<Options, MessageId>({
3042
},
3143
hasSuggestions: true,
3244
messages: {
33-
floating:
34-
'Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.',
45+
floating: messageBase,
3546
floatingFixAwait: 'Add await operator.',
36-
floatingVoid:
37-
'Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler' +
38-
' or be explicitly marked as ignored with the `void` operator.',
47+
floatingVoid: messageBaseVoid,
3948
floatingFixVoid: 'Add void operator to ignore.',
49+
floatingUselessRejectionHandler:
50+
messageBase + ' ' + messageRejectionHandler,
51+
floatingUselessRejectionHandlerVoid:
52+
messageBaseVoid + ' ' + messageRejectionHandler,
4053
},
4154
schema: [
4255
{
@@ -48,7 +61,7 @@ export default util.createRule<Options, MessageId>({
4861
},
4962
ignoreIIFE: {
5063
description:
51-
'Whether to ignore async IIFEs (Immediately Invocated Function Expressions).',
64+
'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).',
5265
type: 'boolean',
5366
},
5467
},
@@ -80,11 +93,18 @@ export default util.createRule<Options, MessageId>({
8093
expression = expression.expression;
8194
}
8295

83-
if (isUnhandledPromise(checker, expression)) {
96+
const { isUnhandled, nonFunctionHandler } = isUnhandledPromise(
97+
checker,
98+
expression,
99+
);
100+
101+
if (isUnhandled) {
84102
if (options.ignoreVoid) {
85103
context.report({
86104
node,
87-
messageId: 'floatingVoid',
105+
messageId: nonFunctionHandler
106+
? 'floatingUselessRejectionHandlerVoid'
107+
: 'floatingVoid',
88108
suggest: [
89109
{
90110
messageId: 'floatingFixVoid',
@@ -110,7 +130,9 @@ export default util.createRule<Options, MessageId>({
110130
} else {
111131
context.report({
112132
node,
113-
messageId: 'floating',
133+
messageId: nonFunctionHandler
134+
? 'floatingUselessRejectionHandler'
135+
: 'floating',
114136
suggest: [
115137
{
116138
messageId: 'floatingFixAwait',
@@ -168,16 +190,31 @@ export default util.createRule<Options, MessageId>({
168190
);
169191
}
170192

193+
function isValidRejectionHandler(rejectionHandler: TSESTree.Node): boolean {
194+
return (
195+
services.program
196+
.getTypeChecker()
197+
.getTypeAtLocation(
198+
services.esTreeNodeToTSNodeMap.get(rejectionHandler),
199+
)
200+
.getCallSignatures().length > 0
201+
);
202+
}
203+
171204
function isUnhandledPromise(
172205
checker: ts.TypeChecker,
173206
node: TSESTree.Node,
174-
): boolean {
207+
): { isUnhandled: boolean; nonFunctionHandler?: boolean } {
175208
// First, check expressions whose resulting types may not be promise-like
176209
if (node.type === AST_NODE_TYPES.SequenceExpression) {
177210
// Any child in a comma expression could return a potentially unhandled
178211
// promise, so we check them all regardless of whether the final returned
179212
// value is promise-like.
180-
return node.expressions.some(item => isUnhandledPromise(checker, item));
213+
return (
214+
node.expressions
215+
.map(item => isUnhandledPromise(checker, item))
216+
.find(result => result.isUnhandled) ?? { isUnhandled: false }
217+
);
181218
}
182219

183220
if (
@@ -192,24 +229,45 @@ export default util.createRule<Options, MessageId>({
192229

193230
// Check the type. At this point it can't be unhandled if it isn't a promise
194231
if (!isPromiseLike(checker, services.esTreeNodeToTSNodeMap.get(node))) {
195-
return false;
232+
return { isUnhandled: false };
196233
}
197234

198235
if (node.type === AST_NODE_TYPES.CallExpression) {
199236
// If the outer expression is a call, it must be either a `.then()` or
200237
// `.catch()` that handles the promise.
201-
return (
202-
!isPromiseCatchCallWithHandler(node) &&
203-
!isPromiseThenCallWithRejectionHandler(node) &&
204-
!isPromiseFinallyCallWithHandler(node)
205-
);
238+
239+
const catchRejectionHandler = getRejectionHandlerFromCatchCall(node);
240+
if (catchRejectionHandler) {
241+
if (isValidRejectionHandler(catchRejectionHandler)) {
242+
return { isUnhandled: false };
243+
} else {
244+
return { isUnhandled: true, nonFunctionHandler: true };
245+
}
246+
}
247+
248+
const thenRejectionHandler = getRejectionHandlerFromThenCall(node);
249+
if (thenRejectionHandler) {
250+
if (isValidRejectionHandler(thenRejectionHandler)) {
251+
return { isUnhandled: false };
252+
} else {
253+
return { isUnhandled: true, nonFunctionHandler: true };
254+
}
255+
}
256+
257+
if (isPromiseFinallyCallWithHandler(node)) {
258+
return { isUnhandled: false };
259+
}
260+
261+
return { isUnhandled: true };
206262
} else if (node.type === AST_NODE_TYPES.ConditionalExpression) {
207263
// We must be getting the promise-like value from one of the branches of the
208264
// ternary. Check them directly.
209-
return (
210-
isUnhandledPromise(checker, node.alternate) ||
211-
isUnhandledPromise(checker, node.consequent)
212-
);
265+
const alternateResult = isUnhandledPromise(checker, node.alternate);
266+
if (alternateResult.isUnhandled) {
267+
return alternateResult;
268+
} else {
269+
return isUnhandledPromise(checker, node.consequent);
270+
}
213271
} else if (
214272
node.type === AST_NODE_TYPES.MemberExpression ||
215273
node.type === AST_NODE_TYPES.Identifier ||
@@ -218,18 +276,20 @@ export default util.createRule<Options, MessageId>({
218276
// If it is just a property access chain or a `new` call (e.g. `foo.bar` or
219277
// `new Promise()`), the promise is not handled because it doesn't have the
220278
// necessary then/catch call at the end of the chain.
221-
return true;
279+
return { isUnhandled: true };
222280
} else if (node.type === AST_NODE_TYPES.LogicalExpression) {
223-
return (
224-
isUnhandledPromise(checker, node.left) ||
225-
isUnhandledPromise(checker, node.right)
226-
);
281+
const leftResult = isUnhandledPromise(checker, node.left);
282+
if (leftResult.isUnhandled) {
283+
return leftResult;
284+
} else {
285+
return isUnhandledPromise(checker, node.right);
286+
}
227287
}
228288

229289
// We conservatively return false for all other types of expressions because
230290
// we don't want to accidentally fail if the promise is handled internally but
231291
// we just can't tell.
232-
return false;
292+
return { isUnhandled: false };
233293
}
234294
},
235295
});
@@ -291,26 +351,34 @@ function isFunctionParam(
291351
return false;
292352
}
293353

294-
function isPromiseCatchCallWithHandler(
354+
function getRejectionHandlerFromCatchCall(
295355
expression: TSESTree.CallExpression,
296-
): boolean {
297-
return (
356+
): TSESTree.CallExpressionArgument | undefined {
357+
if (
298358
expression.callee.type === AST_NODE_TYPES.MemberExpression &&
299359
expression.callee.property.type === AST_NODE_TYPES.Identifier &&
300360
expression.callee.property.name === 'catch' &&
301361
expression.arguments.length >= 1
302-
);
362+
) {
363+
return expression.arguments[0];
364+
} else {
365+
return undefined;
366+
}
303367
}
304368

305-
function isPromiseThenCallWithRejectionHandler(
369+
function getRejectionHandlerFromThenCall(
306370
expression: TSESTree.CallExpression,
307-
): boolean {
308-
return (
371+
): TSESTree.CallExpressionArgument | undefined {
372+
if (
309373
expression.callee.type === AST_NODE_TYPES.MemberExpression &&
310374
expression.callee.property.type === AST_NODE_TYPES.Identifier &&
311375
expression.callee.property.name === 'then' &&
312376
expression.arguments.length >= 2
313-
);
377+
) {
378+
return expression.arguments[1];
379+
} else {
380+
return undefined;
381+
}
314382
}
315383

316384
function isPromiseFinallyCallWithHandler(

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