From 8baa3fa3bcf41467cc0f494229627d01f9b8875f Mon Sep 17 00:00:00 2001 From: Marta Cardoso Date: Mon, 18 Mar 2024 21:09:59 +0000 Subject: [PATCH 01/10] fix [prefer-optional-chain] suggests optional chaining during strict null equality --- .../src/rules/prefer-optional-chain.ts | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 605045b99fd2..2a31c4764b31 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -13,6 +13,7 @@ import { analyzeChain } from './prefer-optional-chain-utils/analyzeChain'; import type { ValidOperand } from './prefer-optional-chain-utils/gatherLogicalOperands'; import { gatherLogicalOperands, + NullishComparisonType, OperandValidity, } from './prefer-optional-chain-utils/gatherLogicalOperands'; import type { @@ -141,6 +142,7 @@ export default createRule< return leftPrecedence < OperatorPrecedence.LeftHandSide; } + context.report({ node: parentNode, messageId: 'preferOptionalChain', @@ -168,7 +170,6 @@ export default createRule< ], }); }, - 'LogicalExpression[operator!="??"]'( node: TSESTree.LogicalExpression, ): void { @@ -183,35 +184,33 @@ export default createRule< options, ); - for (const logical of newlySeenLogicals) { - seenLogicals.add(logical); - } + const hasNullableAndTruthyOperand = operands.some( + operand => + operand.type === OperandValidity.Valid && + operand.comparisonType === + NullishComparisonType.NotEqualNullOrUndefined && + operand.isYoda, + ); - let currentChain: ValidOperand[] = []; - for (const operand of operands) { - if (operand.type === OperandValidity.Invalid) { - analyzeChain( - context, - parserServices, - options, - node.operator, - currentChain, - ); - currentChain = []; - } else { - currentChain.push(operand); - } + if (!hasNullableAndTruthyOperand) { + seenLogicals.add(node); + return; } - // make sure to check whatever's left - if (currentChain.length > 0) { - analyzeChain( - context, - parserServices, - options, - node.operator, - currentChain, - ); + // Analyze the chain for potential fixes + analyzeChain( + context, + parserServices, + options, + node.operator, + operands.filter( + (operand): operand is ValidOperand => + operand.type === OperandValidity.Valid, + ), + ); + + for (const logical of newlySeenLogicals) { + seenLogicals.add(logical); } }, }; From 68acba3c2098b8d85869071f9cc8544441981dba Mon Sep 17 00:00:00 2001 From: Marta Cardoso Date: Tue, 19 Mar 2024 01:19:43 +0000 Subject: [PATCH 02/10] fix [prefer-optional-chain] suggests optional chaining during strict null equality --- .../src/rules/prefer-optional-chain.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 2a31c4764b31..8ab62d3ed63a 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -13,7 +13,6 @@ import { analyzeChain } from './prefer-optional-chain-utils/analyzeChain'; import type { ValidOperand } from './prefer-optional-chain-utils/gatherLogicalOperands'; import { gatherLogicalOperands, - NullishComparisonType, OperandValidity, } from './prefer-optional-chain-utils/gatherLogicalOperands'; import type { @@ -170,9 +169,27 @@ export default createRule< ], }); }, + 'LogicalExpression[operator!="??"]'( node: TSESTree.LogicalExpression, ): void { + const leftNode = node.left; + const rightNode = node.right; + + const isUndefinedIdentifier = + leftNode.type === AST_NODE_TYPES.Identifier; + + const isNotNullCheck = + rightNode.type === AST_NODE_TYPES.BinaryExpression && + rightNode.operator === '!==' && + rightNode.right.type === AST_NODE_TYPES.Literal && + rightNode.right.value === null; + + if (isUndefinedIdentifier && isNotNullCheck) { + // Skip && with strict null equality checks on the right side: data && data.value !== null + return; + } + if (seenLogicals.has(node)) { return; } @@ -184,19 +201,6 @@ export default createRule< options, ); - const hasNullableAndTruthyOperand = operands.some( - operand => - operand.type === OperandValidity.Valid && - operand.comparisonType === - NullishComparisonType.NotEqualNullOrUndefined && - operand.isYoda, - ); - - if (!hasNullableAndTruthyOperand) { - seenLogicals.add(node); - return; - } - // Analyze the chain for potential fixes analyzeChain( context, From cb687479618d37325fd6d283d04a4c06cfdff027 Mon Sep 17 00:00:00 2001 From: jsfm01 Date: Fri, 22 Mar 2024 14:56:53 +0000 Subject: [PATCH 03/10] Fix lint --- .../src/rules/prefer-optional-chain.ts | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 8ab62d3ed63a..58cb117c8961 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -183,7 +183,7 @@ export default createRule< rightNode.type === AST_NODE_TYPES.BinaryExpression && rightNode.operator === '!==' && rightNode.right.type === AST_NODE_TYPES.Literal && - rightNode.right.value === null; + rightNode.right.value == null; if (isUndefinedIdentifier && isNotNullCheck) { // Skip && with strict null equality checks on the right side: data && data.value !== null @@ -201,21 +201,36 @@ export default createRule< options, ); - // Analyze the chain for potential fixes - analyzeChain( - context, - parserServices, - options, - node.operator, - operands.filter( - (operand): operand is ValidOperand => - operand.type === OperandValidity.Valid, - ), - ); - for (const logical of newlySeenLogicals) { seenLogicals.add(logical); } + + let currentChain: ValidOperand[] = []; + for (const operand of operands) { + if (operand.type === OperandValidity.Invalid) { + analyzeChain( + context, + parserServices, + options, + node.operator, + currentChain, + ); + currentChain = []; + } else { + currentChain.push(operand); + } + } + + // make sure to check whatever's left + if (currentChain.length > 0) { + analyzeChain( + context, + parserServices, + options, + node.operator, + currentChain, + ); + } }, }; }, From 29246c457265f1e55cf01176936a7dfe56235241 Mon Sep 17 00:00:00 2001 From: jsfm01 Date: Fri, 22 Mar 2024 15:15:02 +0000 Subject: [PATCH 04/10] Add prefer-optional-chain test --- .../rules/prefer-optional-chain/prefer-optional-chain.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 99b6362132cf..6b5775c99312 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -28,6 +28,7 @@ describe('|| {}', () => { 'foo ?? {};', '(foo ?? {})?.bar;', 'foo ||= bar ?? {};', + 'data && data.value !== null', // https://github.com/typescript-eslint/typescript-eslint/issues/8380 ` const a = null; From 5307759e5b03e67e9da9c119cecd470c65a3bed6 Mon Sep 17 00:00:00 2001 From: jsfm01 Date: Fri, 22 Mar 2024 15:40:48 +0000 Subject: [PATCH 05/10] Fix lint --- packages/eslint-plugin/src/rules/prefer-optional-chain.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 58cb117c8961..76a8d0c8ef0d 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -179,14 +179,13 @@ export default createRule< const isUndefinedIdentifier = leftNode.type === AST_NODE_TYPES.Identifier; - const isNotNullCheck = + const strictlyNullCheck = rightNode.type === AST_NODE_TYPES.BinaryExpression && rightNode.operator === '!==' && rightNode.right.type === AST_NODE_TYPES.Literal && rightNode.right.value == null; - if (isUndefinedIdentifier && isNotNullCheck) { - // Skip && with strict null equality checks on the right side: data && data.value !== null + if (isUndefinedIdentifier && strictlyNullCheck) { return; } From 6fb2f037d7be8c71d3e6bb35257d335ee4d56e6f Mon Sep 17 00:00:00 2001 From: Marta Cardoso Date: Sun, 31 Mar 2024 18:22:03 +0100 Subject: [PATCH 06/10] rebase and fix prefer optional chain on strict null equality --- .../rules/prefer-optional-chain-utils/analyzeChain.ts | 9 +++++++++ .../prefer-optional-chain.test.ts | 11 ++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 5ebb8e02ae51..42ef690789e9 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -61,6 +61,15 @@ const analyzeAndChainOperand: OperandAnalyzer = ( ) => { switch (operand.comparisonType) { case NullishComparisonType.Boolean: + const nextOperand = chain[index + 1] as ValidOperand | undefined; + if ( + nextOperand?.comparisonType === + NullishComparisonType.NotStrictEqualNull && + compareNodes(operand.comparedName, nextOperand.comparedName) === + NodeComparisonResult.Equal + ) { + return null; + } case NullishComparisonType.NotEqualNullOrUndefined: return [operand]; diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 6b5775c99312..10d1f17d987f 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -12,6 +12,16 @@ const ruleTester = new RuleTester({ }, }); +describe('test strict null equality', () => { + ruleTester.run('prefer-optional-chain', rule, { + valid: [ + // https://github.com/typescript-eslint/typescript-eslint/issues/7654 + 'data && data.value !== null;', + ], + invalid: [], + }); +}); + describe('|| {}', () => { ruleTester.run('prefer-optional-chain', rule, { valid: [ @@ -28,7 +38,6 @@ describe('|| {}', () => { 'foo ?? {};', '(foo ?? {})?.bar;', 'foo ||= bar ?? {};', - 'data && data.value !== null', // https://github.com/typescript-eslint/typescript-eslint/issues/8380 ` const a = null; From 3a8bdb7653f663828b48b916670790f9d010e936 Mon Sep 17 00:00:00 2001 From: Marta Cardoso Date: Sun, 31 Mar 2024 19:18:12 +0100 Subject: [PATCH 07/10] fix prefer optional chain on strict null equality --- .../prefer-optional-chain-utils/analyzeChain.ts | 4 ++-- .../src/rules/prefer-optional-chain.ts | 16 ---------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 42ef690789e9..8cfe9572bfb2 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -65,11 +65,11 @@ const analyzeAndChainOperand: OperandAnalyzer = ( if ( nextOperand?.comparisonType === NullishComparisonType.NotStrictEqualNull && - compareNodes(operand.comparedName, nextOperand.comparedName) === - NodeComparisonResult.Equal + operand.comparedName.type === AST_NODE_TYPES.Identifier ) { return null; } + case NullishComparisonType.NotEqualNullOrUndefined: return [operand]; diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 76a8d0c8ef0d..15fb2a99fba0 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -173,22 +173,6 @@ export default createRule< 'LogicalExpression[operator!="??"]'( node: TSESTree.LogicalExpression, ): void { - const leftNode = node.left; - const rightNode = node.right; - - const isUndefinedIdentifier = - leftNode.type === AST_NODE_TYPES.Identifier; - - const strictlyNullCheck = - rightNode.type === AST_NODE_TYPES.BinaryExpression && - rightNode.operator === '!==' && - rightNode.right.type === AST_NODE_TYPES.Literal && - rightNode.right.value == null; - - if (isUndefinedIdentifier && strictlyNullCheck) { - return; - } - if (seenLogicals.has(node)) { return; } From ef31674bc62cf4c2313c3cf6faf75bb79cf13be4 Mon Sep 17 00:00:00 2001 From: Marta Cardoso Date: Mon, 1 Apr 2024 11:07:23 +0100 Subject: [PATCH 08/10] fix lint --- .../src/rules/prefer-optional-chain-utils/analyzeChain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 8cfe9572bfb2..f8ac19e40756 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -60,7 +60,7 @@ const analyzeAndChainOperand: OperandAnalyzer = ( chain, ) => { switch (operand.comparisonType) { - case NullishComparisonType.Boolean: + case NullishComparisonType.Boolean: { const nextOperand = chain[index + 1] as ValidOperand | undefined; if ( nextOperand?.comparisonType === @@ -69,6 +69,8 @@ const analyzeAndChainOperand: OperandAnalyzer = ( ) { return null; } + return [operand]; + } case NullishComparisonType.NotEqualNullOrUndefined: return [operand]; From f49c9641d12f2f596456789fcd4d27bfc5fac1b3 Mon Sep 17 00:00:00 2001 From: Marta Cardoso Date: Wed, 10 Apr 2024 22:11:21 +0100 Subject: [PATCH 09/10] tests: code review suggestion --- .../prefer-optional-chain.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index d19c39d42b31..0609b7727f4e 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -12,16 +12,6 @@ const ruleTester = new RuleTester({ }, }); -describe('test strict null equality', () => { - ruleTester.run('prefer-optional-chain', rule, { - valid: [ - // https://github.com/typescript-eslint/typescript-eslint/issues/7654 - 'data && data.value !== null;', - ], - invalid: [], - }); -}); - describe('|| {}', () => { ruleTester.run('prefer-optional-chain', rule, { valid: [ @@ -803,6 +793,8 @@ describe('hand-crafted cases', () => { '(function () {}) && function () {}.name;', '(class Foo {}) && class Foo {}.constructor;', "new Map().get('a') && new Map().get('a').what;", + // https://github.com/typescript-eslint/typescript-eslint/issues/7654 + 'data && data.value !== null;', { code: '
&& (
).wtf;', parserOptions: { ecmaFeatures: { jsx: true } }, From 7094d288a8239f1352e28e6b62a7026ead010f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 23 Apr 2024 16:34:01 -0400 Subject: [PATCH 10/10] Update packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts --- .../src/rules/prefer-optional-chain-utils/analyzeChain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index f8ac19e40756..28d0b14a839a 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -61,7 +61,7 @@ const analyzeAndChainOperand: OperandAnalyzer = ( ) => { switch (operand.comparisonType) { case NullishComparisonType.Boolean: { - const nextOperand = chain[index + 1] as ValidOperand | undefined; + const nextOperand = chain.at(index + 1); if ( nextOperand?.comparisonType === NullishComparisonType.NotStrictEqualNull &&