@@ -189,6 +189,7 @@ export default createRule<Options, MessageId>({
189
189
WhileStatement : traverseTestExpression ,
190
190
'LogicalExpression[operator!="??"]' : traverseLogicalExpression ,
191
191
'UnaryExpression[operator="!"]' : traverseUnaryLogicalExpression ,
192
+ CallExpression : traverseCallExpression ,
192
193
} ;
193
194
194
195
type TestExpression =
@@ -232,10 +233,139 @@ export default createRule<Options, MessageId>({
232
233
// left argument is always treated as a condition
233
234
traverseNode ( node . left , true ) ;
234
235
// if the logical expression is used for control flow,
235
- // then it's right argument is used for it's side effects only
236
+ // then its right argument is used for its side effects only
236
237
traverseNode ( node . right , isCondition ) ;
237
238
}
238
239
240
+ function traverseCallExpression ( node : TSESTree . CallExpression ) : void {
241
+ const assertedArgument = findAssertedArgument ( node ) ;
242
+ if ( assertedArgument != null ) {
243
+ traverseNode ( assertedArgument , true ) ;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Inspect a call expression to see if it's a call to an assertion function.
249
+ * If it is, return the node of the argument that is asserted.
250
+ */
251
+ function findAssertedArgument (
252
+ node : TSESTree . CallExpression ,
253
+ ) : TSESTree . Expression | undefined {
254
+ // If the call looks like `assert(expr1, expr2, ...c, d, e, f)`, then we can
255
+ // only care if `expr1` or `expr2` is asserted, since anything that happens
256
+ // within or after a spread argument is out of scope to reason about.
257
+ const checkableArguments : TSESTree . Expression [ ] = [ ] ;
258
+ for ( const argument of node . arguments ) {
259
+ if ( argument . type === AST_NODE_TYPES . SpreadElement ) {
260
+ break ;
261
+ }
262
+
263
+ checkableArguments . push ( argument ) ;
264
+ }
265
+
266
+ // nothing to do
267
+ if ( checkableArguments . length === 0 ) {
268
+ return undefined ;
269
+ }
270
+
271
+ // Game plan: we're going to check the type of the callee. If it has call
272
+ // signatures and they _ALL_ agree that they assert on a parameter at the
273
+ // _SAME_ position, we'll consider the argument in that position to be an
274
+ // asserted argument.
275
+ const calleeType = getConstrainedTypeAtLocation ( services , node . callee ) ;
276
+ const callSignatures = tsutils . getCallSignaturesOfType ( calleeType ) ;
277
+
278
+ let assertedParameterIndex : number | undefined = undefined ;
279
+ for ( const signature of callSignatures ) {
280
+ const declaration = signature . getDeclaration ( ) ;
281
+ const returnTypeAnnotation = declaration . type ;
282
+
283
+ // Be sure we're dealing with a truthiness assertion function.
284
+ if (
285
+ ! (
286
+ returnTypeAnnotation != null &&
287
+ ts . isTypePredicateNode ( returnTypeAnnotation ) &&
288
+ // This eliminates things like `x is string` and `asserts x is T`
289
+ // leaving us with just the `asserts x` cases.
290
+ returnTypeAnnotation . type == null &&
291
+ // I think this is redundant but, still, it needs to be true
292
+ returnTypeAnnotation . assertsModifier != null
293
+ )
294
+ ) {
295
+ return undefined ;
296
+ }
297
+
298
+ const assertionTarget = returnTypeAnnotation . parameterName ;
299
+ if ( assertionTarget . kind !== ts . SyntaxKind . Identifier ) {
300
+ // This can happen when asserting on `this`. Ignore!
301
+ return undefined ;
302
+ }
303
+
304
+ // If the first parameter is `this`, skip it, so that our index matches
305
+ // the index of the argument at the call site.
306
+ const firstParameter = declaration . parameters . at ( 0 ) ;
307
+ const nonThisParameters =
308
+ firstParameter ?. name . kind === ts . SyntaxKind . Identifier &&
309
+ firstParameter . name . text === 'this'
310
+ ? declaration . parameters . slice ( 1 )
311
+ : declaration . parameters ;
312
+
313
+ // Don't bother inspecting parameters past the number of
314
+ // arguments we have at the call site.
315
+ const checkableNonThisParameters = nonThisParameters . slice (
316
+ 0 ,
317
+ checkableArguments . length ,
318
+ ) ;
319
+
320
+ let assertedParameterIndexForThisSignature : number | undefined ;
321
+ for ( const [ index , parameter ] of checkableNonThisParameters . entries ( ) ) {
322
+ if ( parameter . dotDotDotToken != null ) {
323
+ // Cannot assert a rest parameter, and can't have a rest parameter
324
+ // before the asserted parameter. It's not only a TS error, it's
325
+ // not something we can logically make sense of, so give up here.
326
+ return undefined ;
327
+ }
328
+
329
+ if ( parameter . name . kind !== ts . SyntaxKind . Identifier ) {
330
+ // Only identifiers are valid for assertion targets, so skip over
331
+ // anything like `{ destructuring: parameter }: T`
332
+ continue ;
333
+ }
334
+
335
+ // we've found a match between the "target"s in
336
+ // `function asserts(target: T): asserts target;`
337
+ if ( parameter . name . text === assertionTarget . text ) {
338
+ assertedParameterIndexForThisSignature = index ;
339
+ break ;
340
+ }
341
+ }
342
+
343
+ if ( assertedParameterIndexForThisSignature == null ) {
344
+ // Didn't find an assertion target in this signature that could match
345
+ // the call site.
346
+ return undefined ;
347
+ }
348
+
349
+ if (
350
+ assertedParameterIndex != null &&
351
+ assertedParameterIndex !== assertedParameterIndexForThisSignature
352
+ ) {
353
+ // The asserted parameter we found for this signature didn't match
354
+ // previous signatures.
355
+ return undefined ;
356
+ }
357
+
358
+ assertedParameterIndex = assertedParameterIndexForThisSignature ;
359
+ }
360
+
361
+ // Didn't find a unique assertion index.
362
+ if ( assertedParameterIndex == null ) {
363
+ return undefined ;
364
+ }
365
+
366
+ return checkableArguments [ assertedParameterIndex ] ;
367
+ }
368
+
239
369
/**
240
370
* Inspects any node.
241
371
*
0 commit comments