@@ -10,6 +10,7 @@ import {
10
10
isAwaitKeyword ,
11
11
isTypeAnyType ,
12
12
isTypeUnknownType ,
13
+ nullThrows ,
13
14
} from '../util' ;
14
15
import { getOperatorPrecedence } from '../util/getOperatorPrecedence' ;
15
16
@@ -41,6 +42,10 @@ export default createRule({
41
42
'Returning an awaited promise is not allowed in this context.' ,
42
43
requiredPromiseAwait :
43
44
'Returning an awaited promise is required in this context.' ,
45
+ requiredPromiseAwaitSuggestion :
46
+ 'Add `await` before the expression. Use caution as this may impact control flow.' ,
47
+ disallowedPromiseAwaitSuggestion :
48
+ 'Remove `await` before the expression. Use caution as this may impact control flow.' ,
44
49
} ,
45
50
schema : [
46
51
{
@@ -68,64 +73,90 @@ export default createRule({
68
73
scopeInfoStack . pop ( ) ;
69
74
}
70
75
71
- function inTry ( node : ts . Node ) : boolean {
72
- let ancestor = node . parent as ts . Node | undefined ;
73
-
74
- while ( ancestor && ! ts . isFunctionLike ( ancestor ) ) {
75
- if ( ts . isTryStatement ( ancestor ) ) {
76
- return true ;
77
- }
78
-
79
- ancestor = ancestor . parent ;
76
+ /**
77
+ * Tests whether a node is inside of an explicit error handling context
78
+ * (try/catch/finally) in a way that throwing an exception will have an
79
+ * impact on the program's control flow.
80
+ */
81
+ function affectsExplicitErrorHandling ( node : ts . Node ) : boolean {
82
+ // If an error-handling block is followed by another error-handling block,
83
+ // control flow is affected by whether promises in it are awaited or not.
84
+ // Otherwise, we need to check recursively for nested try statements until
85
+ // we get to the top level of a function or the program. If by then,
86
+ // there's no offending error-handling blocks, it doesn't affect control
87
+ // flow.
88
+ const tryAncestorResult = findContainingTryStatement ( node ) ;
89
+ if ( tryAncestorResult == null ) {
90
+ return false ;
80
91
}
81
92
82
- return false ;
83
- }
84
-
85
- function inCatch ( node : ts . Node ) : boolean {
86
- let ancestor = node . parent as ts . Node | undefined ;
93
+ const { tryStatement, block } = tryAncestorResult ;
87
94
88
- while ( ancestor && ! ts . isFunctionLike ( ancestor ) ) {
89
- if ( ts . isCatchClause ( ancestor ) ) {
95
+ switch ( block ) {
96
+ case 'try' :
97
+ // Try blocks are always followed by either a catch or finally,
98
+ // so exceptions thrown here always affect control flow.
90
99
return true ;
91
- }
92
-
93
- ancestor = ancestor . parent ;
94
- }
95
-
96
- return false ;
97
- }
98
-
99
- function isReturnPromiseInFinally ( node : ts . Node ) : boolean {
100
- let ancestor = node . parent as ts . Node | undefined ;
100
+ case 'catch' :
101
+ // Exceptions thrown in catch blocks followed by a finally block affect
102
+ // control flow.
103
+ if ( tryStatement . finallyBlock != null ) {
104
+ return true ;
105
+ }
101
106
102
- while ( ancestor && ! ts . isFunctionLike ( ancestor ) ) {
103
- if (
104
- ts . isTryStatement ( ancestor . parent ) &&
105
- ts . isBlock ( ancestor ) &&
106
- ancestor . parent . end === ancestor . end
107
- ) {
108
- return true ;
107
+ // Otherwise recurse.
108
+ return affectsExplicitErrorHandling ( tryStatement ) ;
109
+ case 'finally' :
110
+ return affectsExplicitErrorHandling ( tryStatement ) ;
111
+ default : {
112
+ const __never : never = block ;
113
+ throw new Error ( `Unexpected block type: ${ String ( __never ) } ` ) ;
109
114
}
110
- ancestor = ancestor . parent ;
111
115
}
116
+ }
112
117
113
- return false ;
118
+ interface FindContainingTryStatementResult {
119
+ tryStatement : ts . TryStatement ;
120
+ block : 'try' | 'catch' | 'finally' ;
114
121
}
115
122
116
- function hasFinallyBlock ( node : ts . Node ) : boolean {
123
+ /**
124
+ * A try _statement_ is the whole thing that encompasses try block,
125
+ * catch clause, and finally block. This function finds the nearest
126
+ * enclosing try statement (if present) for a given node, and reports which
127
+ * part of the try statement the node is in.
128
+ */
129
+ function findContainingTryStatement (
130
+ node : ts . Node ,
131
+ ) : FindContainingTryStatementResult | undefined {
132
+ let child = node ;
117
133
let ancestor = node . parent as ts . Node | undefined ;
118
134
119
135
while ( ancestor && ! ts . isFunctionLike ( ancestor ) ) {
120
136
if ( ts . isTryStatement ( ancestor ) ) {
121
- return ! ! ancestor . finallyBlock ;
137
+ let block : 'try' | 'catch' | 'finally' | undefined ;
138
+ if ( child === ancestor . tryBlock ) {
139
+ block = 'try' ;
140
+ } else if ( child === ancestor . catchClause ) {
141
+ block = 'catch' ;
142
+ } else if ( child === ancestor . finallyBlock ) {
143
+ block = 'finally' ;
144
+ }
145
+
146
+ return {
147
+ tryStatement : ancestor ,
148
+ block : nullThrows (
149
+ block ,
150
+ 'Child of a try statement must be a try block, catch clause, or finally block' ,
151
+ ) ,
152
+ } ;
122
153
}
154
+ child = ancestor ;
123
155
ancestor = ancestor . parent ;
124
156
}
125
- return false ;
126
- }
127
157
128
- // function findTokensToRemove()
158
+ return undefined ;
159
+ }
129
160
130
161
function removeAwait (
131
162
fixer : TSESLint . RuleFixer ,
@@ -202,33 +233,35 @@ export default createRule({
202
233
if ( isAwait && ! isThenable ) {
203
234
// any/unknown could be thenable; do not auto-fix
204
235
const useAutoFix = ! ( isTypeAnyType ( type ) || isTypeUnknownType ( type ) ) ;
205
- const fix = ( fixer : TSESLint . RuleFixer ) : TSESLint . RuleFix | null =>
206
- removeAwait ( fixer , node ) ;
207
236
208
237
context . report ( {
209
238
messageId : 'nonPromiseAwait' ,
210
239
node,
211
- ...( useAutoFix
212
- ? { fix }
213
- : {
214
- suggest : [
215
- {
216
- messageId : 'nonPromiseAwait' ,
217
- fix,
218
- } ,
219
- ] ,
220
- } ) ,
240
+ ...fixOrSuggest ( useAutoFix , {
241
+ messageId : 'nonPromiseAwait' ,
242
+ fix : fixer => removeAwait ( fixer , node ) ,
243
+ } ) ,
221
244
} ) ;
222
245
return ;
223
246
}
224
247
248
+ const affectsErrorHandling = affectsExplicitErrorHandling ( expression ) ;
249
+ const useAutoFix = ! affectsErrorHandling ;
250
+
225
251
if ( option === 'always' ) {
226
252
if ( ! isAwait && isThenable ) {
227
253
context . report ( {
228
254
messageId : 'requiredPromiseAwait' ,
229
255
node,
230
- fix : fixer =>
231
- insertAwait ( fixer , node , isHigherPrecedenceThanAwait ( expression ) ) ,
256
+ ...fixOrSuggest ( useAutoFix , {
257
+ messageId : 'requiredPromiseAwaitSuggestion' ,
258
+ fix : fixer =>
259
+ insertAwait (
260
+ fixer ,
261
+ node ,
262
+ isHigherPrecedenceThanAwait ( expression ) ,
263
+ ) ,
264
+ } ) ,
232
265
} ) ;
233
266
}
234
267
@@ -240,35 +273,39 @@ export default createRule({
240
273
context . report ( {
241
274
messageId : 'disallowedPromiseAwait' ,
242
275
node,
243
- fix : fixer => removeAwait ( fixer , node ) ,
276
+ ...fixOrSuggest ( useAutoFix , {
277
+ messageId : 'disallowedPromiseAwaitSuggestion' ,
278
+ fix : fixer => removeAwait ( fixer , node ) ,
279
+ } ) ,
244
280
} ) ;
245
281
}
246
282
247
283
return ;
248
284
}
249
285
250
286
if ( option === 'in-try-catch' ) {
251
- const isInTryCatch = inTry ( expression ) || inCatch ( expression ) ;
252
- if ( isAwait && ! isInTryCatch ) {
287
+ if ( isAwait && ! affectsErrorHandling ) {
253
288
context . report ( {
254
289
messageId : 'disallowedPromiseAwait' ,
255
290
node,
256
- fix : fixer => removeAwait ( fixer , node ) ,
291
+ ...fixOrSuggest ( useAutoFix , {
292
+ messageId : 'disallowedPromiseAwaitSuggestion' ,
293
+ fix : fixer => removeAwait ( fixer , node ) ,
294
+ } ) ,
257
295
} ) ;
258
- } else if ( ! isAwait && isInTryCatch ) {
259
- if ( inCatch ( expression ) && ! hasFinallyBlock ( expression ) ) {
260
- return ;
261
- }
262
-
263
- if ( isReturnPromiseInFinally ( expression ) ) {
264
- return ;
265
- }
266
-
296
+ } else if ( ! isAwait && affectsErrorHandling ) {
267
297
context . report ( {
268
298
messageId : 'requiredPromiseAwait' ,
269
299
node,
270
- fix : fixer =>
271
- insertAwait ( fixer , node , isHigherPrecedenceThanAwait ( expression ) ) ,
300
+ ...fixOrSuggest ( useAutoFix , {
301
+ messageId : 'requiredPromiseAwaitSuggestion' ,
302
+ fix : fixer =>
303
+ insertAwait (
304
+ fixer ,
305
+ node ,
306
+ isHigherPrecedenceThanAwait ( expression ) ,
307
+ ) ,
308
+ } ) ,
272
309
} ) ;
273
310
}
274
311
@@ -321,3 +358,12 @@ export default createRule({
321
358
} ;
322
359
} ,
323
360
} ) ;
361
+
362
+ function fixOrSuggest < MessageId extends string > (
363
+ useFix : boolean ,
364
+ suggestion : TSESLint . SuggestionReportDescriptor < MessageId > ,
365
+ ) :
366
+ | { fix : TSESLint . ReportFixFunction }
367
+ | { suggest : TSESLint . SuggestionReportDescriptor < MessageId > [ ] } {
368
+ return useFix ? { fix : suggestion . fix } : { suggest : [ suggestion ] } ;
369
+ }
0 commit comments