Skip to content

Commit 9344233

Browse files
Rajin9601bradzacher
authored andcommitted
feat(eslint-plugin): [strict-bool-expr] add allowSafe option (#1385)
1 parent 0596476 commit 9344233

File tree

3 files changed

+153
-12
lines changed

3 files changed

+153
-12
lines changed

packages/eslint-plugin/docs/rules/strict-boolean-expressions.md

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ while (typeof str !== 'undefined') {
5757
Options may be provided as an object with:
5858

5959
- `allowNullable` to allow `undefined` and `null` in addition to `boolean` as a type of all boolean expressions. (`false` by default).
60+
- `allowSafe` to allow non-falsy types (i.e. non string / number / boolean) in addition to `boolean` as a type of all boolean expressions. (`false` by default).
6061
- `ignoreRhs` to skip the check on the right hand side of expressions like `a && b` or `a || b` - allows these operators to be used for their short-circuiting behavior. (`false` by default).
6162

6263
## Related To

packages/eslint-plugin/src/rules/strict-boolean-expressions.ts

+28-12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Options = [
1717
{
1818
ignoreRhs?: boolean;
1919
allowNullable?: boolean;
20+
allowSafe?: boolean;
2021
},
2122
];
2223

@@ -40,6 +41,9 @@ export default util.createRule<Options, 'strictBooleanExpression'>({
4041
allowNullable: {
4142
type: 'boolean',
4243
},
44+
allowSafe: {
45+
type: 'boolean',
46+
},
4347
},
4448
additionalProperties: false,
4549
},
@@ -52,16 +56,17 @@ export default util.createRule<Options, 'strictBooleanExpression'>({
5256
{
5357
ignoreRhs: false,
5458
allowNullable: false,
59+
allowSafe: false,
5560
},
5661
],
5762
create(context, [options]) {
5863
const service = util.getParserServices(context);
5964
const checker = service.program.getTypeChecker();
6065

6166
/**
62-
* Determines if the node has a boolean type.
67+
* Determines if the node is safe for boolean type
6368
*/
64-
function isBooleanType(node: TSESTree.Node): boolean {
69+
function isValidBooleanNode(node: TSESTree.Node): boolean {
6570
const tsNode = service.esTreeNodeToTSNodeMap.get<ts.ExpressionStatement>(
6671
node,
6772
);
@@ -79,20 +84,31 @@ export default util.createRule<Options, 'strictBooleanExpression'>({
7984
hasBoolean = true;
8085
continue;
8186
}
82-
if (options.allowNullable) {
83-
if (tsutils.isTypeFlagSet(ty, ts.TypeFlags.Null)) {
84-
continue;
87+
88+
if (
89+
tsutils.isTypeFlagSet(ty, ts.TypeFlags.Null) ||
90+
tsutils.isTypeFlagSet(ty, ts.TypeFlags.Undefined)
91+
) {
92+
if (!options.allowNullable) {
93+
return false;
8594
}
86-
if (tsutils.isTypeFlagSet(ty, ts.TypeFlags.Undefined)) {
95+
continue;
96+
}
97+
98+
if (
99+
!tsutils.isTypeFlagSet(ty, ts.TypeFlags.StringLike) &&
100+
!tsutils.isTypeFlagSet(ty, ts.TypeFlags.NumberLike)
101+
) {
102+
if (options.allowSafe) {
103+
hasBoolean = true;
87104
continue;
88105
}
89106
}
90-
// Union variant is something else
107+
91108
return false;
92109
}
93110
return hasBoolean;
94111
}
95-
96112
return false;
97113
}
98114

@@ -106,7 +122,7 @@ export default util.createRule<Options, 'strictBooleanExpression'>({
106122
if (
107123
node.test !== null &&
108124
node.test.type !== AST_NODE_TYPES.LogicalExpression &&
109-
!isBooleanType(node.test)
125+
!isValidBooleanNode(node.test)
110126
) {
111127
reportNode(node.test);
112128
}
@@ -119,8 +135,8 @@ export default util.createRule<Options, 'strictBooleanExpression'>({
119135
node: TSESTree.LogicalExpression,
120136
): void {
121137
if (
122-
!isBooleanType(node.left) ||
123-
(!options.ignoreRhs && !isBooleanType(node.right))
138+
!isValidBooleanNode(node.left) ||
139+
(!options.ignoreRhs && !isValidBooleanNode(node.right))
124140
) {
125141
reportNode(node);
126142
}
@@ -132,7 +148,7 @@ export default util.createRule<Options, 'strictBooleanExpression'>({
132148
function assertUnaryExpressionContainsBoolean(
133149
node: TSESTree.UnaryExpression,
134150
): void {
135-
if (!isBooleanType(node.argument)) {
151+
if (!isValidBooleanNode(node.argument)) {
136152
reportNode(node.argument);
137153
}
138154
}

packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts

+124
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,35 @@ ruleTester.run('strict-boolean-expressions', rule, {
178178
declare const x: string | null;
179179
y = x ?? 'foo';
180180
`,
181+
{
182+
options: [{ allowSafe: true }],
183+
code: `
184+
type TestType = { a: string; };
185+
const f1 = (x: boolean | TestType) => x ? 1 : 0;
186+
const f2 = (x: true | TestType) => x ? 1 : 0;
187+
const f3 = (x: TestType | false) => x ? 1 : 0;
188+
`,
189+
},
190+
{
191+
options: [{ allowNullable: true, allowSafe: true }],
192+
code: `
193+
type TestType = { a: string; };
194+
type TestType2 = { b: number; };
195+
const f1 = (x?: boolean | TestType) => x ? 1 : 0;
196+
const f2 = (x: TestType | TestType2 | null) => x ? 1 : 0;
197+
const f3 = (x?: TestType | TestType2 | null) => x ? 1 : 0;
198+
const f4 = (x?: TestType2 | true) => x ? 1 : 0;
199+
const f5 = (g?: (x: number) => number) => g ? g(1) : 0;
200+
`,
201+
},
202+
{
203+
options: [{ allowNullable: true, allowSafe: true, ignoreRhs: true }],
204+
code: `
205+
type TestType = { foo? : { bar?: string; }; };
206+
const f1 = (x?: TestType) => x && x.foo && x.foo.bar
207+
const f2 = (g?: (x: number) => number) => g && g(1)
208+
`,
209+
},
181210
],
182211

183212
invalid: [
@@ -925,6 +954,30 @@ ruleTester.run('strict-boolean-expressions', rule, {
925954
},
926955
],
927956
},
957+
{
958+
errors: [
959+
{
960+
messageId: 'strictBooleanExpression',
961+
line: 2,
962+
column: 55,
963+
},
964+
{
965+
messageId: 'strictBooleanExpression',
966+
line: 3,
967+
column: 37,
968+
},
969+
{
970+
messageId: 'strictBooleanExpression',
971+
line: 4,
972+
column: 41,
973+
},
974+
],
975+
code: `
976+
const f1 = (x: boolean | null | undefined) => x ? 1 : 0;
977+
const f2 = (x?: boolean) => x ? 1 : 0;
978+
const f3 = (x: boolean | {}) => x ? 1 : 0;
979+
`,
980+
},
928981
{
929982
options: [{ ignoreRhs: true }],
930983
errors: [
@@ -965,5 +1018,76 @@ const objAndBool = obj && bool;
9651018
const f = (x?: number) => x ? 1 : 0;
9661019
`,
9671020
},
1021+
{
1022+
options: [{ allowSafe: true }],
1023+
errors: [
1024+
{
1025+
messageId: 'strictBooleanExpression',
1026+
line: 3,
1027+
column: 42,
1028+
},
1029+
{
1030+
messageId: 'strictBooleanExpression',
1031+
line: 4,
1032+
column: 42,
1033+
},
1034+
{
1035+
messageId: 'strictBooleanExpression',
1036+
line: 5,
1037+
column: 44,
1038+
},
1039+
],
1040+
code: `
1041+
type Type = { a: string; };
1042+
const f1 = (x: Type | string) => x ? 1 : 0;
1043+
const f2 = (x: Type | number) => x ? 1 : 0;
1044+
const f3 = (x: number | string) => x ? 1 : 0;
1045+
`,
1046+
},
1047+
{
1048+
options: [{ allowSafe: true }],
1049+
errors: [
1050+
{
1051+
messageId: 'strictBooleanExpression',
1052+
line: 8,
1053+
column: 34,
1054+
},
1055+
{
1056+
messageId: 'strictBooleanExpression',
1057+
line: 9,
1058+
column: 34,
1059+
},
1060+
],
1061+
code: `
1062+
enum Enum1 {
1063+
A, B, C
1064+
}
1065+
enum Enum2 {
1066+
A = 'A', B = 'B', C = 'C'
1067+
}
1068+
const f1 = (x: Enum1) => x ? 1 : 0;
1069+
const f2 = (x: Enum2) => x ? 1 : 0;
1070+
`,
1071+
},
1072+
{
1073+
options: [{ allowNullable: true, allowSafe: true }],
1074+
errors: [
1075+
{
1076+
messageId: 'strictBooleanExpression',
1077+
line: 3,
1078+
column: 43,
1079+
},
1080+
{
1081+
messageId: 'strictBooleanExpression',
1082+
line: 4,
1083+
column: 49,
1084+
},
1085+
],
1086+
code: `
1087+
type Type = { a: string; };
1088+
const f1 = (x?: Type | string) => x ? 1 : 0;
1089+
const f2 = (x: Type | number | null) => x ? 1 : 0;
1090+
`,
1091+
},
9681092
],
9691093
});

0 commit comments

Comments
 (0)