1
+ import type { Scope } from '@typescript-eslint/scope-manager' ;
1
2
import type { TSESLint , TSESTree } from '@typescript-eslint/utils' ;
2
3
import { AST_NODE_TYPES } from '@typescript-eslint/utils' ;
3
- import type {
4
- ReportDescriptor ,
5
- Scope ,
6
- } from '@typescript-eslint/utils/ts-eslint' ;
4
+ import type { ReportDescriptor } from '@typescript-eslint/utils/ts-eslint' ;
7
5
import * as tsutils from 'ts-api-utils' ;
8
6
import type * as ts from 'typescript' ;
9
7
@@ -18,7 +16,6 @@ import {
18
16
19
17
type MessageIds =
20
18
| 'useUnknown'
21
- | 'useUnknownSpreadArgs'
22
19
| 'useUnknownArrayDestructuringPattern'
23
20
| 'useUnknownObjectDestructuringPattern'
24
21
| 'addUnknownTypeAnnotationSuggestion'
@@ -27,14 +24,27 @@ type MessageIds =
27
24
| 'wrongRestTypeAnnotationSuggestion' ;
28
25
29
26
const useUnknownMessageBase =
30
- 'Prefer the safe `: unknown` for a catch callback variable.' ;
27
+ 'Prefer the safe `: unknown` for a `{{method}}`{{append}} callback variable.' ;
28
+
29
+ /**
30
+ * `x.memberName` => 'memberKey'
31
+ *
32
+ * `const mk = 'memberKey'; x[mk]` => 'memberKey'
33
+ *
34
+ * `const mk = 1234; x[mk]` => 1234
35
+ */
36
+ const getStaticMemberAccessKey = (
37
+ { computed, property } : TSESTree . MemberExpression ,
38
+ scope : Scope ,
39
+ ) : { value : unknown } | null =>
40
+ computed ? getStaticValue ( property , scope ) : { value : property . name } ;
31
41
32
42
export default createRule < [ ] , MessageIds > ( {
33
43
name : 'use-unknown-in-catch-callback-variable' ,
34
44
meta : {
35
45
docs : {
36
46
description :
37
- 'Enforce typing arguments in `.catch()` callbacks as `unknown`' ,
47
+ 'Enforce typing arguments in Promise rejection callbacks as `unknown`' ,
38
48
requiresTypeChecking : true ,
39
49
recommended : 'strict' ,
40
50
} ,
@@ -45,13 +55,10 @@ export default createRule<[], MessageIds>({
45
55
useUnknownObjectDestructuringPattern : `${
46
56
useUnknownMessageBase
47
57
} The thrown error may be nullable, or may not have the expected shape.`,
48
- useUnknownSpreadArgs : `${
49
- useUnknownMessageBase
50
- } The argument list may contain a handler that does not use \`unknown\` for the catch callback variable.`,
51
58
addUnknownTypeAnnotationSuggestion :
52
- 'Add an explicit `: unknown` type annotation to the catch variable.' ,
59
+ 'Add an explicit `: unknown` type annotation to the rejection callback variable.' ,
53
60
addUnknownRestTypeAnnotationSuggestion :
54
- 'Add an explicit `: [unknown]` type annotation to the catch rest variable.' ,
61
+ 'Add an explicit `: [unknown]` type annotation to the rejection callback rest variable.' ,
55
62
wrongTypeAnnotationSuggestion :
56
63
'Change existing type annotation to `: unknown`.' ,
57
64
wrongRestTypeAnnotationSuggestion :
@@ -65,27 +72,8 @@ export default createRule<[], MessageIds>({
65
72
defaultOptions : [ ] ,
66
73
67
74
create ( context ) {
68
- const services = getParserServices ( context ) ;
69
- const checker = services . program . getTypeChecker ( ) ;
70
-
71
- function isPromiseCatchAccess ( node : TSESTree . Expression ) : boolean {
72
- if (
73
- ! (
74
- node . type === AST_NODE_TYPES . MemberExpression &&
75
- isStaticMemberAccessOfValue ( node , 'catch' )
76
- )
77
- ) {
78
- return false ;
79
- }
80
-
81
- const objectTsNode = services . esTreeNodeToTSNodeMap . get ( node . object ) ;
82
- const tsNode = services . esTreeNodeToTSNodeMap . get ( node ) ;
83
- return tsutils . isThenableType (
84
- checker ,
85
- tsNode ,
86
- checker . getTypeAtLocation ( objectTsNode ) ,
87
- ) ;
88
- }
75
+ const { program, esTreeNodeToTSNodeMap } = getParserServices ( context ) ;
76
+ const checker = program . getTypeChecker ( ) ;
89
77
90
78
function isFlaggableHandlerType ( type : ts . Type ) : boolean {
91
79
for ( const unionPart of tsutils . unionTypeParts ( type ) ) {
@@ -125,59 +113,20 @@ export default createRule<[], MessageIds>({
125
113
return false ;
126
114
}
127
115
128
- /**
129
- * If passed an ordinary expression, this will check it as expected.
130
- *
131
- * If passed a spread element, it treats it as the union of unwrapped array/tuple type.
132
- */
133
- function shouldFlagArgument (
134
- node : TSESTree . Expression | TSESTree . SpreadElement ,
135
- ) : boolean {
136
- const argument = services . esTreeNodeToTSNodeMap . get ( node ) ;
116
+ function shouldFlagArgument ( node : TSESTree . Expression ) : boolean {
117
+ const argument = esTreeNodeToTSNodeMap . get ( node ) ;
137
118
const typeOfArgument = checker . getTypeAtLocation ( argument ) ;
138
119
return isFlaggableHandlerType ( typeOfArgument ) ;
139
120
}
140
121
141
- function shouldFlagMultipleSpreadArgs (
142
- argumentsList : TSE
1C6A
STree . CallExpressionArgument [ ] ,
143
- ) : boolean {
144
- // One could try to be clever about unpacking fixed length tuples and stuff
145
- // like that, but there's no need, since this is all invalid use of `.catch`
146
- // anyway at the end of the day. Instead, we'll just check whether any of the
147
- // possible args types would violate the rule on its own.
148
- return argumentsList . some ( argument => shouldFlagArgument ( argument ) ) ;
149
- }
150
-
151
- function shouldFlagSingleSpreadArg ( node : TSESTree . SpreadElement ) : boolean {
152
- const spreadArgs = services . esTreeNodeToTSNodeMap . get ( node . argument ) ;
153
-
154
- const spreadArgsType = checker . getTypeAtLocation ( spreadArgs ) ;
155
-
156
- if ( checker . isArrayType ( spreadArgsType ) ) {
157
- const arrayType = checker . getTypeArguments ( spreadArgsType ) [ 0 ] ;
158
- return isFlaggableHandlerType ( arrayType ) ;
159
- }
160
-
161
- if ( checker . isTupleType ( spreadArgsType ) ) {
162
- const firstType = checker . getTypeArguments ( spreadArgsType ) . at ( 0 ) ;
163
- if ( ! firstType ) {
164
- // empty spread args. Suspect code, but not a problem for this rule.
165
- return false ;
166
- }
167
- return isFlaggableHandlerType ( firstType ) ;
168
- }
169
-
170
- return true ;
171
- }
172
-
173
122
/**
174
123
* Analyzes the syntax of the catch argument and makes a best effort to pinpoint
175
124
* why it's reporting, and to come up with a suggested fix if possible.
176
125
*
177
126
* This function is explicitly operating under the assumption that the
178
127
* rule _is reporting_, so it is not guaranteed to be sound to call otherwise.
179
128
*/
180
- function refineReportForNormalArgumentIfPossible (
129
+ function refineReportIfPossible (
181
130
argument : TSESTree . Expression ,
182
131
) : undefined | Partial < ReportDescriptor < MessageIds > > {
183
132
// Only know how to be helpful if a function literal has been provided.
@@ -288,68 +237,75 @@ export default createRule<[], MessageIds>({
288
237
}
289
238
290
239
return {
291
- CallExpression ( node ) : void {
292
- if ( node . arguments . length === 0 || ! isPromiseCatchAccess ( node . callee ) ) {
240
+ CallExpression ( { arguments : args , callee } ) : void {
241
+ if ( callee . type !== AST_NODE_TYPES . MemberExpression ) {
293
242
return ;
294
243
}
295
244
296
- const firstArgument = node . arguments [ 0 ] ;
245
+ const staticMemberAccessKey = getStaticMemberAccessKey (
246
+ callee ,
247
+ context . sourceCode . getScope ( callee ) ,
248
+ ) ;
249
+ if ( ! staticMemberAccessKey ) {
250
+ return ;
251
+ }
297
252
298
- // Deal with some special cases around spread element args.
299
- // promise.catch(...handlers), promise.catch(...handlers, ...moreHandlers).
300
- if ( firstArgument . type === AST_NODE_TYPES . SpreadElement ) {
301
- if ( node . arguments . length === 1 ) {
302
- if ( shouldFlagSingleSpreadArg ( firstArgument ) ) {
303
- context . report ( {
304
- node : firstArgument ,
305
- messageId : 'useUnknown' ,
306
- } ) ;
307
- }
308
- } else if ( shouldFlagMultipleSpreadArgs ( node . arguments ) ) {
309
- context . report ( {
310
- node,
311
- messageId : 'useUnknownSpreadArgs' ,
312
- } ) ;
313
- }
253
+ const promiseMethodInfo = (
254
+ [
255
+ { method : 'catch' , append : '' , argIndexToCheck : 0 } ,
256
+ { method : 'then' , append : ' rejection' , argIndexToCheck : 1 } ,
257
+ ] satisfies {
258
+ method : string ;
259
+ append : string ;
260
+ argIndexToCheck : number ;
261
+ } [ ]
262
+ ) . find ( ( { method } ) => staticMemberAccessKey . value === method ) ;
263
+ if ( ! promiseMethodInfo ) {
264
+ return ;
265
+ }
266
+
267
+ // Need to be enough args to check
268
+ const { argIndexToCheck, ...data } = promiseMethodInfo ;
269
+ if ( args . length < argIndexToCheck + 1 ) {
314
270
return ;
315
271
}
316
272
317
- // First argument is an "ordinary" argument (i.e. not a spread argument )
273
+ // Argument to check, and all arguments before it, must be "ordinary" arguments (i.e. no spread arguments )
318
274
// promise.catch(f), promise.catch(() => {}), promise.catch(<expression>, <<other-args>>)
319
- if ( shouldFlagArgument ( firstArgument ) ) {
275
+ const argsToCheck = args . slice ( 0 , argIndexToCheck + 1 ) ;
276
+ if (
277
+ argsToCheck . some ( ( { type } ) => type === AST_NODE_TYPES . SpreadElement )
278
+ ) {
279
+ return ;
280
+ }
281
+
282
+ if (
283
+ ! tsutils . isThenableType (
284
+ checker ,
285
+ esTreeNodeToTSNodeMap . get ( callee ) ,
286
+ checker . getTypeAtLocation ( esTreeNodeToTSNodeMap . get ( callee . object ) ) ,
287
+ )
288
+ ) {
289
+ return ;
290
+ }
291
+
292
+ // the `some` check above has already excluded `SpreadElement`, so we are safe to assert the same
293
+ const node = argsToCheck [ argIndexToCheck ] as Exclude <
294
+ ( typeof argsToCheck ) [ number ] ,
295
+ TSESTree . SpreadElement
296
+ > ;
297
+ if ( shouldFlagArgument ( node ) ) {
320
298
// We are now guaranteed to report, but we have a bit of work to do
321
299
// to determine exactly where, and whether we can fix it.
322
- const overrides =
323
- refineReportForNormalArgumentIfPossible ( firstArgument ) ;
300
+ const overrides = refineReportIfPossible ( node ) ;
324
301
context . report ( {
325
- node : firstArgument ,
302
+ node,
326
303
messageId : 'useUnknown' ,
304
+ data,
327
305
...overrides ,
328
306
} ) ;
329
307
}
330
308
} ,
331
309
} ;
332
310
} ,
333
311
} ) ;
334
-
335
- /**
336
- * Answers whether the member expression looks like
337
- * `x.memberName`, `x['memberName']`,
338
- * or even `const mn = 'memberName'; x[mn]` (or optional variants thereof).
339
- */
340
- function isStaticMemberAccessOfValue (
341
- memberExpression :
342
- | TSESTree . MemberExpressionComputedName
343
- | TSESTree . MemberExpressionNonComputedName ,
344
- value : string ,
345
- scope ?: Scope . Scope | undefined ,
346
- ) : boolean {
347
- if ( ! memberExpression . computed ) {
348
- // x.memberName case.
349
- return memberExpression . property . name === value ;
350
- }
351
-
352
- // x['memberName'] cases.
353
- const staticValueResult = getStaticValue ( memberExpression . property , scope ) ;
354
- return staticValueResult != null && value === staticValueResult . value ;
355
- }
0 commit comments