Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): [strict-boolean-expressions] check array predicate functions' return statements#10106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
feat(eslint-plugin): [strict-boolean-expressions] check array predicate functions' return statements#10106
Changes fromall commits
95525e0
e6dd55c
1a898fe
a16f46e
057a68e
d2ea1ab
116c80b
d641dc9
8d997d1
47c167c
90f9ba8
42a1336
655e52c
b8643c9
c665d82
b3dd8be
fc0ea08
5691380
f718885
dbc4c7a
f6329dc
3ce22c5
96ef5c0
403c011
339fa3a
30b3d1c
690fdbc
d607a00
f5c7300
287122e
7566d0b
d472c86
00b8424
5e59733
d2a254a
6b32e50
c97d53c
ac6be7e
e94c0ee
601dfba
1994ba6
c71112b
8e54722
abfb9f7
f17a05c
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
JoshuaKGoldberg marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -3,7 +3,7 @@ import type { | ||
TSESTree, | ||
} from '@typescript-eslint/utils'; | ||
import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; | ||
import * as tsutils from 'ts-api-utils'; | ||
import * as ts from 'typescript'; | ||
@@ -12,7 +12,10 @@ import { | ||
getConstrainedTypeAtLocation, | ||
getParserServices, | ||
getWrappingFixer, | ||
isArrayMethodCallWithPredicate, | ||
isParenlessArrowFunction, | ||
isTypeArrayTypeOrUnionOfArrayTypes, | ||
nullThrows, | ||
} from '../util'; | ||
import { findTruthinessAssertedArgument } from '../util/assertionFunctionUtils'; | ||
@@ -53,7 +56,10 @@ export type MessageId = | ||
| 'conditionFixDefaultEmptyString' | ||
| 'conditionFixDefaultFalse' | ||
| 'conditionFixDefaultZero' | ||
| 'explicitBooleanReturnType' | ||
| 'noStrictNullCheck' | ||
| 'predicateCannotBeAsync' | ||
| 'predicateReturnsNonBoolean'; | ||
export default createRule<Options, MessageId>({ | ||
name: 'strict-boolean-expressions', | ||
@@ -122,8 +128,13 @@ export default createRule<Options, MessageId>({ | ||
'Explicitly treat nullish value the same as false (`value ?? false`)', | ||
conditionFixDefaultZero: | ||
'Explicitly treat nullish value the same as 0 (`value ?? 0`)', | ||
explicitBooleanReturnType: | ||
'Add an explicit `boolean` return type annotation.', | ||
noStrictNullCheck: | ||
'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', | ||
predicateCannotBeAsync: | ||
"Predicate function should not be 'async'; expected a boolean return type.", | ||
predicateReturnsNonBoolean: 'Predicate function should return a boolean.', | ||
}, | ||
schema: [ | ||
{ | ||
@@ -275,6 +286,104 @@ export default createRule<Options, MessageId>({ | ||
if (assertedArgument != null) { | ||
traverseNode(assertedArgument, true); | ||
} | ||
if (isArrayMethodCallWithPredicate(context, services, node)) { | ||
const predicate = node.arguments.at(0); | ||
if (predicate) { | ||
checkArrayMethodCallPredicate(predicate); | ||
} | ||
} | ||
} | ||
/** | ||
* Dedicated function to check array method predicate calls. Reports predicate | ||
* arguments that don't return a boolean value. | ||
* | ||
* Ignores the `allow*` options and requires a boolean value. | ||
*/ | ||
function checkArrayMethodCallPredicate( | ||
predicateNode: TSESTree.CallExpressionArgument, | ||
): void { | ||
const isFunctionExpression = ASTUtils.isFunction(predicateNode); | ||
// custom message for accidental `async` function expressions | ||
if (isFunctionExpression && predicateNode.async) { | ||
return context.report({ | ||
node: predicateNode, | ||
messageId: 'predicateCannotBeAsync', | ||
}); | ||
} | ||
const returnTypes = services | ||
.getTypeAtLocation(predicateNode) | ||
.getCallSignatures() | ||
.map(signature => { | ||
const type = signature.getReturnType(); | ||
if (tsutils.isTypeParameter(type)) { | ||
return checker.getBaseConstraintOfType(type) ?? type; | ||
} | ||
return type; | ||
}); | ||
if (returnTypes.every(returnType => isBooleanType(returnType))) { | ||
return; | ||
} | ||
const canFix = isFunctionExpression && !predicateNode.returnType; | ||
return context.report({ | ||
node: predicateNode, | ||
messageId: 'predicateReturnsNonBoolean', | ||
suggest: canFix | ||
kirkwaiblinger marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
? [ | ||
{ | ||
messageId: 'explicitBooleanReturnType', | ||
fix: fixer => { | ||
if ( | ||
predicateNode.type === | ||
AST_NODE_TYPES.ArrowFunctionExpression && | ||
isParenlessArrowFunction(predicateNode, context.sourceCode) | ||
) { | ||
return [ | ||
fixer.insertTextBefore(predicateNode.params[0], '('), | ||
fixer.insertTextAfter( | ||
predicateNode.params[0], | ||
'): boolean', | ||
), | ||
]; | ||
} | ||
if (predicateNode.params.length === 0) { | ||
const closingBracket = nullThrows( | ||
context.sourceCode.getFirstToken( | ||
predicateNode, | ||
token => token.value === ')', | ||
), | ||
'function expression has to have a closing parenthesis.', | ||
); | ||
return fixer.insertTextAfter(closingBracket, ': boolean'); | ||
} | ||
const lastClosingParenthesis = nullThrows( | ||
context.sourceCode.getTokenAfter( | ||
predicateNode.params[predicateNode.params.length - 1], | ||
token => token.value === ')', | ||
), | ||
'function expression has to have a closing parenthesis.', | ||
); | ||
return fixer.insertTextAfter( | ||
lastClosingParenthesis, | ||
': boolean', | ||
); | ||
}, | ||
}, | ||
] | ||
: null, | ||
}); | ||
} | ||
/** | ||
@@ -1007,11 +1116,13 @@ function isArrayLengthExpression( | ||
function isBrandedBoolean(type: ts.Type): boolean { | ||
return ( | ||
type.isIntersection() && | ||
type.types.some(childType => isBooleanType(childType)) | ||
); | ||
} | ||
function isBooleanType(expressionType: ts.Type): boolean { | ||
return tsutils.isTypeFlagSet( | ||
expressionType, | ||
ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral, | ||
); | ||
} |
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.