Skip to content

Commit 606a52c

Browse files
fix(eslint-plugin): [no-floating-promises] false negative calling .then with second argument undefined (#6881)
* fix(eslint-plugin): [no-floating-promises] False negative calling .then with second argument undefined (Issue #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

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

+1-1
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

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

+105-37
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)