@typescript-eslint/eslint-plugin 8.27.1-alpha.2 → 8.28.1-alpha.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-unnecessary-condition.d.ts","sourceRoot":"","sources":["../../src/rules/no-unnecessary-condition.ts"],"names":[],"mappings":"AAyHA,KAAK,iCAAiC,GAAG,OAAO,CAAC;AAEjD,KAAK,2BAA2B,GAAG,QAAQ,GAAG,OAAO,GAAG,uBAAuB,CAAC;AAShF,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,2BAA2B,CAAC,EACxB,2BAA2B,GAC3B,iCAAiC,CAAC;QACtC,sDAAsD,CAAC,EAAE,OAAO,CAAC;QACjE,mBAAmB,CAAC,EAAE,OAAO,CAAC;KAC/B;CACF,CAAC;AAEF,MAAM,MAAM,SAAS,GACjB,aAAa,GACb,iBAAiB,GACjB,eAAe,GACf,cAAc,GACd,kBAAkB,GAClB,+BAA+B,GAC/B,OAAO,GACP,cAAc,GACd,oBAAoB,GACpB,4BAA4B,GAC5B,mBAAmB,GACnB,wBAAwB,CAAC;;AAE7B,wBAgwBG"}
1
+ {"version":3,"file":"no-unnecessary-condition.d.ts","sourceRoot":"","sources":["../../src/rules/no-unnecessary-condition.ts"],"names":[],"mappings":"AAyHA,KAAK,iCAAiC,GAAG,OAAO,CAAC;AAEjD,KAAK,2BAA2B,GAAG,QAAQ,GAAG,OAAO,GAAG,uBAAuB,CAAC;AAShF,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,2BAA2B,CAAC,EACxB,2BAA2B,GAC3B,iCAAiC,CAAC;QACtC,sDAAsD,CAAC,EAAE,OAAO,CAAC;QACjE,mBAAmB,CAAC,EAAE,OAAO,CAAC;KAC/B;CACF,CAAC;AAEF,MAAM,MAAM,SAAS,GACjB,aAAa,GACb,iBAAiB,GACjB,eAAe,GACf,cAAc,GACd,kBAAkB,GAClB,+BAA+B,GAC/B,OAAO,GACP,cAAc,GACd,oBAAoB,GACpB,4BAA4B,GAC5B,mBAAmB,GACnB,wBAAwB,CAAC;;AAE7B,wBAowBG"}
@@ -258,7 +258,7 @@ exports.default = (0, util_1.createRule)({
258
258
  // Since typescript array index signature types don't represent the
259
259
  // possibility of out-of-bounds access, if we're indexing into an array
260
260
  // just skip the check, to avoid false positives
261
- if (isArrayIndexExpression(expression)) {
261
+ if (!isNoUncheckedIndexedAccess && isArrayIndexExpression(expression)) {
262
262
  return;
263
263
  }
264
264
  // When checking logical expressions, only check the right side
@@ -308,10 +308,11 @@ exports.default = (0, util_1.createRule)({
308
308
  // Since typescript array index signature types don't represent the
309
309
  // possibility of out-of-bounds access, if we're indexing into an array
310
310
  // just skip the check, to avoid false positives
311
- if (!isArrayIndexExpression(node) &&
312
- !(node.type === utils_1.AST_NODE_TYPES.ChainExpression &&
313
- node.expression.type !== utils_1.AST_NODE_TYPES.TSNonNullExpression &&
314
- optionChainContainsOptionArrayIndex(node.expression))) {
311
+ if (isNoUncheckedIndexedAccess ||
312
+ (!isArrayIndexExpression(node) &&
313
+ !(node.type === utils_1.AST_NODE_TYPES.ChainExpression &&
314
+ node.expression.type !== utils_1.AST_NODE_TYPES.TSNonNullExpression &&
315
+ optionChainContainsOptionArrayIndex(node.expression)))) {
315
316
  messageId = 'neverNullish';
316
317
  }
317
318
  }
@@ -598,7 +599,8 @@ exports.default = (0, util_1.createRule)({
598
599
  // Since typescript array index signature types don't represent the
599
600
  // possibility of out-of-bounds access, if we're indexing into an array
600
601
  // just skip the check, to avoid false positives
601
- if (optionChainContainsOptionArrayIndex(node)) {
602
+ if (!isNoUncheckedIndexedAccess &&
603
+ optionChainContainsOptionArrayIndex(node)) {
602
604
  return;
603
605
  }
604
606
  const nodeToCheck = node.type === utils_1.AST_NODE_TYPES.CallExpression ? node.callee : node.object;
@@ -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)
@@ -4,6 +4,7 @@ export type Options = [
4
4
  allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
5
5
  ignoreBooleanCoercion?: boolean;
6
6
  ignoreConditionalTests?: boolean;
7
+ ignoreIfStatements?: boolean;
7
8
  ignoreMixedLogicalExpressions?: boolean;
8
9
  ignorePrimitives?: {
9
10
  bigint?: boolean;
@@ -14,7 +15,7 @@ export type Options = [
14
15
  ignoreTernaryTests?: boolean;
15
16
  }
16
17
  ];
17
- export type MessageIds = 'noStrictNullCheck' | 'preferNullishOverOr' | 'preferNullishOverTernary' | 'suggestNullish';
18
+ export type MessageIds = 'noStrictNullCheck' | 'preferNullishOverAssignment' | 'preferNullishOverOr' | 'preferNullishOverTernary' | 'suggestNullish';
18
19
  declare const _default: TSESLint.RuleModule<MessageIds, Options, import("../../rules").ESLintPluginDocs, TSESLint.RuleListener>;
19
20
  export default _default;
20
21
  //# 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,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAC7B,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,wBA8jBG"}
@@ -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 }}`).',
@@ -75,6 +79,10 @@ exports.default = (0, util_1.createRule)({
75
79
  type: 'boolean',
76
80
  description: 'Whether to ignore cases that are located within a conditional test.',
77
81
  },
82
+ ignoreIfStatements: {
83
+ type: 'boolean',
84
+ description: 'Whether to ignore any if statements that could be simplified by using the nullish coalescing operator.',
85
+ },
78
86
  ignoreMixedLogicalExpressions: {
79
87
  type: 'boolean',
80
88
  description: 'Whether to ignore any logical or expressions that are part of a mixed logical expression (with `&&`).',
@@ -124,6 +132,7 @@ exports.default = (0, util_1.createRule)({
124
132
  allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
125
133
  ignoreBooleanCoercion: false,
126
134
  ignoreConditionalTests: true,
135
+ ignoreIfStatements: false,
127
136
  ignoreMixedLogicalExpressions: false,
128
137
  ignorePrimitives: {
129
138
  bigint: false,
@@ -134,7 +143,7 @@ exports.default = (0, util_1.createRule)({
134
143
  ignoreTernaryTests: false,
135
144
  },
136
145
  ],
137
- create(context, [{ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, ignoreBooleanCoercion, ignoreConditionalTests, ignoreMixedLogicalExpressions, ignorePrimitives, ignoreTernaryTests, },]) {
146
+ create(context, [{ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, ignoreBooleanCoercion, ignoreConditionalTests, ignoreIfStatements, ignoreMixedLogicalExpressions, ignorePrimitives, ignoreTernaryTests, },]) {
138
147
  const parserServices = (0, util_1.getParserServices)(context);
139
148
  const compilerOptions = parserServices.program.getCompilerOptions();
140
149
  const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks');
@@ -258,6 +267,81 @@ exports.default = (0, util_1.createRule)({
258
267
  ],
259
268
  });
260
269
  }
270
+ function getNullishCoalescingParams(node, nonNullishNode, nodesInsideTestExpression, operator) {
271
+ let nullishCoalescingLeftNode;
272
+ let hasTruthinessCheck = false;
273
+ let hasNullCheckWithoutTruthinessCheck = false;
274
+ let hasUndefinedCheckWithoutTruthinessCheck = false;
275
+ if (!nodesInsideTestExpression.length) {
276
+ hasTruthinessCheck = true;
277
+ nullishCoalescingLeftNode =
278
+ node.test.type === utils_1.AST_NODE_TYPES.UnaryExpression
279
+ ? node.test.argument
280
+ : node.test;
281
+ if (!areNodesSimilarMemberAccess(nullishCoalescingLeftNode, nonNullishNode)) {
282
+ return { isFixable: false };
283
+ }
284
+ }
285
+ else {
286
+ // we check that the test only contains null, undefined and the identifier
287
+ for (const testNode of nodesInsideTestExpression) {
288
+ if ((0, util_1.isNullLiteral)(testNode)) {
289
+ hasNullCheckWithoutTruthinessCheck = true;
290
+ }
291
+ else if ((0, util_1.isUndefinedIdentifier)(testNode)) {
292
+ hasUndefinedCheckWithoutTruthinessCheck = true;
293
+ }
294
+ else if (areNodesSimilarMemberAccess(testNode, nonNullishNode)) {
295
+ // Only consider the first expression in a multi-part nullish check,
296
+ // as subsequent expressions might not require all the optional chaining operators.
297
+ // For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo';
298
+ // This works because `node.test` is always evaluated first in the loop
299
+ // and has the same or more necessary optional chaining operators
300
+ // than `node.alternate` or `node.consequent`.
301
+ nullishCoalescingLeftNode ??= testNode;
302
+ }
303
+ else {
304
+ return { isFixable: false };
305
+ }
306
+ }
307
+ }
308
+ if (!nullishCoalescingLeftNode) {
309
+ return { isFixable: false };
310
+ }
311
+ const isFixable = (() => {
312
+ if (hasTruthinessCheck) {
313
+ return isTruthinessCheckEligibleForPreferNullish({
314
+ node,
315
+ testNode: nullishCoalescingLeftNode,
316
+ });
317
+ }
318
+ // it is fixable if we check for both null and undefined, or not if neither
319
+ if (hasUndefinedCheckWithoutTruthinessCheck ===
320
+ hasNullCheckWithoutTruthinessCheck) {
321
+ return hasUndefinedCheckWithoutTruthinessCheck;
322
+ }
323
+ // it is fixable if we loosely check for either null or undefined
324
+ if (['==', '!='].includes(operator)) {
325
+ return true;
326
+ }
327
+ const type = parserServices.getTypeAtLocation(nullishCoalescingLeftNode);
328
+ const flags = (0, util_1.getTypeFlags)(type);
329
+ if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
330
+ return false;
331
+ }
332
+ const hasNullType = (flags & ts.TypeFlags.Null) !== 0;
333
+ // it is fixable if we check for undefined and the type is not nullable
334
+ if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) {
335
+ return true;
336
+ }
337
+ const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0;
338
+ // it is fixable if we check for null and the type can't be undefined
339
+ return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType;
340
+ })();
341
+ return isFixable
342
+ ? { isFixable: true, nullishCoalescingLeftNode }
343
+ : { isFixable: false };
344
+ }
261
345
  return {
262
346
  'AssignmentExpression[operator = "||="]'(node) {
263
347
  checkAndFixWithPreferNullishOverOr(node, 'assignment', '=');
@@ -266,134 +350,12 @@ exports.default = (0, util_1.createRule)({
266
350
  if (ignoreTernaryTests) {
267
351
  return;
268
352
  }
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) {
353
+ const { nodesInsideTestExpression, operator } = getOperatorAndNodesInsideTestExpression(node);
354
+ if (operator == null) {
363
355
  return;
364
356
  }
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) {
357
+ const nullishCoalescingParams = getNullishCoalescingParams(node, getBranchNodes(node, operator).nonNullishBranch, nodesInsideTestExpression, operator);
358
+ if (nullishCoalescingParams.isFixable) {
397
359
  context.report({
398
360
  node,
399
361
  messageId: 'preferNullishOverTernary',
@@ -404,7 +366,63 @@ exports.default = (0, util_1.createRule)({
404
366
  messageId: 'suggestNullish',
405
367
  data: { equals: '' },
406
368
  fix(fixer) {
407
- return fixer.replaceText(node, `${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingLeftNode)} ?? ${(0, util_1.getTextWithParentheses)(context.sourceCode, getBranchNodes(node, operator).nullishBranch)}`);
369
+ return fixer.replaceText(node, `${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingParams.nullishCoalescingLeftNode)} ?? ${(0, util_1.getTextWithParentheses)(context.sourceCode, getBranchNodes(node, operator).nullishBranch)}`);
370
+ },
371
+ },
372
+ ],
373
+ });
374
+ }
375
+ },
376
+ IfStatement(node) {
377
+ if (ignoreIfStatements || node.alternate != null) {
378
+ return;
379
+ }
380
+ let assignmentExpression;
381
+ if (node.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement &&
382
+ node.consequent.body.length === 1 &&
383
+ node.consequent.body[0].type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
384
+ assignmentExpression = node.consequent.body[0].expression;
385
+ }
386
+ else if (node.consequent.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
387
+ assignmentExpression = node.consequent.expression;
388
+ }
389
+ if (!assignmentExpression ||
390
+ assignmentExpression.type !== utils_1.AST_NODE_TYPES.AssignmentExpression ||
391
+ !isMemberAccessLike(assignmentExpression.left)) {
392
+ return;
393
+ }
394
+ const nullishCoalescingLeftNode = assignmentExpression.left;
395
+ const nullishCoalescingRightNode = assignmentExpression.right;
396
+ const { nodesInsideTestExpression, operator } = getOperatorAndNodesInsideTestExpression(node);
397
+ if (operator == null || !['!', '==', '==='].includes(operator)) {
398
+ return;
399
+ }
400
+ const nullishCoalescingParams = getNullishCoalescingParams(node, nullishCoalescingLeftNode, nodesInsideTestExpression, operator);
401
+ if (nullishCoalescingParams.isFixable) {
402
+ // Handle comments
403
+ const isConsequentNodeBlockStatement = node.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement;
404
+ const commentsBefore = formatComments(context.sourceCode.getCommentsBefore(assignmentExpression), isConsequentNodeBlockStatement ? '\n' : ' ');
405
+ const commentsAfter = isConsequentNodeBlockStatement
406
+ ? formatComments(context.sourceCode.getCommentsAfter(assignmentExpression.parent), '\n')
407
+ : '';
408
+ context.report({
409
+ node,
410
+ messageId: 'preferNullishOverAssignment',
411
+ data: { equals: '=' },
412
+ suggest: [
413
+ {
414
+ messageId: 'suggestNullish',
415
+ data: { equals: '=' },
416
+ fix(fixer) {
417
+ const fixes = [];
418
+ if (commentsBefore) {
419
+ fixes.push(fixer.insertTextBefore(node, commentsBefore));
420
+ }
421
+ fixes.push(fixer.replaceText(node, `${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingLeftNode)} ??= ${(0, util_1.getTextWithParentheses)(context.sourceCode, nullishCoalescingRightNode)};`));
422
+ if (commentsAfter) {
423
+ fixes.push(fixer.insertTextAfter(node, ` ${commentsAfter.slice(0, -1)}`));
424
+ }
425
+ return fixes;
408
426
  },
409
427
  },
410
428
  ],
@@ -512,8 +530,21 @@ function isMixedLogicalExpression(node) {
512
530
  function areNodesSimilarMemberAccess(a, b) {
513
531
  if (a.type === utils_1.AST_NODE_TYPES.MemberExpression &&
514
532
  b.type === utils_1.AST_NODE_TYPES.MemberExpression) {
515
- return ((0, util_1.isNodeEqual)(a.property, b.property) &&
516
- areNodesSimilarMemberAccess(a.object, b.object));
533
+ if (!areNodesSimilarMemberAccess(a.object, b.object)) {
534
+ return false;
535
+ }
536
+ if (a.computed === b.computed) {
537
+ return (0, util_1.isNodeEqual)(a.property, b.property);
538
+ }
539
+ if (a.property.type === utils_1.AST_NODE_TYPES.Literal &&
540
+ b.property.type === utils_1.AST_NODE_TYPES.Identifier) {
541
+ return a.property.value === b.property.name;
542
+ }
543
+ if (a.property.type === utils_1.AST_NODE_TYPES.Identifier &&
544
+ b.property.type === utils_1.AST_NODE_TYPES.Literal) {
545
+ return a.property.name === b.property.value;
546
+ }
547
+ return false;
517
548
  }
518
549
  if (a.type === utils_1.AST_NODE_TYPES.ChainExpression ||
519
550
  b.type === utils_1.AST_NODE_TYPES.ChainExpression) {
@@ -527,8 +558,82 @@ function areNodesSimilarMemberAccess(a, b) {
527
558
  * - the "nullish branch" is the branch when test node is nullish
528
559
  */
529
560
  function getBranchNodes(node, operator) {
530
- if (!operator || ['!=', '!=='].includes(operator)) {
561
+ if (['', '!=', '!=='].includes(operator)) {
531
562
  return { nonNullishBranch: node.consequent, nullishBranch: node.alternate };
532
563
  }
533
564
  return { nonNullishBranch: node.alternate, nullishBranch: node.consequent };
534
565
  }
566
+ function getOperatorAndNodesInsideTestExpression(node) {
567
+ let operator = null;
568
+ let nodesInsideTestExpression = [];
569
+ if (isMemberAccessLike(node.test) ||
570
+ node.test.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
571
+ operator = getNonBinaryNodeOperator(node.test);
572
+ }
573
+ else if (node.test.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
574
+ nodesInsideTestExpression = [node.test.left, node.test.right];
575
+ if (node.test.operator === '==' ||
576
+ node.test.operator === '!=' ||
577
+ node.test.operator === '===' ||
578
+ node.test.operator === '!==') {
579
+ operator = node.test.operator;
580
+ }
581
+ }
582
+ else if (node.test.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
583
+ node.test.left.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
584
+ node.test.right.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
585
+ if (isNodeNullishComparison(node.test.left) ||
586
+ isNodeNullishComparison(node.test.right)) {
587
+ return { nodesInsideTestExpression, operator };
588
+ }
589
+ nodesInsideTestExpression = [
590
+ node.test.left.left,
591
+ node.test.left.right,
592
+ node.test.right.left,
593
+ node.test.right.right,
594
+ ];
595
+ if (['||', '||='].includes(node.test.operator)) {
596
+ if (node.test.left.operator === '===' &&
597
+ node.test.right.operator === '===') {
598
+ operator = '===';
599
+ }
600
+ else if (((node.test.left.operator === '===' ||
601
+ node.test.right.operator === '===') &&
602
+ (node.test.left.operator === '==' ||
603
+ node.test.right.operator === '==')) ||
604
+ (node.test.left.operator === '==' && node.test.right.operator === '==')) {
605
+ operator = '==';
606
+ }
607
+ }
608
+ else if (node.test.operator === '&&') {
609
+ if (node.test.left.operator === '!==' &&
610
+ node.test.right.operator === '!==') {
611
+ operator = '!==';
612
+ }
613
+ else if (((node.test.left.operator === '!==' ||
614
+ node.test.right.operator === '!==') &&
615
+ (node.test.left.operator === '!=' ||
616
+ node.test.right.operator === '!=')) ||
617
+ (node.test.left.operator === '!=' && node.test.right.operator === '!=')) {
618
+ operator = '!=';
619
+ }
620
+ }
621
+ }
622
+ return { nodesInsideTestExpression, operator };
623
+ }
624
+ function getNonBinaryNodeOperator(node) {
625
+ if (node.type !== utils_1.AST_NODE_TYPES.UnaryExpression) {
626
+ return '';
627
+ }
628
+ if (isMemberAccessLike(node.argument) && node.operator === '!') {
629
+ return '!';
630
+ }
631
+ return null;
632
+ }
633
+ function formatComments(comments, separator) {
634
+ return comments
635
+ .map(({ type, value }) => type === utils_1.AST_TOKEN_TYPES.Line
636
+ ? `//${value}${separator}`
637
+ : `/*${value}*/${separator}`)
638
+ .join('');
639
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"use-unknown-in-catch-callback-variable.d.ts","sourceRoot":"","sources":["../../src/rules/use-unknown-in-catch-callback-variable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAC;AAgBnE,MAAM,MAAM,UAAU,GAClB,wCAAwC,GACxC,oCAAoC,GACpC,YAAY,GACZ,qCAAqC,GACrC,sCAAsC,GACtC,mCAAmC,GACnC,+BAA+B,CAAC;;AAKpC,wBAmSG"}
1
+ {"version":3,"file":"use-unknown-in-catch-callback-variable.d.ts","sourceRoot":"","sources":["../../src/rules/use-unknown-in-catch-callback-variable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAC;AAgBnE,MAAM,MAAM,UAAU,GAClB,wCAAwC,GACxC,oCAAoC,GACpC,YAAY,GACZ,qCAAqC,GACrC,sCAAsC,GACtC,mCAAmC,GACnC,+BAA+B,CAAC;;AAKpC,wBAkSG"}
@@ -46,7 +46,6 @@ exports.default = (0, util_1.createRule)({
46
46
  recommended: 'strict',
47
47
  requiresTypeChecking: true,
48
48
  },
49
- fixable: 'code',
50
49
  hasSuggestions: true,
51
50
  messages: {
52
51
  addUnknownRestTypeAnnotationSuggestion: 'Add an explicit `: [unknown]` type annotation to the rejection callback rest variable.',
@@ -1 +1 @@
1
- {"version":3,"file":"isAssignee.d.ts","sourceRoot":"","sources":["../../src/util/isAssignee.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAIzD,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAG,OAAO,CAoDvD"}
1
+ {"version":3,"file":"isAssignee.d.ts","sourceRoot":"","sources":["../../src/util/isAssignee.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAIzD,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAG,OAAO,CA+DvD"}
@@ -38,5 +38,13 @@ function isAssignee(node) {
38
38
  isAssignee(parent.parent)) {
39
39
  return true;
40
40
  }
41
+ // (a[i] as number)++, [...a[i]!] = [0], etc.
42
+ if ((parent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
43
+ parent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
44
+ parent.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
45
+ parent.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) &&
46
+ isAssignee(parent)) {
47
+ return true;
48
+ }
41
49
  return false;
42
50
  }
@@ -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.
@@ -70,6 +118,49 @@ c ?? 'a string';
70
118
  </TabItem>
71
119
  </Tabs>
72
120
 
121
+ ### `ignoreIfStatements`
122
+
123
+ {/* insert option description */}
124
+
125
+ Examples of code for this rule with `{ ignoreIfStatements: false }`:
126
+
127
+ <Tabs>
128
+ <TabItem value="❌ Incorrect">
129
+
130
+ ```ts option='{ "ignoreIfStatements": false }'
131
+ declare let foo: { a: string } | null;
132
+ declare function makeFoo(): { a: string };
133
+
134
+ function lazyInitializeFoo1() {
135
+ if (!foo) {
136
+ foo = makeFoo();
137
+ }
138
+ }
139
+
140
+ function lazyInitializeFoo2() {
141
+ if (!foo) foo = makeFoo();
142
+ }
143
+ ```
144
+
145
+ </TabItem>
146
+ <TabItem value="✅ Correct">
147
+
148
+ ```ts option='{ "ignoreIfStatements": false }'
149
+ declare let foo: { a: string } | null;
150
+ declare function makeFoo(): { a: string };
151
+
152
+ function lazyInitializeFoo1() {
153
+ foo ??= makeFoo();
154
+ }
155
+
156
+ function lazyInitializeFoo2() {
157
+ foo ??= makeFoo();
158
+ }
159
+ ```
160
+
161
+ </TabItem>
162
+ </Tabs>
163
+
73
164
  ### `ignoreConditionalTests`
74
165
 
75
166
  {/* insert option description */}
@@ -255,3 +346,4 @@ If you are not using TypeScript 3.7 (or greater), then you will not be able to u
255
346
 
256
347
  - [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html)
257
348
  - [Nullish Coalescing Operator Proposal](https://github.com/tc39/proposal-nullish-coalescing/)
349
+ - [`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.1-alpha.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.1-alpha.0",
66
+ "@typescript-eslint/type-utils": "8.28.1-alpha.0",
67
+ "@typescript-eslint/utils": "8.28.1-alpha.0",
68
+ "@typescript-eslint/visitor-keys": "8.28.1-alpha.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.1-alpha.0",
80
+ "@typescript-eslint/rule-tester": "8.28.1-alpha.0",
81
81
  "ajv": "^6.12.6",
82
82
  "cross-env": "^7.0.3",
83
83
  "cross-fetch": "*",