@@ -10,6 +10,7 @@ import {
10
10
isFalsyType ,
11
11
isBooleanLiteralType ,
12
12
isLiteralType ,
13
+ getCallSignaturesOfType ,
13
14
} from 'tsutils' ;
14
15
import {
15
16
createRule ,
@@ -
8000
60,12 +61,15 @@ export type Options = [
60
61
{
61
62
allowConstantLoopConditions ?: boolean ;
62
63
ignoreRhs ?: boolean ;
64
+ checkArrayPredicates ?: boolean ;
63
65
} ,
64
66
] ;
65
67
66
68
export type MessageId =
67
69
| 'alwaysTruthy'
68
70
| 'alwaysFalsy'
71
+ | 'alwaysTruthyFunc'
72
+ | 'alwaysFalsyFunc'
69
73
| 'neverNullish'
70
74
| 'alwaysNullish'
71
75
| 'literalBooleanExpression'
@@ -92,6 +96,9 @@ export default createRule<Options, MessageId>({
92
96
ignoreRhs : {
93
97
type : 'boolean' ,
94
98
} ,
99
+ checkArrayPredicates : {
100
+ type : 'boolean' ,
101
+ } ,
95
102
} ,
96
103
additionalProperties : false ,
97
104
} ,
@@ -100,6 +107,10 @@ export default createRule<Options, MessageId>({
100
107
messages : {
101
108
alwaysTruthy : 'Unnecessary conditional, value is always truthy.' ,
102
109
alwaysFalsy : 'Unnecessary conditional, value is always falsy.' ,
110
+ alwaysTruthyFunc :
111
+ 'This callback should return a conditional, but return is always truthy' ,
112
+ alwaysFalsyFunc :
113
+ 'This callback should return a conditional, but return is always falsy' ,
103
114
neverNullish :
104
115
'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.' ,
105
116
alwaysNullish :
@@ -114,9 +125,13 @@ export default createRule<Options, MessageId>({
114
125
{
115
126
allowConstantLoopConditions : false ,
116
127
ignoreRhs : false ,
128
+ checkArrayPredicates : false ,
117
129
} ,
118
130
]
A3E2
span>,
119
- create ( context , [ { allowConstantLoopConditions, ignoreRhs } ] ) {
131
+ create (
132
+ context ,
133
+ [ { allowConstantLoopConditions, checkArrayPredicates, ignoreRhs } ] ,
134
+ ) {
120
135
const service = getParserServices ( context ) ;
121
136
const checker = service . program . getTypeChecker ( ) ;
122
137
const sourceCode = context . getSourceCode ( ) ;
@@ -126,6 +141,11 @@ export default createRule<Options, MessageId>({
126
141
return getConstrainedTypeAtLocation ( checker , tsNode ) ;
127
142
}
128
143
144
+ function nodeIsArrayType ( node : TSESTree . Node ) : boolean {
145
+ const nodeType = getNodeType ( node ) ;
146
+ return checker . isArrayType ( nodeType ) || checker . isTupleType ( nodeType ) ;
147
+ }
148
+
129
149
/**
130
150
* Checks if a conditional node is necessary:
131
151
* if the type of the node is always true or always false, it's not necessary.
@@ -270,6 +290,77 @@ export default createRule<Options, MessageId>({
270
290
checkNode ( node . test ) ;
271
291
}
272
292
293
+ const ARRAY_PREDICATE_FUNCTIONS = new Set ( [
294
+ 'filter' ,
295
+ 'find' ,
296
+ 'some' ,
297
+ 'every' ,
298
+ ] ) ;
299
+ function shouldCheckCallback ( node : TSESTree . CallExpression ) : boolean {
300
+ const { callee } = node ;
301
+ return (
302
+ // option is on
303
+ ! ! checkArrayPredicates &&
304
+ // looks like `something.filter` or `something.find`
305
+ callee . type === AST_NODE_TYPES . MemberExpression &&
306
+ callee . property . type === AST_NODE_TYPES . Identifier &&
307
+ ARRAY_PREDICATE_FUNCTIONS . has ( callee . property . name ) &&
308
+ // and the left-hand side is an array, according to the types
309
+ nodeIsArrayType ( callee . object )
310
+ ) ;
311
+ }
312
+ function checkCallExpression ( node : TSESTree . CallExpression ) : void {
313
+ const {
314
+ arguments : [ callback ] ,
315
+ } = node ;
316
+ if ( callback && shouldCheckCallback ( node ) ) {
317
+ // Inline defined functions
318
+ if (
319
+ ( callback . type === AST_NODE_TYPES . ArrowFunctionExpression ||
320
+ callback . type === AST_NODE_TYPES . FunctionExpression ) &&
321
+ callback . body
322
+ ) {
323
+ // Two special cases, where we can directly check the node that's returned:
324
+ // () => something
325
+ if ( callback . body . type !== AST_NODE_TYPES . BlockStatement ) {
326
+ return checkNode ( callback . body ) ;
327
+ }
328
+ // () => { return something; }
329
+ const callbackBody = callback . body . body ;
330
+ if (
331
+ callbackBody . length === 1 &&
332
+ callbackBody [ 0 ] . type === AST_NODE_TYPES . ReturnStatement &&
333
+ callbackBody [ 0 ] . argument
334
+ ) {
335
+ return checkNode ( callbackBody [ 0 ] . argument ) ;
336
+ }
337
+ // Potential enhancement: could use code-path analysis to check
338
+ // any function with a single return statement
339
+ // (Value to complexity ratio is dubious however)
340
+ }
341
+ // Otherwise just do type analysis on the function as a whole.
342
+ const returnTypes = getCallSignaturesOfType (
343
+ getNodeType ( callback ) ,
344
+ ) . map ( sig => sig . getReturnType ( ) ) ;
345
+ /* istanbul ignore if */ if ( returnTypes . length === 0 ) {
346
+ // Not a callable function
347
+ return ;
348
+ }
349
+ if ( ! returnTypes . some ( isPossiblyFalsy ) ) {
350
+ return context . report ( {
351
+ node : callback ,
352
+ messageId : 'alwaysTruthyFunc' ,
353
+ } ) ;
354
+ }
355
+ if ( ! returnTypes . some ( isPossiblyTruthy ) ) {
356
+ return context . report ( {
357
+ node : callback ,
358
+ messageId : 'alwaysFalsyFunc' ,
359
+ } ) ;
360
+ }
361
+ }
362
+ }
363
+
273
364
function checkOptionalChain (
274
365
node : TSESTree . OptionalMemberExpression | TSESTree . OptionalCallExpression ,
275
366
beforeOperator : TSESTree . Node ,
@@ -323,6 +414,7 @@ export default createRule<Options, MessageId>({
323
414
324
415
return {
325
416
BinaryExpression : checkIfBinaryExpressionIsNecessaryConditional ,
417
+ CallExpression : checkCallExpression ,
326
418
ConditionalExpression : checkIfTestExpressionIsNecessaryConditional ,
327
419
DoWhileStatement : checkIfLoopIsNecessaryConditional ,
328
420
ForStatement : checkIfLoopIsNecessaryConditional ,
0 commit comments