@@ -6,16 +6,33 @@ import * as tsutils from 'tsutils';
66import * as ts from 'typescript' ;
77import * as util from '../util' ;
88
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+ ] ;
1022
1123interface BooleanComparison {
1224 expression : TSESTree . Expression ;
25+ literalBooleanInComparison : boolean ;
1326 forTruthy : boolean ;
1427 negated : boolean ;
1528 range : [ number , number ] ;
1629}
1730
18- export default util . createRule < [ ] , MessageIds > ( {
31+ interface BooleanComparisonWithTypeInformation extends BooleanComparison {
32+ expressionIsNullableBoolean : boolean ;
33+ }
34+
35+ export default util . createRule < Options , MessageIds > ( {
1936 name : 'no-unnecessary-boolean-literal-compare' ,
2037 meta : {
2138 docs : {
@@ -31,18 +48,42 @@ export default util.createRule<[], MessageIds>({
3148 'This expression unnecessarily compares a boolean value to a boolean instead of using it directly.' ,
3249 negated :
3350 '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.' ,
3457 } ,
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+ ] ,
3672 type : 'suggestion' ,
3773 } ,
38- defaultOptions : [ ] ,
39- create ( context ) {
74+ defaultOptions : [
75+ {
76+ allowComparingNullableBooleansToTrue : true ,
77+ allowComparingNullableBooleansToFalse : true ,
78+ } ,
79+ ] ,
80+ create ( context , [ options ] ) {
4081 const parserServices = util . getParserServices ( context ) ;
4182 const checker = parserServices . program . getTypeChecker ( ) ;
4283
4384 function getBooleanComparison (
4485 node : TSESTree . BinaryExpression ,
45- ) : BooleanComparison | undefined {
86+ ) : BooleanComparisonWithTypeInformation | undefined {
4687 const comparison = deconstructComparison ( node ) ;
4788 if ( ! comparison ) {
4889 return undefined ;
@@ -52,16 +93,67 @@ export default util.createRule<[], MessageIds>({
5293 parserServices . esTreeNodeToTSNodeMap . get ( comparison . expression ) ,
5394 ) ;
5495
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+ } ;
62108 }
63109
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 ;
65157 }
66158
67159 function deconstructComparison (
@@ -83,11 +175,12 @@ export default util.createRule<[], MessageIds>({
83175 continue ;
84176 }
85177
86- const { value } = against ;
87- const negated = node . operator . startsWith ( '!' ) ;
178+ const { value : literalBooleanInComparison } = against ;
179+ const negated = ! comparisonType . isPositive ;
88180
89181 return {
90- forTruthy : value ? ! negated : negated ,
182+ literalBooleanInComparison,
183+ forTruthy : literalBooleanInComparison ? ! negated : negated ,
91184 expression,
92185 negated,
93186 range :
@@ -100,23 +193,85 @@ export default util.createRule<[], MessageIds>({
100193 return undefined ;
101194 }
102195
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+
103204 return {
104205 BinaryExpression ( node ) : void {
105206 const comparison = getBooleanComparison ( node ) ;
207+ if ( comparison === undefined ) {
208+ return ;
209+ }
106210
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+ }
111225
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+ ) {
112236 if ( ! comparison . forTruthy ) {
113237 yield fixer . insertTextBefore ( node , '!' ) ;
114238 }
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+ } ) ;
120275 } ,
121276 } ;
122277 } ,
0 commit comments