@@ -15,9 +15,21 @@ type Options = [
1515
1616type 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
2234export 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
316384function isPromiseFinallyCallWithHandler (
0 commit comments