@typescript-eslint/eslint-plugin 8.27.1-alpha.2 → 8.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rules/consistent-type-exports.d.ts.map +1 -1
- package/dist/rules/consistent-type-exports.js +4 -6
- package/dist/rules/no-unsafe-function-type.d.ts.map +1 -1
- package/dist/rules/no-unsafe-function-type.js +0 -1
- package/dist/rules/no-unsafe-return.d.ts.map +1 -1
- package/dist/rules/no-unsafe-return.js +1 -3
- package/dist/rules/prefer-nullish-coalescing.d.ts +1 -1
- package/dist/rules/prefer-nullish-coalescing.d.ts.map +1 -1
- package/dist/rules/prefer-nullish-coalescing.js +231 -131
- package/docs/rules/prefer-nullish-coalescing.mdx +49 -0
- package/package.json +7 -7
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"consistent-type-exports.d.ts","sourceRoot":"","sources":["../../src/rules/consistent-type-exports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAC;AAgBnE,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,sCAAsC,EAAE,OAAO,CAAC;KACjD;CACF,CAAC;AAgBF,MAAM,MAAM,UAAU,GAClB,yBAAyB,GACzB,oBAAoB,GACpB,eAAe,CAAC;;AAEpB,
|
1
|
+
{"version":3,"file":"consistent-type-exports.d.ts","sourceRoot":"","sources":["../../src/rules/consistent-type-exports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAC;AAgBnE,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,sCAAsC,EAAE,OAAO,CAAC;KACjD;CACF,CAAC;AAgBF,MAAM,MAAM,UAAU,GAClB,yBAAyB,GACzB,oBAAoB,GACpB,eAAe,CAAC;;AAEpB,wBA8RG"}
|
@@ -158,14 +158,12 @@ exports.default = (0, util_1.createRule)({
|
|
158
158
|
// Cache the first encountered exports for the package. We will need to come
|
159
159
|
// back to these later when fixing the problems.
|
160
160
|
if (node.exportKind === 'type') {
|
161
|
-
|
162
|
-
|
163
|
-
sourceExports.typeOnlyNamedExport = node;
|
164
|
-
}
|
161
|
+
// The export is a type export
|
162
|
+
sourceExports.typeOnlyNamedExport ??= node;
|
165
163
|
}
|
166
|
-
else
|
164
|
+
else {
|
167
165
|
// The export is a value export
|
168
|
-
sourceExports.valueOnlyNamedExport
|
166
|
+
sourceExports.valueOnlyNamedExport ??= node;
|
169
167
|
}
|
170
168
|
// Next for the current export, we will separate type/value specifiers.
|
171
169
|
const typeBasedSpecifiers = [];
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"no-unsafe-function-type.d.ts","sourceRoot":"","sources":["../../src/rules/no-unsafe-function-type.ts"],"names":[],"mappings":";AAMA,
|
1
|
+
{"version":3,"file":"no-unsafe-function-type.d.ts","sourceRoot":"","sources":["../../src/rules/no-unsafe-function-type.ts"],"names":[],"mappings":";AAMA,wBA2CG"}
|
@@ -10,7 +10,6 @@ exports.default = (0, util_1.createRule)({
|
|
10
10
|
description: 'Disallow using the unsafe built-in Function type',
|
11
11
|
recommended: 'recommended',
|
12
12
|
},
|
13
|
-
fixable: 'code',
|
14
13
|
messages: {
|
15
14
|
bannedFunctionType: [
|
16
15
|
'The `Function` type accepts any function-like value.',
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"no-unsafe-return.d.ts","sourceRoot":"","sources":["../../src/rules/no-unsafe-return.ts"],"names":[],"mappings":";AAqBA,
|
1
|
+
{"version":3,"file":"no-unsafe-return.d.ts","sourceRoot":"","sources":["../../src/rules/no-unsafe-return.ts"],"names":[],"mappings":";AAqBA,wBAsMG"}
|
@@ -81,9 +81,7 @@ exports.default = (0, util_1.createRule)({
|
|
81
81
|
ts.isArrowFunction(functionTSNode)
|
82
82
|
? (0, util_1.getContextualType)(checker, functionTSNode)
|
83
83
|
: services.getTypeAtLocation(functionNode);
|
84
|
-
|
85
|
-
functionType = services.getTypeAtLocation(functionNode);
|
86
|
-
}
|
84
|
+
functionType ??= services.getTypeAtLocation(functionNode);
|
87
85
|
const callSignatures = tsutils.getCallSignaturesOfType(functionType);
|
88
86
|
// If there is an explicit type annotation *and* that type matches the actual
|
89
87
|
// function return type, we shouldn't complain (it's intentional, even if unsafe)
|
@@ -14,7 +14,7 @@ export type Options = [
|
|
14
14
|
ignoreTernaryTests?: boolean;
|
15
15
|
}
|
16
16
|
];
|
17
|
-
export type MessageIds = 'noStrictNullCheck' | 'preferNullishOverOr' | 'preferNullishOverTernary' | 'suggestNullish';
|
17
|
+
export type MessageIds = 'noStrictNullCheck' | 'preferNullishOverAssignment' | 'preferNullishOverOr' | 'preferNullishOverTernary' | 'suggestNullish';
|
18
18
|
declare const _default: TSESLint.RuleModule<MessageIds, Options, import("../../rules").ESLintPluginDocs, TSESLint.RuleListener>;
|
19
19
|
export default _default;
|
20
20
|
//# sourceMappingURL=prefer-nullish-coalescing.d.ts.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"prefer-nullish-coalescing.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-nullish-coalescing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAC;
|
1
|
+
{"version":3,"file":"prefer-nullish-coalescing.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-nullish-coalescing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAC;AAqCnE,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,sDAAsD,CAAC,EAAE,OAAO,CAAC;QACjE,qBAAqB,CAAC,EAAE,OAAO,CAAC;QAChC,sBAAsB,CAAC,EAAE,OAAO,CAAC;QACjC,6BAA6B,CAAC,EAAE,OAAO,CAAC;QACxC,gBAAgB,CAAC,EACb;YACE,MAAM,CAAC,EAAE,OAAO,CAAC;YACjB,OAAO,CAAC,EAAE,OAAO,CAAC;YAClB,MAAM,CAAC,EAAE,OAAO,CAAC;YACjB,MAAM,CAAC,EAAE,OAAO,CAAC;SAClB,GACD,IAAI,CAAC;QACT,kBAAkB,CAAC,EAAE,OAAO,CAAC;KAC9B;CACF,CAAC;AAEF,MAAM,MAAM,UAAU,GAClB,mBAAmB,GACnB,6BAA6B,GAC7B,qBAAqB,GACrB,0BAA0B,GAC1B,gBAAgB,CAAC;;AAErB,wBAujBG"}
|
@@ -37,11 +37,14 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
37
37
|
const tsutils = __importStar(require("ts-api-utils"));
|
38
38
|
const ts = __importStar(require("typescript"));
|
39
39
|
const util_1 = require("../util");
|
40
|
-
const
|
40
|
+
const isMemberAccessLike = (0, util_1.isNodeOfTypes)([
|
41
41
|
utils_1.AST_NODE_TYPES.ChainExpression,
|
42
42
|
utils_1.AST_NODE_TYPES.Identifier,
|
43
43
|
utils_1.AST_NODE_TYPES.MemberExpression,
|
44
44
|
]);
|
45
|
+
const isNullLiteralOrUndefinedIdentifier = (node) => (0, util_1.isNullLiteral)(node) || (0, util_1.isUndefinedIdentifier)(node);
|
46
|
+
const isNodeNullishComparison = (node) => isNullLiteralOrUndefinedIdentifier(node.left) &&
|
47
|
+
isNullLiteralOrUndefinedIdentifier(node.right);
|
45
48
|
exports.default = (0, util_1.createRule)({
|
46
49
|
name: 'prefer-nullish-coalescing',
|
47
50
|
meta: {
|
@@ -54,6 +57,7 @@ exports.default = (0, util_1.createRule)({
|
|
54
57
|
hasSuggestions: true,
|
55
58
|
messages: {
|
56
59
|
noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
|
60
|
+
preferNullishOverAssignment: 'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of an assignment expression, as it is simpler to read.',
|
57
61
|
preferNullishOverOr: 'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of a logical {{ description }} (`||{{ equals }}`), as it is a safer operator.',
|
58
62
|
preferNullishOverTernary: 'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of a ternary expression, as it is simpler to read.',
|
59
63
|
suggestNullish: 'Fix to nullish coalescing operator (`??{{ equals }}`).',
|
@@ -258,6 +262,81 @@ exports.default = (0, util_1.createRule)({
|
|
258
262
|
],
|
259
263
|
});
|
260
264
|
}
|
265
|
+
function getNullishCoalescingParams(node, nonNullishNode, nodesInsideTestExpression, operator) {
|
266
|
+
let nullishCoalescingLeftNode;
|
267
|
+
let hasTruthinessCheck = false;
|
268
|
+
let hasNullCheckWithoutTruthinessCheck = false;
|
269
|
+
let hasUndefinedCheckWithoutTruthinessCheck = false;
|
270
|
+
if (!nodesInsideTestExpression.length) {
|
271
|
+
hasTruthinessCheck = true;
|
272
|
+
nullishCoalescingLeftNode =
|
273
|
+
node.test.type === utils_1.AST_NODE_TYPES.UnaryExpression
|
274
|
+
? node.test.argument
|
275
|
+
: node.test;
|
276
|
+
if (!areNodesSimilarMemberAccess(nullishCoalescingLeftNode, nonNullishNode)) {
|
277
|
+
return { isFixable: false };
|
278
|
+
}
|
279
|
+
}
|
280
|
+
else {
|
281
|
+
// we check that the test only contains null, undefined and the identifier
|
282
|
+
for (const testNode of nodesInsideTestExpression) {
|
283
|
+
if ((0, util_1.isNullLiteral)(testNode)) {
|
284
|
+
hasNullCheckWithoutTruthinessCheck = true;
|
285
|
+
}
|
286
|
+
else if ((0, util_1.isUndefinedIdentifier)(testNode)) {
|
287
|
+
hasUndefinedCheckWithoutTruthinessCheck = true;
|
288
|
+
}
|
289
|
+
else if (areNodesSimilarMemberAccess(testNode, nonNullishNode)) {
|
290
|
+
// Only consider the first expression in a multi-part nullish check,
|
291
|
+
// as subsequent expressions might not require all the optional chaining operators.
|
292
|
+
// For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo';
|
293
|
+
// This works because `node.test` is always evaluated first in the loop
|
294
|
+
// and has the same or more necessary optional chaining operators
|
295
|
+
// than `node.alternate` or `node.consequent`.
|
296
|
+
nullishCoalescingLeftNode ??= testNode;
|
297
|
+
}
|
298
|
+
else {
|
299
|
+
return { isFixable: false };
|
300
|
+
}
|
301
|
+
}
|
302
|
+
}
|
303
|
+
if (!nullishCoalescingLeftNode) {
|
304
|
+
return { isFixable: false };
|
305
|
+
}
|
306
|
+
const isFixable = (() => {
|
307
|
+
if (hasTruthinessCheck) {
|
308
|
+
return isTruthinessCheckEligibleForPreferNullish({
|
309
|
+
node,
|
310
|
+
testNode: nullishCoalescingLeftNode,
|
311
|
+
});
|
312
|
+
}
|
313
|
+
// it is fixable if we check for both null and undefined, or not if neither
|
314
|
+
if (hasUndefinedCheckWithoutTruthinessCheck ===
|
315
|
+
hasNullCheckWithoutTruthinessCheck) {
|
316
|
+
return hasUndefinedCheckWithoutTruthinessCheck;
|
317
|
+
}
|
318
|
+
// it is fixable if we loosely check for either null or undefined
|
319
|
+
if (['==', '!='].includes(operator)) {
|
320
|
+
return true;
|
321
|
+
}
|
322
|
+
const type = parserServices.getTypeAtLocation(nullishCoalescingLeftNode);
|
323
|
+
const flags = (0, util_1.getTypeFlags)(type);
|
324
|
+
if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
|
325
|
+
return false;
|
326
|
+
}
|
327
|
+
const hasNullType = (flags & ts.TypeFlags.Null) !== 0;
|
328
|
+
// it is fixable if we check for undefined and the type is not nullable
|
329
|
+
if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) {
|
330
|
+
return true;
|
331
|
+
}
|
332
|
+
const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0;
|
333
|
+
// it is fixable if we check for null and the type can't be undefined
|
334
|
+
return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType;
|
335
|
+
})();
|
336
|
+
return isFixable
|
337
|
+
? { isFixable: true, nullishCoalescingLeftNode }
|
338
|
+
: { isFixable: false };
|
339
|
+
}
|
261
340
|
return {
|
262
341
|
'AssignmentExpression[operator = "||="]'(node) {
|
263
342
|
checkAndFixWithPreferNullishOverOr(node, 'assignment', '=');
|
@@ -266,134 +345,12 @@ exports.default = (0, util_1.createRule)({
|
|
266
345
|
if (ignoreTernaryTests) {
|
267
346
|
return;
|
268
347
|
}
|
269
|
-
|
270
|
-
|
271
|
-
if (node.test.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
272
|
-
nodesInsideTestExpression = [node.test.left, node.test.right];
|
273
|
-
if (node.test.operator === '==' ||
|
274
|
-
node.test.operator === '!=' ||
|
275
|
-
node.test.operator === '===' ||
|
276
|
-
node.test.operator === '!==') {
|
277
|
-
operator = node.test.operator;
|
278
|
-
}
|
279
|
-
}
|
280
|
-
else if (node.test.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
281
|
-
node.test.left.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
282
|
-
node.test.right.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
283
|
-
nodesInsideTestExpression = [
|
284
|
-
node.test.left.left,
|
285
|
-
node.test.left.right,
|
286
|
-
node.test.right.left,
|
287
|
-
node.test.right.right,
|
288
|
-
];
|
289
|
-
if (['||', '||='].includes(node.test.operator)) {
|
290
|
-
if (node.test.left.operator === '===' &&
|
291
|
-
node.test.right.operator === '===') {
|
292
|
-
operator = '===';
|
293
|
-
}
|
294
|
-
else if (((node.test.left.operator === '===' ||
|
295
|
-
node.test.right.operator === '===') &&
|
296
|
-
(node.test.left.operator === '==' ||
|
297
|
-
node.test.right.operator === '==')) ||
|
298
|
-
(node.test.left.operator === '==' &&
|
299
|
-
node.test.right.operator === '==')) {
|
300
|
-
operator = '==';
|
301
|
-
}
|
302
|
-
}
|
303
|
-
else if (node.test.operator === '&&') {
|
304
|
-
if (node.test.left.operator === '!==' &&
|
305
|
-
node.test.right.operator === '!==') {
|
306
|
-
operator = '!==';
|
307
|
-
}
|
308
|
-
else if (((node.test.left.operator === '!==' ||
|
309
|
-
node.test.right.operator === '!==') &&
|
310
|
-
(node.test.left.operator === '!=' ||
|
311
|
-
node.test.right.operator === '!=')) ||
|
312
|
-
(node.test.left.operator === '!=' &&
|
313
|
-
node.test.right.operator === '!=')) {
|
314
|
-
operator = '!=';
|
315
|
-
}
|
316
|
-
}
|
317
|
-
}
|
318
|
-
let nullishCoalescingLeftNode;
|
319
|
-
let hasTruthinessCheck = false;
|
320
|
-
let hasNullCheckWithoutTruthinessCheck = false;
|
321
|
-
let hasUndefinedCheckWithoutTruthinessCheck = false;
|
322
|
-
if (!operator) {
|
323
|
-
let testNode;
|
324
|
-
hasTruthinessCheck = true;
|
325
|
-
if (isIdentifierOrMemberOrChainExpression(node.test)) {
|
326
|
-
testNode = node.test;
|
327
|
-
}
|
328
|
-
else if (node.test.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
329
|
-
isIdentifierOrMemberOrChainExpression(node.test.argument) &&
|
330
|
-
node.test.operator === '!') {
|
331
|
-
testNode = node.test.argument;
|
332
|
-
operator = '!';
|
333
|
-
}
|
334
|
-
if (testNode &&
|
335
|
-
areNodesSimilarMemberAccess(testNode, getBranchNodes(node, operator).nonNullishBranch)) {
|
336
|
-
nullishCoalescingLeftNode = testNode;
|
337
|
-
}
|
338
|
-
}
|
339
|
-
else {
|
340
|
-
// we check that the test only contains null, undefined and the identifier
|
341
|
-
for (const testNode of nodesInsideTestExpression) {
|
342
|
-
if ((0, util_1.isNullLiteral)(testNode)) {
|
343
|
-
hasNullCheckWithoutTruthinessCheck = true;
|
344
|
-
}
|
345
|
-
else if ((0, util_1.isUndefinedIdentifier)(testNode)) {
|
346
|
-
hasUndefinedCheckWithoutTruthinessCheck = true;
|
347
|
-
}
|
348
|
-
else if (areNodesSimilarMemberAccess(testNode, getBranchNodes(node, operator).nonNullishBranch)) {
|
349
|
-
// Only consider the first expression in a multi-part nullish check,
|
350
|
-
// as subsequent expressions might not require all the optional chaining operators.
|
351
|
-
// For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo';
|
352
|
-
// This works because `node.test` is always evaluated first in the loop
|
353
|
-
// and has the same or more necessary optional chaining operators
|
354
|
-
// than `node.alternate` or `node.consequent`.
|
355
|
-
nullishCoalescingLeftNode ??= testNode;
|
356
|
-
}
|
357
|
-
else {
|
358
|
-
return;
|
359
|
-
}
|
360
|
-
}
|
361
|
-
}
|
362
|
-
if (!nullishCoalescingLeftNode) {
|
348
|
+
const { nodesInsideTestExpression, operator } = getOperatorAndNodesInsideTestExpression(node);
|
349
|
+
if (operator == null) {
|
363
350
|
return;
|
364
351
|
}
|
365
|
-
const
|
366
|
-
|
367
|
-
if (hasTruthinessCheck) {
|
368
|
-
return isTruthinessCheckEligibleForPreferNullish({
|
369
|
-
node,
|
370
|
-
testNode: nullishCoalescingLeftNode,
|
371
|
-
});
|
372
|
-
}
|
373
|
-
// it is fixable if we check for both null and undefined, or not if neither
|
374
|
-
if (hasUndefinedCheckWithoutTruthinessCheck ===
|
375
|
-
hasNullCheckWithoutTruthinessCheck) {
|
376
|
-
return hasUndefinedCheckWithoutTruthinessCheck;
|
377
|
-
}
|
378
|
-
// it is fixable if we loosely check for either null or undefined
|
379
|
-
if (operator === '==' || operator === '!=') {
|
380
|
-
return true;
|
381
|
-
}
|
382
|
-
const type = parserServices.getTypeAtLocation(nullishCoalescingLeftNode);
|
383
|
-
const flags = (0, util_1.getTypeFlags)(type);
|
384
|
-
if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
|
385
|
-
return false;
|
386
|
-
}
|
387
|
-
const hasNullType = (flags & ts.TypeFlags.Null) !== 0;
|
388
|
-
// it is fixable if we check for undefined and the type is not nullable
|
389
|
-
if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) {
|
390
|
-
return true;
|
391
|
-
}
|
392
|
-
const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0;
|
393
|
-
// it is fixable if we check for null and the type can't be undefined
|
394
|
-
return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType;
|
395
|
-
})();
|
396
|
-
if (isFixableWithPreferNullishOverTernary) {
|
352
|
+
const nullishCoalescingParams = getNullishCoalescingParams(node, getBranchNodes(node, operator).nonNullishBranch, nodesInsideTestExpression, operator);
|
353
|
+
if (nullishCoalescingParams.isFixable) {
|
397
354
|
context.report({
|
398
355
|
node,
|
399
356
|
messageId: 'preferNullishOverTernary',
|
@@ -404,7 +361,63 @@ exports.default = (0, util_1.createRule)({
|
|
404
361
|
messageId: 'suggestNullish',
|
405
362
|
data: { equals: '' },
|
406
363
|
fix(fixer) {
|
407
|
-
return fixer.replaceText(node, `${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingLeftNode)} ?? ${(0, util_1.getTextWithParentheses)(context.sourceCode, getBranchNodes(node, operator).nullishBranch)}`);
|
364
|
+
return fixer.replaceText(node, `${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingParams.nullishCoalescingLeftNode)} ?? ${(0, util_1.getTextWithParentheses)(context.sourceCode, getBranchNodes(node, operator).nullishBranch)}`);
|
365
|
+
},
|
366
|
+
},
|
367
|
+
],
|
368
|
+
});
|
369
|
+
}
|
370
|
+
},
|
371
|
+
IfStatement(node) {
|
372
|
+
if (node.alternate != null) {
|
373
|
+
return;
|
374
|
+
}
|
375
|
+
let assignmentExpression;
|
376
|
+
if (node.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement &&
|
377
|
+
node.consequent.body.length === 1 &&
|
378
|
+
node.consequent.body[0].type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
|
379
|
+
assignmentExpression = node.consequent.body[0].expression;
|
380
|
+
}
|
381
|
+
else if (node.consequent.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
|
382
|
+
assignmentExpression = node.consequent.expression;
|
383
|
+
}
|
384
|
+
if (!assignmentExpression ||
|
385
|
+
assignmentExpression.type !== utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
386
|
+
!isMemberAccessLike(assignmentExpression.left)) {
|
387
|
+
return;
|
388
|
+
}
|
389
|
+
const nullishCoalescingLeftNode = assignmentExpression.left;
|
390
|
+
const nullishCoalescingRightNode = assignmentExpression.right;
|
391
|
+
const { nodesInsideTestExpression, operator } = getOperatorAndNodesInsideTestExpression(node);
|
392
|
+
if (operator == null || !['!', '==', '==='].includes(operator)) {
|
393
|
+
return;
|
394
|
+
}
|
395
|
+
const nullishCoalescingParams = getNullishCoalescingParams(node, nullishCoalescingLeftNode, nodesInsideTestExpression, operator);
|
396
|
+
if (nullishCoalescingParams.isFixable) {
|
397
|
+
// Handle comments
|
398
|
+
const isConsequentNodeBlockStatement = node.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement;
|
399
|
+
const commentsBefore = formatComments(context.sourceCode.getCommentsBefore(assignmentExpression), isConsequentNodeBlockStatement ? '\n' : ' ');
|
400
|
+
const commentsAfter = isConsequentNodeBlockStatement
|
401
|
+
? formatComments(context.sourceCode.getCommentsAfter(assignmentExpression.parent), '\n')
|
402
|
+
: '';
|
403
|
+
context.report({
|
404
|
+
node,
|
405
|
+
messageId: 'preferNullishOverAssignment',
|
406
|
+
data: { equals: '=' },
|
407
|
+
suggest: [
|
408
|
+
{
|
409
|
+
messageId: 'suggestNullish',
|
410
|
+
data: { equals: '=' },
|
411
|
+
fix(fixer) {
|
412
|
+
const fixes = [];
|
413
|
+
if (commentsBefore) {
|
414
|
+
fixes.push(fixer.insertTextBefore(node, commentsBefore));
|
415
|
+
}
|
416
|
+
fixes.push(fixer.replaceText(node, `${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingLeftNode)} ??= ${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingRightNode)};`));
|
417
|
+
if (commentsAfter) {
|
418
|
+
fixes.push(fixer.insertTextAfter(node, ` ${commentsAfter.slice(0, -1)}`));
|
419
|
+
}
|
420
|
+
return fixes;
|
408
421
|
},
|
409
422
|
},
|
410
423
|
],
|
@@ -512,8 +525,21 @@ function isMixedLogicalExpression(node) {
|
|
512
525
|
function areNodesSimilarMemberAccess(a, b) {
|
513
526
|
if (a.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
514
527
|
b.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
515
|
-
|
516
|
-
|
528
|
+
if (!areNodesSimilarMemberAccess(a.object, b.object)) {
|
529
|
+
return false;
|
530
|
+
}
|
531
|
+
if (a.computed === b.computed) {
|
532
|
+
return (0, util_1.isNodeEqual)(a.property, b.property);
|
533
|
+
}
|
534
|
+
if (a.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
535
|
+
b.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
536
|
+
return a.property.value === b.property.name;
|
537
|
+
}
|
538
|
+
if (a.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
539
|
+
b.property.type === utils_1.AST_NODE_TYPES.Literal) {
|
540
|
+
return a.property.name === b.property.value;
|
541
|
+
}
|
542
|
+
return false;
|
517
543
|
}
|
518
544
|
if (a.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
519
545
|
b.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
@@ -527,8 +553,82 @@ function areNodesSimilarMemberAccess(a, b) {
|
|
527
553
|
* - the "nullish branch" is the branch when test node is nullish
|
528
554
|
*/
|
529
555
|
function getBranchNodes(node, operator) {
|
530
|
-
if (
|
556
|
+
if (['', '!=', '!=='].includes(operator)) {
|
531
557
|
return { nonNullishBranch: node.consequent, nullishBranch: node.alternate };
|
532
558
|
}
|
533
559
|
return { nonNullishBranch: node.alternate, nullishBranch: node.consequent };
|
534
560
|
}
|
561
|
+
function getOperatorAndNodesInsideTestExpression(node) {
|
562
|
+
let operator = null;
|
563
|
+
let nodesInsideTestExpression = [];
|
564
|
+
if (isMemberAccessLike(node.test) ||
|
565
|
+
node.test.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
566
|
+
operator = getNonBinaryNodeOperator(node.test);
|
567
|
+
}
|
568
|
+
else if (node.test.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
569
|
+
nodesInsideTestExpression = [node.test.left, node.test.right];
|
570
|
+
if (node.test.operator === '==' ||
|
571
|
+
node.test.operator === '!=' ||
|
572
|
+
node.test.operator === '===' ||
|
573
|
+
node.test.operator === '!==') {
|
574
|
+
operator = node.test.operator;
|
575
|
+
}
|
576
|
+
}
|
577
|
+
else if (node.test.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
578
|
+
node.test.left.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
579
|
+
node.test.right.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
580
|
+
if (isNodeNullishComparison(node.test.left) ||
|
581
|
+
isNodeNullishComparison(node.test.right)) {
|
582
|
+
return { nodesInsideTestExpression, operator };
|
583
|
+
}
|
584
|
+
nodesInsideTestExpression = [
|
585
|
+
node.test.left.left,
|
586
|
+
node.test.left.right,
|
587
|
+
node.test.right.left,
|
588
|
+
node.test.right.right,
|
589
|
+
];
|
590
|
+
if (['||', '||='].includes(node.test.operator)) {
|
591
|
+
if (node.test.left.operator === '===' &&
|
592
|
+
node.test.right.operator === '===') {
|
593
|
+
operator = '===';
|
594
|
+
}
|
595
|
+
else if (((node.test.left.operator === '===' ||
|
596
|
+
node.test.right.operator === '===') &&
|
597
|
+
(node.test.left.operator === '==' ||
|
598
|
+
node.test.right.operator === '==')) ||
|
599
|
+
(node.test.left.operator === '==' && node.test.right.operator === '==')) {
|
600
|
+
operator = '==';
|
601
|
+
}
|
602
|
+
}
|
603
|
+
else if (node.test.operator === '&&') {
|
604
|
+
if (node.test.left.operator === '!==' &&
|
605
|
+
node.test.right.operator === '!==') {
|
606
|
+
operator = '!==';
|
607
|
+
}
|
608
|
+
else if (((node.test.left.operator === '!==' ||
|
609
|
+
node.test.right.operator === '!==') &&
|
610
|
+
(node.test.left.operator === '!=' ||
|
611
|
+
node.test.right.operator === '!=')) ||
|
612
|
+
(node.test.left.operator === '!=' && node.test.right.operator === '!=')) {
|
613
|
+
operator = '!=';
|
614
|
+
}
|
615
|
+
}
|
616
|
+
}
|
617
|
+
return { nodesInsideTestExpression, operator };
|
618
|
+
}
|
619
|
+
function getNonBinaryNodeOperator(node) {
|
620
|
+
if (node.type !== utils_1.AST_NODE_TYPES.UnaryExpression) {
|
621
|
+
return '';
|
622
|
+
}
|
623
|
+
if (isMemberAccessLike(node.argument) && node.operator === '!') {
|
624
|
+
return '!';
|
625
|
+
}
|
626
|
+
return null;
|
627
|
+
}
|
628
|
+
function formatComments(comments, separator) {
|
629
|
+
return comments
|
630
|
+
.map(({ type, value }) => type === utils_1.AST_TOKEN_TYPES.Line
|
631
|
+
? `//${value}${separator}`
|
632
|
+
: `/*${value}*/${separator}`)
|
633
|
+
.join('');
|
634
|
+
}
|
@@ -17,6 +17,54 @@ This rule reports when you may consider replacing:
|
|
17
17
|
- An `||` operator with `??`
|
18
18
|
- An `||=` operator with `??=`
|
19
19
|
- Ternary expressions (`?:`) that are equivalent to `||` or `??` with `??`
|
20
|
+
- Assignment expressions (`=`) that can be safely replaced by `??=`
|
21
|
+
|
22
|
+
## Examples
|
23
|
+
|
24
|
+
<Tabs>
|
25
|
+
<TabItem value="❌ Incorrect">
|
26
|
+
|
27
|
+
```ts
|
28
|
+
declare const a: string | null;
|
29
|
+
declare const b: string | null;
|
30
|
+
|
31
|
+
const c = a || b;
|
32
|
+
|
33
|
+
declare let foo: { a: string } | null;
|
34
|
+
declare function makeFoo(): { a: string };
|
35
|
+
|
36
|
+
function lazyInitializeFooByTruthiness() {
|
37
|
+
if (!foo) {
|
38
|
+
foo = makeFoo();
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
function lazyInitializeFooByNullCheck() {
|
43
|
+
if (foo == null) {
|
44
|
+
foo = makeFoo();
|
45
|
+
}
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
</TabItem>
|
50
|
+
<TabItem value="✅ Correct">
|
51
|
+
|
52
|
+
```ts
|
53
|
+
declare const a: string | null;
|
54
|
+
declare const b: string | null;
|
55
|
+
|
56
|
+
const c = a ?? b;
|
57
|
+
|
58
|
+
declare let foo: { a: string } | null;
|
59
|
+
declare function makeFoo(): { a: string };
|
60
|
+
|
61
|
+
function lazyInitializeFoo() {
|
62
|
+
foo ??= makeFoo();
|
63
|
+
}
|
64
|
+
```
|
65
|
+
|
66
|
+
</TabItem>
|
67
|
+
</Tabs>
|
20
68
|
|
21
69
|
:::caution
|
22
70
|
This rule will not work as expected if [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) is not enabled.
|
@@ -255,3 +303,4 @@ If you are not using TypeScript 3.7 (or greater), then you will not be able to u
|
|
255
303
|
|
256
304
|
- [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html)
|
257
305
|
- [Nullish Coalescing Operator Proposal](https://github.com/tc39/proposal-nullish-coalescing/)
|
306
|
+
- [`logical-assignment-operators`](https://eslint.org/docs/latest/rules/logical-assignment-operators)
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@typescript-eslint/eslint-plugin",
|
3
|
-
"version": "8.
|
3
|
+
"version": "8.28.0",
|
4
4
|
"description": "TypeScript plugin for ESLint",
|
5
5
|
"files": [
|
6
6
|
"dist",
|
@@ -62,10 +62,10 @@
|
|
62
62
|
},
|
63
63
|
"dependencies": {
|
64
64
|
"@eslint-community/regexpp": "^4.10.0",
|
65
|
-
"@typescript-eslint/scope-manager": "8.
|
66
|
-
"@typescript-eslint/type-utils": "8.
|
67
|
-
"@typescript-eslint/utils": "8.
|
68
|
-
"@typescript-eslint/visitor-keys": "8.
|
65
|
+
"@typescript-eslint/scope-manager": "8.28.0",
|
66
|
+
"@typescript-eslint/type-utils": "8.28.0",
|
67
|
+
"@typescript-eslint/utils": "8.28.0",
|
68
|
+
"@typescript-eslint/visitor-keys": "8.28.0",
|
69
69
|
"graphemer": "^1.4.0",
|
70
70
|
"ignore": "^5.3.1",
|
71
71
|
"natural-compare": "^1.4.0",
|
@@ -76,8 +76,8 @@
|
|
76
76
|
"@types/marked": "^5.0.2",
|
77
77
|
"@types/mdast": "^4.0.3",
|
78
78
|
"@types/natural-compare": "*",
|
79
|
-
"@typescript-eslint/rule-schema-to-typescript-types": "8.
|
80
|
-
"@typescript-eslint/rule-tester": "8.
|
79
|
+
"@typescript-eslint/rule-schema-to-typescript-types": "8.28.0",
|
80
|
+
"@typescript-eslint/rule-tester": "8.28.0",
|
81
81
|
"ajv": "^6.12.6",
|
82
82
|
"cross-env": "^7.0.3",
|
83
83
|
"cross-fetch": "*",
|