Skip to content

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
95525e0
initial implementation
ronami Oct 4, 2024
e6dd55c
tests
ronami Oct 4, 2024
1a898fe
refactor
ronami Oct 5, 2024
a16f46e
some more tests
ronami Oct 5, 2024
057a68e
update docs
ronami Oct 5, 2024
d2ea1ab
update snapshots
ronami Oct 5, 2024
116c80b
merge conflicts
ronami Oct 11, 2024
d641dc9
handle implicit or explicit undefined return types
ronami Oct 11, 2024
8d997d1
refactor and update tests
ronami Oct 11, 2024
47c167c
remove unnecessary union check
ronami Oct 11, 2024
90f9ba8
inline check
ronami Oct 11, 2024
42a1336
cover empty return staetments
ronami Oct 11, 2024
655e52c
report a function as returning undefined only if no other isssues wer…
ronami Oct 11, 2024
b8643c9
update comments
ronami Oct 11, 2024
c665d82
add test
ronami Oct 11, 2024
b3dd8be
remove unnecessary test
ronami Oct 11, 2024
fc0ea08
Merge branch 'main' into strict-boolean-expressions-predicates
ronami Nov 2, 2024
5691380
update code to match function's inferred return type rather than each…
ronami Oct 30, 2024
f718885
update tests to match the implementation changes
ronami Nov 2, 2024
dbc4c7a
adjust suggestion message
ronami Nov 2, 2024
f6329dc
final adjustments
ronami Nov 2, 2024
3ce22c5
final adjustments #2
ronami Nov 2, 2024
96ef5c0
test additions
ronami Nov 2, 2024
403c011
update snapshots
ronami Nov 2, 2024
339fa3a
Update packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx
ronami Nov 11, 2024
30b3d1c
initial implementation of deducing the correct signature for non-func…
ronami Nov 27, 2024
690fdbc
comments
ronami Nov 27, 2024
d607a00
take type constraints into consideration
ronami Nov 27, 2024
f5c7300
only check type constraints on type parameters
ronami Nov 27, 2024
287122e
update tests
ronami Nov 27, 2024
7566d0b
update index tests
ronami Nov 27, 2024
d472c86
update snapshot
ronami Nov 27, 2024
00b8424
simplify code a bit
ronami Nov 28, 2024
5e59733
remove overly complex heuristic
ronami Dec 17, 2024
d2a254a
use an existing helper rather than implementing one
ronami Dec 17, 2024
6b32e50
update tests
ronami Dec 17, 2024
c97d53c
Update packages/eslint-plugin/src/rules/strict-boolean-expressions.ts
ronami Dec 17, 2024
ac6be7e
fix codecov
ronami Dec 17, 2024
e94c0ee
cleanup old code, support type constraints
ronami Dec 19, 2024
601dfba
refactor fixer
ronami Dec 19, 2024
1994ba6
remove unnecessary tests
ronami Dec 19, 2024
c71112b
Merge branch 'main' into strict-boolean-expressions-predicates
ronami Dec 19, 2024
8e54722
revert changes to isParenlessArrowFunction
ronami Dec 26, 2024
abfb9f7
Merge branch 'main' into strict-boolean-expressions-predicates
ronami Dec 26, 2024
f17a05c
oops
ronami Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following nodes are considered boolean expressions and their type is checked
- Right-hand side operand is ignored when it's not a descendant of another boolean expression.
This is to allow usage of boolean operators for their short-circuiting behavior.
- Asserted argument of an assertion function (`assert(arg)`).
- Return type of array predicate functions such as `filter()`, `some()`, etc.

## Examples

Expand Down Expand Up @@ -61,6 +62,9 @@ while (obj) {
declare function assert(value: unknown): asserts value;
let maybeString = Math.random() > 0.5 ? '' : undefined;
assert(maybeString);

// array predicates' return types are boolean contexts.
['one', null].filter(x => x);
```

</TabItem>
Expand Down
127 changes: 119 additions & 8 deletions packages/eslint-plugin/src/rules/strict-boolean-expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
TSESTree,
} from '@typescript-eslint/utils';

import { AST_NODE_TYPES } 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';

Expand All @@ -12,7 +12,10 @@ import {
getConstrainedTypeAtLocation,
getParserServices,
getWrappingFixer,
isArrayMethodCallWithPredicate,
isParenlessArrowFunction,
isTypeArrayTypeOrUnionOfArrayTypes,
nullThrows,
} from '../util';
import { findTruthinessAssertedArgument } from '../util/assertionFunctionUtils';

Expand Down Expand Up @@ -53,7 +56,10 @@ export type MessageId =
| 'conditionFixDefaultEmptyString'
| 'conditionFixDefaultFalse'
| 'conditionFixDefaultZero'
| 'noStrictNullCheck';
| 'explicitBooleanReturnType'
| 'noStrictNullCheck'
| 'predicateCannotBeAsync'
| 'predicateReturnsNonBoolean';

export default createRule<Options, MessageId>({
name: 'strict-boolean-expressions',
Expand Down Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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
? [
{
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,
});
}

/**
Expand Down Expand Up @@ -1007,11 +1116,13 @@ function isArrayLengthExpression(
function isBrandedBoolean(type: ts.Type): boolean {
return (
type.isIntersection() &&
type.types.some(childType =>
tsutils.isTypeFlagSet(
childType,
ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Boolean,
),
)
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 about how customized files appear on GitHub.

Loading
Loading