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
95525e0e6dd55c1a898fea16f46e057a68ed2ea1ab116c80bd641dc98d997d147c167c90f9ba842a1336655e52cb8643c9c665d82b3dd8befc0ea085691380f718885dbc4c7af6329dc3ce22c596ef5c0403c011339fa3a30b3d1c690fdbcd607a00f5c7300287122e7566d0bd472c8600b84245e59733d2a254a6b32e50c97d53cac6be7ee94c0ee601dfba1994ba6c71112b8e54722abfb9f7f17a05cFile 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.