@@ -15,9 +15,21 @@ type Options = [
15
15
16
16
type MessageId =
17
17
| 'floating'
18
+ | 'floatingVoid'
19
+ | 'floatingUselessRejectionHandler'
20
+ | 'floatingUselessRejectionHandlerVoid'
18
21
| '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.' ;
21
33
22
34
export default util . createRule < Options , MessageId > ( {
23
35
name : 'no-floating-promises' ,
@@ -30,13 +42,14 @@ export default util.createRule<Options, MessageId>({
30
42
} ,
31
43
hasSuggestions : true ,
32
44
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 ,
35
46
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 ,
39
48
floatingFixVoid : 'Add void operator to ignore.' ,
49
+ floatingUselessRejectionHandler :
50
+ messageBase + ' ' + messageRejectionHandler ,
51
+ floatingUselessRejectionHandlerVoid :
52
+ messageBaseVoid + ' ' + messageRejectionHandler ,
40
53
} ,
41
54
schema : [
42
55
{
@@ -48,7 +61,7 @@ export default util.createRule<Options, MessageId>({
48
61
} ,
49
62
ignoreIIFE : {
50
63
description :
51
- 'Whether to ignore async IIFEs (Immediately Invocated Function Expressions).' ,
64
+ 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).' ,
52
65
type : 'boolean' ,
53
66
} ,
54
67
} ,
@@ -80,11 +93,18 @@ export default util.createRule<Options, MessageId>({
80
93
expression = expression . expression ;
81
94
}
82
95
83
- if ( isUnhandledPromise ( checker , expression ) ) {
96
+ const { isUnhandled, nonFunctionHandler } = isUnhandledPromise (
97
+ checker ,
98
+ expression ,
99
+ ) ;
100
+
101
+ if ( isUnhandled ) {
84
102
if ( options . ignoreVoid ) {
85
103
context . report ( {
86
104
node,
87
- messageId : 'floatingVoid' ,
105
+ messageId : nonFunctionHandler
106
+ ? 'floatingUselessRejectionHandlerVoid'
107
+ : 'floatingVoid' ,
88
108
suggest : [
89
109
{
90
110
messageId : 'floatingFixVoid' ,
@@ -110,7 +130,9 @@ export default util.createRule<Options, MessageId>({
110
130
} else {
111
131
context . report ( {
112
132
node,
113
- messageId : 'floating' ,
133
+ messageId : nonFunctionHandler
134
+ ? 'floatingUselessRejectionHandler'
135
+ : 'floating' ,
114
136
suggest : [
115
137
{
116
138
messageId : 'floatingFixAwait' ,
@@ -168,16 +190,31 @@ export default util.createRule<Options, MessageId>({
168
190
) ;
169
191
}
170
192
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
+
171
204
function isUnhandledPromise (
172
205
checker : ts . TypeChecker ,
173
206
node : TSESTree . Node ,
174
- ) : boolean {
207
+ ) : { isUnhandled : boolean ; nonFunctionHandler ?: boolean } {
175
208
// First, check expressions whose resulting types may not be promise-like
176
209
if ( node . type === AST_NODE_TYPES . SequenceExpression ) {
177
210
// Any child in a comma expression could return a potentially unhandled
178
211
// promise, so we check them all regardless of whether the final returned
179
212
// 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
+ ) ;
181
218
}
182
219
183
220
if (
@@ -192,24 +229,45 @@ export default util.createRule<Options, MessageId>({
192
229
193
230
// Check the type. At this point it can't be unhandled if it isn't a promise
194
231
if ( ! isPromiseLike ( checker , services . esTreeNodeToTSNodeMap . get ( node ) ) ) {
195
- return false ;
232
+ return { isUnhandled : false } ;
196
233
}
197
234
198
235
if ( node . type === AST_NODE_TYPES . CallExpression ) {
199
236
// If the outer expression is a call, it must be either a `.then()` or
200
237
// `.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 } ;
206
262
} else if ( node . type === AST_NODE_TYPES . ConditionalExpression ) {
207
263
// We must be getting the promise-like value from one of the branches of the
208
264
// 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
+ }
213
271
} else if (
214
272
node . type === AST_NODE_TYPES . MemberExpression ||
215
273
node . type === AST_NODE_TYPES . Identifier ||
@@ -218,18 +276,20 @@ export default util.createRule<Options, MessageId>({
218
276
// If it is just a property access chain or a `new` call (e.g. `foo.bar` or
219
277
// `new Promise()`), the promise is not handled because it doesn't have the
220
278
// necessary then/catch call at the end of the chain.
221
- return true ;
279
+ return { isUnhandled : true } ;
222
280
} 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
+ }
227
287
}
228
288
229
289
// We conservatively return false for all other types of expressions because
230
290
// we don't want to accidentally fail if the promise is handled internally but
231
291
// we just can't tell.
232
- return false ;
292
+ return { isUnhandled : false } ;
233
293
}
234
294
} ,
235
295
} ) ;
@@ -291,26 +351,34 @@ function isFunctionParam(
291
351
return false ;
292
352
}
293
353
294
- function isPromiseCatchCallWithHandler (
354
+ function getRejectionHandlerFromCatchCall (
295
355
expression : TSESTree . CallExpression ,
296
- ) : boolean {
297
- return (
356
+ ) : TSESTree . CallExpressionArgument | undefined {
357
+ if (
298
358
expression . callee . type === AST_NODE_TYPES . MemberExpression &&
299
359
expression . callee . property . type === AST_NODE_TYPES . Identifier &&
300
360
expression . callee . property . name === 'catch' &&
301
361
expression . arguments . length >= 1
302
- ) ;
362
+ ) {
363
+ return expression . arguments [ 0 ] ;
364
+ } else {
365
+ return undefined ;
366
+ }
303
367
}
304
368
305
- function isPromiseThenCallWithRejectionHandler (
369
+ function getRejectionHandlerFromThenCall (
306
370
expression : TSESTree . CallExpression ,
307
- ) : boolean {
308
- return (
371
+ ) : TSESTree . CallExpressionArgument | undefined {
372
+ if (
309
373
expression . callee . type === AST_NODE_TYPES . MemberExpression &&
310
374
expression . callee . property . type === AST_NODE_TYPES . Identifier &&
311
375
expression . callee . property . name === 'then' &&
312
376
expression . arguments . length >= 2
313
- ) ;
377
+ ) {
378
+ return expression . arguments [ 1 ] ;
379
+ } else {
380
+ return undefined ;
381
+ }
314
382
}
315
383
316
384
function isPromiseFinallyCallWithHandler (
0 commit comments