Skip to content

Commit 34e7d1e

Browse files
jonathanrdelgadobradzacher
authored andcommitted
feat(eslint-plugin): add rule strict-boolean-expressions (#579)
1 parent 44677b4 commit 34e7d1e

File tree

6 files changed

+1069
-1
lines changed

6 files changed

+1069
-1
lines changed

packages/eslint-plugin/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
178178
| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Enforce giving `compare` argument to `Array#sort` | | | :thought_balloon: |
179179
| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: |
180180
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
181+
| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: |
181182
| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | |
182183
| [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | | | :thought_balloon: |
183184
| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | |

packages/eslint-plugin/ROADMAP.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
| [`prefer-object-spread`] | 🌟 | [`prefer-object-spread`][prefer-object-spread] |
9393
| [`radix`] | 🌟 | [`radix`][radix] |
9494
| [`restrict-plus-operands`] || [`@typescript-eslint/restrict-plus-operands`] |
95-
| [`strict-boolean-expressions`] | 🛑 | N/A |
95+
| [`strict-boolean-expressions`] | | [`@typescript-eslint/strict-boolean-expressions`] |
9696
| [`strict-type-predicates`] | 🛑 | N/A |
9797
| [`switch-default`] | 🌟 | [`default-case`][default-case] |
9898
| [`triple-equals`] | 🌟 | [`eqeqeq`][eqeqeq] |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Boolean expressions are limited to booleans (strict-boolean-expressions)
2+
3+
Requires that any boolean expression is limited to true booleans rather than
4+
casting another primitive to a boolean at runtime.
5+
6+
It is useful to be explicit, for example, if you were trying to check if a
7+
number was defined. Doing `if (number)` would evaluate to `false` if `number`
8+
was defined and `0`. This rule forces these expressions to be explicit and to
9+
strictly use booleans.
10+
11+
The following nodes are checked:
12+
13+
- Arguments to the `!`, `&&`, and `||` operators
14+
- The condition in a conditional expression `(cond ? x : y)`
15+
- Conditions for `if`, `for`, `while`, and `do-while` statements.
16+
17+
Examples of **incorrect** code for this rule:
18+
19+
```ts
20+
const number = 0;
21+
if (number) {
22+
return;
23+
}
24+
25+
let foo = bar || 'foobar';
26+
27+
let undefinedItem;
28+
let foo = undefinedItem ? 'foo' : 'bar';
29+
30+
let str = 'foo';
31+
while (str) {
32+
break;
33+
}
34+
```
35+
36+
Examples of **correct** code for this rule:
37+
38+
```ts
39+
const number = 0;
40+
if (typeof number !== 'undefined') {
41+
return;
42+
}
43+
44+
let foo = typeof bar !== 'undefined' ? bar : 'foobar';
45+
46+
let undefinedItem;
47+
let foo = typeof undefinedItem !== 'undefined' ? 'foo' : 'bar';
48+
49+
let str = 'foo';
50+
while (typeof str !== 'undefined') {
51+
break;
52+
}
53+
```
54+
55+
## Related To
56+
57+
- TSLint: [strict-boolean-expressions](https://palantir.github.io/tslint/rules/strict-boolean-expressions)

packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import promiseFunctionAsync from './promise-function-async';
5353
import requireArraySortCompare from './require-array-sort-compare';
5454
import restrictPlusOperands from './restrict-plus-operands';
5555
import semi from './semi';
56+
import strictBooleanExpressions from './strict-boolean-expressions';
5657
import typeAnnotationSpacing from './type-annotation-spacing';
5758
import unboundMethod from './unbound-method';
5859
import unifiedSignatures from './unified-signatures';
@@ -113,6 +114,7 @@ export default {
113114
'require-array-sort-compare': requireArraySortCompare,
114115
'restrict-plus-operands': restrictPlusOperands,
115116
semi: semi,
117+
'strict-boolean-expressions': strictBooleanExpressions,
116118
'type-annotation-spacing': typeAnnotationSpacing,
117119
'unbound-method': unboundMethod,
118120
'unified-signatures': unifiedSignatures,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {
2+
TSESTree,
3+
AST_NODE_TYPES,
4+
} from '@typescript-eslint/experimental-utils';
5+
import ts from 'typescript';
6+
import * as tsutils from 'tsutils';
7+
import * as util from '../util';
8+
9+
type ExpressionWithTest =
10+
| TSESTree.ConditionalExpression
11+
| TSESTree.DoWhileStatement
12+
| TSESTree.ForStatement
13+
| TSESTree.IfStatement
14+
| TSESTree.WhileStatement;
15+
16+
export default util.createRule({
17+
name: 'strict-boolean-expressions',
18+
meta: {
19+
type: 'suggestion',
20+
docs: {
21+
description: 'Restricts the types allowed in boolean expressions',
22+
category: 'Best Practices',
23+
recommended: false,
24+
},
25+
schema: [],
26+
messages: {
27+
strictBooleanExpression: 'Unexpected non-boolean in conditional.',
28+
},
29+
},
30+
defaultOptions: [],
31+
create(context) {
32+
const service = util.getParserServices(context);
33+
const checker = service.program.getTypeChecker();
34+
35+
/**
36+
* Determines if the node has a boolean type.
37+
*/
38+
function isBooleanType(node: TSESTree.Node): boolean {
39+
const tsNode = service.esTreeNodeToTSNodeMap.get<ts.ExpressionStatement>(
40+
node,
41+
);
42+
const type = util.getConstrainedTypeAtLocation(checker, tsNode);
43+
return tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike);
44+
}
45+
46+
/**
47+
* Asserts that a testable expression contains a boolean, reports otherwise.
48+
* Filters all LogicalExpressions to prevent some duplicate reports.
49+
*/
50+
function assertTestExpressionContainsBoolean(
51+
node: ExpressionWithTest,
52+
): void {
53+
if (
54+
node.test !== null &&
55+
node.test.type !== AST_NODE_TYPES.LogicalExpression &&
56+
!isBooleanType(node.test)
57+
) {
58+
reportNode(node.test);
59+
}
60+
}
61+
62+
/**
63+
* Asserts that a logical expression contains a boolean, reports otherwise.
64+
*/
65+
function assertLocalExpressionContainsBoolean(
66+
node: TSESTree.LogicalExpression,
67+
): void {
68+
if (!isBooleanType(node.left) || !isBooleanType(node.right)) {
69+
reportNode(node);
70+
}
71+
}
72+
73+
/**
74+
* Asserts that a unary expression contains a boolean, reports otherwise.
75+
*/
76+
function assertUnaryExpressionContainsBoolean(
77+
node: TSESTree.UnaryExpression,
78+
): void {
79+
if (!isBooleanType(node.argument)) {
80+
reportNode(node.argument);
81+
}
82+
}
83+
84+
/**
85+
* Reports an offending node in context.
86+
*/
87+
function reportNode(node: TSESTree.Node): void {
88+
context.report({ node, messageId: 'strictBooleanExpression' });
89+
}
90+
91+
return {
92+
ConditionalExpression: assertTestExpressionContainsBoolean,
93+
DoWhileStatement: assertTestExpressionContainsBoolean,
94+
ForStatement: assertTestExpressionContainsBoolean,
95+
IfStatement: assertTestExpressionContainsBoolean,
96+
WhileStatement: assertTestExpressionContainsBoolean,
97+
LogicalExpression: assertLocalExpressionContainsBoolean,
98+
'UnaryExpression[operator="!"]': assertUnaryExpressionContainsBoolean,
99+
};
100+
},
101+
});

0 commit comments

Comments
 (0)