@@ -31,6 +31,17 @@ const isPossiblyFalsy = (type: ts.Type): boolean =>
31
31
const isPossiblyTruthy = ( type : ts . Type ) : boolean =>
32
32
unionTypeParts ( type ) . some ( type => ! isFalsyType ( type ) ) ;
33
33
34
+ // Nullish utilities
35
+ const nullishFlag = ts . TypeFlags . Undefined | ts . TypeFlags . Null ;
36
+ const isNullishType = ( type : ts . Type ) : boolean =>
37
+ isTypeFlagSet ( type , nullishFlag ) ;
38
+
39
+ const isPossiblyNullish = ( type : ts . Type ) : boolean =>
40
+ unionTypeParts ( type ) . some ( isNullishType ) ;
41
+
42
+ const isAlwaysNullish = ( type : ts . Type ) : boolean =>
43
+ unionTypeParts ( type ) . every ( isNullishType ) ;
44
+
34
45
// isLiteralType only covers numbers and strings, this is a more exhaustive check.
35
46
const isLiteral = ( type : ts . Type ) : boolean =>
36
47
isBooleanLiteralType ( type , true ) ||
@@ -51,6 +62,8 @@ export type Options = [
51
62
export type MessageId =
52
63
| 'alwaysTruthy'
53
64
| 'alwaysFalsy'
65
+ | 'neverNullish'
66
+ | 'alwaysNullish'
54
67
| 'literalBooleanExpression'
55
68
| 'never' ;
56
69
export default createRule < Options , MessageId > ( {
@@ -81,6 +94,10 @@ export default createRule<Options, MessageId>({
81
94
messages : {
82
95
alwaysTruthy : 'Unnecessary conditional, value is always truthy.' ,
83
96
alwaysFalsy : 'Unnecessary conditional, value is always falsy.' ,
97
+ neverNullish :
98
+ 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.' ,
99
+ alwaysNullish :
100
+ 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`' ,
84
101
literalBooleanExpression :
85
102
'Unnecessary conditional, both sides of the expression are literal values' ,
86
103
never : 'Unnecessary conditional, value is `never`' ,
@@ -120,12 +137,35 @@ export default createRule<Options, MessageId>({
120
137
) {
121
138
return ;
122
139
}
123
- if ( isTypeFlagSet ( type , TypeFlags . Never ) ) {
124
- context . report ( { node, messageId : 'never' } ) ;
125
- } else if ( ! isPossiblyTruthy ( type ) ) {
126
- context . report ( { node, messageId : 'alwaysFalsy' } ) ;
127
- } else if ( ! isPossiblyFalsy ( type ) ) {
128
- context . report ( { node, messageId : 'alwaysTruthy' } ) ;
140
+ const messageId = isTypeFlagSet ( type , TypeFlags . Never )
141
+ ? 'never'
142
+ : ! isPossiblyTruthy ( type )
143
+ ? 'alwaysFalsy'
144
+ : ! isPossiblyFalsy ( type )
145
+ ? 'alwaysTruthy'
146
+ : undefined ;
147
+
148
+ if ( messageId ) {
149
+ context . report ( { node, messageId } ) ;
150
+ }
151
+ }
152
+
153
+ function checkNodeForNullish ( node : TSESTree . Node ) : void {
154
+ const type = getNodeType ( node ) ;
155
+ // Conditional is always necessary if it involves `any` or `unknown`
156
+ if ( isTypeFlagSet ( type , TypeFlags . Any | TypeFlags . Unknown ) ) {
157
+ return ;
158
+ }
159
+ const messageId = isTypeFlagSet ( type , TypeFlags . Never )
160
+ ? 'never'
161
+ : ! isPossiblyNullish ( type )
162
+ ? 'neverNullish'
163
+ : isAlwaysNullish ( type )
164
+ ? 'alwaysNullish'
165
+ : undefined ;
166
+
167
+ if ( messageId ) {
168
+ context . report ( { node, messageId } ) ;
129
169
}
130
170
}
131
171
@@ -178,6 +218,10 @@ export default createRule<Options, MessageId>({
178
218
function checkLogicalExpressionForUnnecessaryConditionals (
179
219
node : TSESTree . LogicalExpression ,
180
220
) : void {
221
+ if ( node . operator === '??' ) {
222
+ checkNodeForNullish ( node . left ) ;
223
+ return ;
224
+ }
181
225
checkNode ( node . left ) ;
182
226
if ( ! ignoreRhs ) {
183
227
checkNode ( node . right ) ;
0 commit comments