@@ -6,16 +6,33 @@ import * as tsutils from 'tsutils';
6
6
import * as ts from 'typescript' ;
7
7
import * as util from '../util' ;
8
8
9
- type MessageIds = 'direct' | 'negated' ;
9
+ type MessageIds =
10
+ | 'direct'
11
+ | 'negated'
12
+ | 'comparingNullableToTrueDirect'
13
+ | 'comparingNullableToTrueNegated'
14
+ | 'comparingNullableToFalse' ;
15
+
16
+ type Options = [
17
+ {
18
+ allowComparingNullableBooleansToTrue ?: boolean ;
19
+ allowComparingNullableBooleansToFalse ?: boolean ;
20
+ } ,
21
+ ] ;
10
22
11
23
interface BooleanComparison {
12
24
expression : TSESTree . Expression ;
25
+ literalBooleanInComparison : boolean ;
13
26
forTruthy : boolean ;
14
27
negated : boolean ;
15
28
range : [ number , number ] ;
16
29
}
17
30
18
- export default util . createRule < [ ] , MessageIds > ( {
31
+ interface BooleanComparisonWithTypeInformation extends BooleanComparison {
32
+ expressionIsNullableBoolean : boolean ;
33
+ }
34
+
35
+ export default util . createRule < Options , MessageIds > ( {
19
36
name : 'no-unnecessary-boolean-literal-compare' ,
20
37
meta : {
21
38
docs : {
@@ -31,18 +48,42 @@ export default util.createRule<[], MessageIds>({
31
48
'This expression unnecessarily compares a boolean value to a boolean instead of using it directly.' ,
32
49
negated :
33
50
'This expression unnecessarily compares a boolean value to a boolean instead of negating it.' ,
51
+ comparingNullableToTrueDirect :
52
+ 'This expression unnecessarily compares a nullable boolean value to true instead of using it directly.' ,
53
+ comparingNullableToTrueNegated :
54
+ 'This expression unnecessarily compares a nullable boolean value to true instead of negating it.' ,
55
+ comparingNullableToFalse :
56
+ 'This expression unnecessarily compares a nullable boolean value to false instead of using the ?? operator to provide a default.' ,
34
57
} ,
35
- schema : [ ] ,
58
+ schema : [
59
+ {
60
+ type : 'object' ,
61
+ properties : {
62
+ allowComparingNullableBooleansToTrue : {
63
+ type : 'boolean' ,
64
+ } ,
65
+ allowComparingNullableBooleansToFalse : {
66
+ type : 'boolean' ,
67
+ } ,
68
+ } ,
69
+ additionalProperties : false ,
70
+ } ,
71
+ ] ,
36
72
type : 'suggestion' ,
37
73
} ,
38
- defaultOptions : [ ] ,
39
- create ( context ) {
74
+ defaultOptions : [
75
+ {
76
+ allowComparingNullableBooleansToTrue : true ,
77
+ allowComparingNullableBooleansToFalse : true ,
78
+ } ,
79
+ ] ,
80
+ create ( context , [ options ] ) {
40
81
const parserServices = util . getParserServices ( context ) ;
41
82
const checker = parserServices . program . getTypeChecker ( ) ;
42
83
43
84
function getBooleanComparison (
44
85
node : TSESTree . BinaryExpression ,
45
- ) : BooleanComparison | undefined {
86
+ ) : BooleanComparisonWithTypeInformation | undefined {
46
87
const comparison = deconstructComparison ( node ) ;
47
88
if ( ! comparison ) {
48
89
return undefined ;
@@ -52,16 +93,67 @@ export default util.createRule<[], MessageIds>({
52
93
parserServices . esTreeNodeToTSNodeMap . get ( comparison . expression ) ,
53
94
) ;
54
95
55
- if (
56
- ! tsutils . isTypeFlagSet (
57
- expressionType ,
58
- ts . TypeFlags . Boolean | ts . TypeFlags . BooleanLiteral ,
59
- )
60
- ) {
61
- return undefined ;
96
+ if ( isBooleanType ( expressionType ) ) {
97
+ return {
98
+ ...comparison ,
99
+ expressionIsNullableBoolean : false ,
100
+ } ;
101
+ }
102
+
103
+ if ( isNullableBoolean ( expressionType ) ) {
104
+ return {
105
+ ...comparison ,
106
+ expressionIsNullableBoolean : true ,
107
+ } ;
62
108
}
63
109
64
- return comparison ;
110
+ return undefined ;
111
+ }
112
+
113
+ function isBooleanType ( expressionType : ts . Type ) : boolean {
114
+ return tsutils . isTypeFlagSet (
115
+ expressionType ,
116
+ ts . TypeFlags . Boolean | ts . TypeFlags . BooleanLiteral ,
117
+ ) ;
118
+ }
119
+
120
+ /**
121
+ * checks if the expressionType is a union that
122
+ * 1) contains at least one nullish type (null or undefined)
123
+ * 2) contains at least once boolean type (true or false or boolean)
124
+ * 3) does not contain any types besides nullish and boolean types
125
+ */
126
+ function isNullableBoolean ( expressionType : ts . Type ) : boolean {
127
+ if ( ! expressionType . isUnion ( ) ) {
128
+ return false ;
129
+ }
130
+
131
+ const { types } = expressionType ;
132
+
133
+ const nonNullishTypes = types . filter (
134
+ type =>
135
+ ! tsutils . isTypeFlagSet (
136
+ type ,
137
+ ts . TypeFlags . Undefined | ts . TypeFlags . Null ,
138
+ ) ,
139
+ ) ;
140
+
141
+ const hasNonNullishType = nonNullishTypes . length > 0 ;
142
+ if ( ! hasNonNullishType ) {
143
+ return false ;
144
+ }
145
+
146
+ const hasNullableType = nonNullishTypes . length < types . length ;
147
+ if ( ! hasNullableType ) {
148
+ return false ;
149
+ }
150
+
151
+ const allNonNullishTypesAreBoolean = nonNullishTypes . every ( isBooleanType ) ;
152
+ if ( ! allNonNullishTypesAreBoolean ) {
153
+ return false ;
154
+ }
155
+
156
+ return true ;
65
157
}
66
158
67
159
function deconstructComparison (
@@ -83,11 +175,12 @@ export default util.createRule<[], MessageIds>({
83
175
continue ;
84
176
}
85
177
86
- const { value } = against ;
87
- const negated = node . operator . startsWith ( '!' ) ;
178
+ const { value : literalBooleanInComparison } = against ;
179
+ const negated = ! comparisonType . isPositive ;
88
180
89
181
return {
90
- forTruthy : value ? ! negated : negated ,
182
+ literalBooleanInComparison,
183
+ forTruthy : literalBooleanInComparison ? ! negated : negated ,
91
184
expression,
92
185
negated,
93
186
range :
@@ -100,23 +193,85 @@ export default util.createRule<[], MessageIds>({
100
193
return undefined ;
101
194
}
102
195
196
+ function nodeIsUnaryNegation ( node : TSESTree . Node ) : boolean {
197
+ return (
198
+ node . type === AST_NODE_TYPES . UnaryExpression &&
199
+ node . prefix &&
200
+ node . operator === '!'
201
+ ) ;
202
+ }
203
+
103
204
return {
104
205
BinaryExpression ( node ) : void {
105
206
const comparison = getBooleanComparison ( node ) ;
207
+ if ( comparison === undefined ) {
208
+ return ;
209
+ }
106
210
107
- if ( comparison ) {
108
- context . report ( {
109
- fix : function * ( fixer ) {
110
- yield fixer . removeRange ( comparison . range ) ;
211
+ if ( comparison . expressionIsNullableBoolean ) {
212
+ if (
213
+ comparison . literalBooleanInComparison &&
214
+ options . allowComparingNullableBooleansToTrue
215
+ ) {
216
+ return ;
217
+ }
218
+ if (
219
+ ! comparison . literalBooleanInComparison &&
220
+ options . allowComparingNullableBooleansToFalse
221
+ ) {
222
+ return ;
223
+ }
224
+ }
111
225
226
+ context . report ( {
227
+ fix : function * ( fixer ) {
228
+ yield fixer . removeRange ( comparison . range ) ;
229
+
230
+ // if the expression `exp` isn't nullable, or we're comparing to `true`,
231
+ // we can just replace the entire comparison with `exp` or `!exp`
232
+ if (
233
+ ! comparison . expressionIsNullableBoolean ||
234
+ comparison . literalBooleanInComparison
235
+ ) {
112
236
if ( ! comparison . forTruthy ) {
113
237
yield fixer . insertTextBefore ( node , '!' ) ;
114
238
}
115
- } ,
116
- messageId : comparison . negated ? 'negated' : 'direct' ,
117
- node,
118
- } ) ;
119
- }
239
+ return ;
240
+ }
241
+
242
+ // if we're here, then the expression is a nullable boolean and we're
243
+ // comparing to a literal `false`
244
+
245
+ // if we're doing `== false` or `=== false`, then we need to negate the expression
246
+ if ( ! comparison . negated ) {
247
+ const { parent } = node ;
248
+ // if the parent is a negation, we can instead just get rid of the parent's negation.
249
+ // i.e. instead of resulting in `!(!(exp))`, we can just result in `exp`
250
+ if ( parent != null && nodeIsUnaryNegation ( parent ) ) {
251
+ // remove from the beginning of the parent to the beginning of this node
252
+ yield fixer . removeRange ( [ parent . range [ 0 ] , node . range [ 0 ] ] ) ;
253
+ // remove from the end of the node to the end of the parent
254
+ yield fixer . removeRange ( [ node . range [ 1 ] , parent . range [ 1 ] ] ) ;
255
+ } else {
256
+ yield fixer . insertTextBefore ( node , '!' ) ;
257
+ }
258
+ }
259
+
260
+ // provide the default `true`
261
+ yield fixer . insertTextBefore ( node , '(' ) ;
262
+ yield fixer . insertTextAfter ( node , ' ?? true)' ) ;
263
+ } ,
264
+ messageId : comparison . expressionIsNullableBoolean
265
+ ? comparison . literalBooleanInComparison
266
+ ? comparison . negated
267
+ ? 'comparingNullableToTrueNegated'
268
+ : 'comparingNullableToTrueDirect'
269
+ : 'comparingNullableToFalse'
270
+ : comparison . negated
271
+ ? 'negated'
272
+ : 'direct' ,
273
+ node,
274
+ } ) ;
120
275
} ,
121
276
} ;
122
277
} ,
0 commit comments