@@ -23,17 +23,17 @@ export default util.createRule<Options, MessageIds>({
23
23
type : 'suggestion' ,
24
24
docs : {
25
25
description :
26
- 'Enforce using the nullish coalescing operator instead of logical chaining' ,
26
+ 'Enforce using the nullish coalescing operator instead of logical assignments or chaining' ,
27
27
recommended : 'strict' ,
28
28
requiresTypeChecking : true ,
29
29
} ,
30
30
hasSuggestions : true ,
31
31
messages : {
32
32
preferNullishOverOr :
33
- 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.' ,
33
+ 'Prefer using nullish coalescing operator (`??{{ equals }} `) instead of a logical {{ description }} (`||{{ equals }} `), as it is a safer operator.' ,
34
34
preferNullishOverTernary :
35
- 'Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read.' ,
36
- suggestNullish : 'Fix to nullish coalescing operator (`??`).' ,
35
+ 'Prefer using nullish coalescing operator (`??{{ equals }} `) instead of a ternary expression, as it is simpler to read.' ,
36
+ suggestNullish : 'Fix to nullish coalescing operator (`??{{ equals }} `).' ,
37
37
} ,
38
38
schema : [
39
39
{
@@ -74,6 +74,75 @@ export default util.createRule<Options, MessageIds>({
74
74
const sourceCode = context . getSourceCode ( ) ;
75
75
const checker = parserServices . program . getTypeChecker ( ) ;
76
76
77
+ // todo: rename to something more specific?
78
+ function checkAssignmentOrLogicalExpression (
79
+ node : TSESTree . AssignmentExpression | TSESTree . LogicalExpression ,
80
+ description : string ,
81
+ equals : string ,
82
+ ) : void {
83
+ const tsNode = parserServices . esTreeNodeToTSNodeMap . get ( node ) ;
84
+ const type = checker . getTypeAtLocation ( tsNode . left ) ;
85
+ const isNullish = util . isNullableType ( type , { allowUndefined : true } ) ;
86
+ if ( ! isNullish ) {
87
+ return ;
88
+ }
89
+
90
+ if ( ignoreConditionalTests === true && isConditionalTest ( node ) ) {
91
+ return ;
92
+ }
93
+
94
+ if (
95
+ ignoreMixedLogicalExpressions === true &&
96
+ isMixedLogicalExpression ( node )
97
+ ) {
98
+ return ;
99
+ }
100
+
101
+ const barBarOperator = util . nullThrows (
102
+ sourceCode . getTokenAfter (
103
+ node . left ,
104
+ token =>
105
+ token . type === AST_TOKEN_TYPES . Punctuator &&
106
+ token . value === node . operator ,
107
+ ) ,
108
+ util . NullThrowsReasons . MissingToken ( 'operator' , node . type ) ,
109
+ ) ;
110
+
111
+ function * fix (
112
+ fixer : TSESLint . RuleFixer ,
113
+ ) : IterableIterator < TSESLint . RuleFix > {
114
+ if ( node . parent && util . isLogicalOrOperator ( node . parent ) ) {
115
+ // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)
116
+ if (
117
+ node . left . type === AST_NODE_TYPES . LogicalExpression &&
118
+ ! util . isLogicalOrOperator ( node . left . left )
119
+ ) {
120
+ yield fixer . insertTextBefore ( node . left . right , '(' ) ;
121
+ } else {
122
+ yield fixer . insertTextBefore ( node . left , '(' ) ;
123
+ }
124
+ yield fixer . insertTextAfter ( node . right , ')' ) ;
125
+ }
126
+ yield fixer . replaceText (
127
+ barBarOperator ,
128
+ node . operator . replace ( '||' , '??' ) ,
129
+ ) ;
130
+ }
131
+
132
+ context . report ( {
133
+ data : { equals, description } ,
134
+ node : barBarOperator ,
135
+ messageId : 'preferNullishOverOr' ,
136
+ suggest : [
137
+ {
138
+ data : { equals } ,
139
+ messageId : 'suggestNullish' ,
140
+ fix,
141
+ } ,
142
+ ] ,
143
+ } ) ;
144
+ }
145
+
77
146
return {
78
147
ConditionalExpression ( node : TSESTree . ConditionalExpression ) : void {
79
148
if ( ignoreTernaryTests ) {
@@ -103,7 +172,7 @@ export default util.createRule<Options, MessageIds>({
103
172
node . test . right . left ,
104
173
node . test . right . right ,
105
174
] ;
106
- if ( node . test . operator === '||' ) {
175
+ if ( [ '||' , '||=' ] . includes ( node . test . operator ) ) {
107
176
if (
108
177
node . test . left . operator === '===' &&
109
178
node . test . right . operator === '==='
@@ -205,10 +274,13 @@ export default util.createRule<Options, MessageIds>({
205
274
206
275
if ( isFixable ) {
207
276
context . report ( {
277
+ // TODO: also account for = in the ternary clause
278
+ data : { equals : '' } ,
208
279
node,
209
280
messageId : 'preferNullishOverTernary' ,
210
281
suggest : [
211
282
{
283
+ data : { equals : '' } ,
212
284
messageId : 'suggestNullish' ,
213
285
fix ( fixer : TSESLint . RuleFixer ) : TSESLint . RuleFix {
214
286
const [ left , right ] =
@@ -231,64 +303,15 @@ export default util.createRule<Options, MessageIds>({
231
303
} ) ;
232
304
}
233
305
} ,
234
-
306
+ 'AssignmentExpression[operator = "||="]' (
307
+ node : TSESTree . AssignmentExpression ,
308
+ ) : void {
309
+ checkAssignmentOrLogicalExpression ( node , 'assignment' , '=' ) ;
310
+ } ,
235
311
'LogicalExpression[operator = "||"]' (
236
312
node : TSESTree . LogicalExpression ,
237
313
) : void {
238
- const tsNode = parserServices . esTreeNodeToTSNodeMap . get ( node ) ;
239
- const type = checker . getTypeAtLocation ( tsNode . left ) ;
240
- const isNullish = util . isNullableType ( type , { allowUndefined : true } ) ;
241
- if ( ! isNullish ) {
242
- return ;
243
- }
244
-
245
- if ( ignoreConditionalTests === true && isConditionalTest ( node ) ) {
246
- return ;
247
- }
248
-
249
- const isMixedLogical = isMixedLogicalExpression ( node ) ;
250
- if ( ignoreMixedLogicalExpressions === true && isMixedLogical ) {
251
- return ;
252
- }
253
-
254
- const barBarOperator = util . nullThrows (
255
- sourceCode . getTokenAfter (
256
- node . left ,
257
- token =>
258
- token . type === AST_TOKEN_TYPES . Punctuator &&
259
- token . value === node . operator ,
260
- ) ,
261
- util . NullThrowsReasons . MissingToken ( 'operator' , node . type ) ,
262
- ) ;
263
-
264
- function * fix (
265
- fixer : TSESLint . RuleFixer ,
266
- ) : IterableIterator < TSESLint . RuleFix > {
267
- if ( node . parent && util . isLogicalOrOperator ( node . parent ) ) {
268
- // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)
269
- if (
270
- node . left . type === AST_NODE_TYPES . LogicalExpression &&
271
- ! util . isLogicalOrOperator ( node . left . left )
272
- ) {
273
- yield fixer . insertTextBefore ( node . left . right , '(' ) ;
274
- } else {
275
- yield fixer . insertTextBefore ( node . left , '(' ) ;
276
- }
277
- yield fixer . insertTextAfter ( node . right , ')' ) ;
278
- }
279
- yield fixer . replaceText ( barBarOperator , '??' ) ;
280
- }
281
-
282
- context . report ( {
283
- node : barBarOperator ,
284
- messageId : 'preferNullishOverOr' ,
285
- suggest : [
286
- {
287
- messageId : 'suggestNullish' ,
288
- fix,
289
- } ,
290
- ] ,
291
- } ) ;
314
+ checkAssignmentOrLogicalExpression ( node , 'or' , '' ) ;
292
315
} ,
293
316
} ;
294
317
} ,
@@ -331,7 +354,9 @@ function isConditionalTest(node: TSESTree.Node): boolean {
331
354
return false ;
332
355
}
333
356
334
- function isMixedLogicalExpression ( node : TSESTree . LogicalExpression ) : boolean {
357
+ function isMixedLogicalExpression (
358
+ node : TSESTree . AssignmentExpression | TSESTree . LogicalExpression ,
359
+ ) : boolean {
335
360
const seen = new Set < TSESTree . Node | undefined > ( ) ;
336
361
const queue = [ node . parent , node . left , node . right ] ;
337
362
for ( const current of queue ) {
@@ -343,7 +368,7 @@ function isMixedLogicalExpression(node: TSESTree.LogicalExpression): boolean {
343
368
if ( current && current . type === AST_NODE_TYPES . LogicalExpression ) {
344
369
if ( current . operator === '&&' ) {
345
370
return true ;
346
- } else if ( current . operator === '||' ) {
371
+ } else if ( [ '||' , '||=' ] . includes ( current . operator ) ) {
347
372
// check the pieces of the node to catch cases like `a || b || c && d`
348
373
queue . push ( current . parent , current . left , current . right ) ;
349
374
}
0 commit comments