@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.
@@ -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,wBAgSG"}
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
- if (sourceExports.typeOnlyNamedExport == null) {
162
- // The export is a type export
163
- sourceExports.typeOnlyNamedExport = node;
164
- }
161
+ // The export is a type export
162
+ sourceExports.typeOnlyNamedExport ??= node;
165
163
  }
166
- else if (sourceExports.valueOnlyNamedExport == null) {
164
+ else {
167
165
  // The export is a value export
168
- sourceExports.valueOnlyNamedExport = node;
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,wBA4CG"}
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,wBAwMG"}
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
- if (!functionType) {
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;AA8BnE,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,qBAAqB,GACrB,0BAA0B,GAC1B,gBAAgB,CAAC;;AAErB,wBAsfG"}
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 isIdentifierOrMemberOrChainExpression = (0, util_1.isNodeOfTypes)([
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
- let operator;
270
- let nodesInsideTestExpression = [];
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 isFixableWithPreferNullishOverTernary = (() => {
366
- // x ? x : y and !x ? y : x patterns
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
- return ((0, util_1.isNodeEqual)(a.property, b.property) &&
516
- areNodesSimilarMemberAccess(a.object, b.object));
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 (!operator || ['!=', '!=='].includes(operator)) {
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.27.1-alpha.2",
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.27.1-alpha.2",
66
- "@typescript-eslint/type-utils": "8.27.1-alpha.2",
67
- "@typescript-eslint/utils": "8.27.1-alpha.2",
68
- "@typescript-eslint/visitor-keys": "8.27.1-alpha.2",
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.27.1-alpha.2",
80
- "@typescript-eslint/rule-tester": "8.27.1-alpha.2",
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": "*",