From 200e6e2e94b2715acc261c1f3c11e7b015f34df3 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:27:57 +0100 Subject: [PATCH 01/23] handle ChainExpression in preferNullishOverTernary --- .../src/rules/prefer-nullish-coalescing.ts | 109 +++++-- .../eslint-plugin/src/util/isNodeEqual.ts | 6 + .../rules/prefer-nullish-coalescing.test.ts | 304 ++++++++++++++++++ .../tests/util/isNodeEqual.test.ts | 5 + 4 files changed, 395 insertions(+), 29 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index e52f83543af7..078b5c9bfce9 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -19,11 +19,16 @@ import { NullThrowsReasons, } from '../util'; -const isIdentifierOrMemberExpression = isNodeOfTypes([ +const isIdentifierOrMemberOrChainExpression = isNodeOfTypes([ + AST_NODE_TYPES.ChainExpression, AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression, ] as const); +type NullishCheckOperator = '!' | '!=' | '!==' | '==' | '===' | undefined; + +const nullishInequalityOperators = new Set(['!=', '!==']); + export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -340,7 +345,7 @@ export default createRule({ return; } - let operator: '!' | '!=' | '!==' | '==' | '===' | undefined; + let operator: NullishCheckOperator; let nodesInsideTestExpression: TSESTree.Node[] = []; if (node.test.type === AST_NODE_TYPES.BinaryExpression) { nodesInsideTestExpression = [node.test.left, node.test.right]; @@ -398,28 +403,32 @@ export default createRule({ } } - let identifierOrMemberExpression: TSESTree.Node | undefined; + let nullishCoalescingLeftNode: TSESTree.Node | undefined; let hasTruthinessCheck = false; let hasNullCheckWithoutTruthinessCheck = false; let hasUndefinedCheckWithoutTruthinessCheck = false; if (!operator) { + let testNode: TSESTree.Node | undefined; hasTruthinessCheck = true; - if ( - isIdentifierOrMemberExpression(node.test) && - isNodeEqual(node.test, node.consequent) - ) { - identifierOrMemberExpression = node.test; + if (isIdentifierOrMemberOrChainExpression(node.test)) { + testNode = node.test; } else if ( node.test.type === AST_NODE_TYPES.UnaryExpression && - node.test.operator === '!' && - isIdentifierOrMemberExpression(node.test.argument) && - isNodeEqual(node.test.argument, node.alternate) + isIdentifierOrMemberOrChainExpression(node.test.argument) && + node.test.operator === '!' ) { - identifierOrMemberExpression = node.test.argument; + testNode = node.test.argument; operator = '!'; } + + if ( + testNode && + isTestNodeEquivalentToNonNullishBranchNode(testNode, node, operator) + ) { + nullishCoalescingLeftNode = testNode; + } } else { // we check that the test only contains null, undefined and the identifier for (const testNode of nodesInsideTestExpression) { @@ -428,22 +437,20 @@ export default createRule({ } else if (isUndefinedIdentifier(testNode)) { hasUndefinedCheckWithoutTruthinessCheck = true; } else if ( - (operator === '!==' || operator === '!=') && - isNodeEqual(testNode, node.consequent) - ) { - identifierOrMemberExpression = testNode; - } else if ( - (operator === '===' || operator === '==') && - isNodeEqual(testNode, node.alternate) + isTestNodeEquivalentToNonNullishBranchNode( + testNode, + node, + operator, + ) ) { - identifierOrMemberExpression = testNode; + nullishCoalescingLeftNode = testNode; } else { return; } } } - if (!identifierOrMemberExpression) { + if (!nullishCoalescingLeftNode) { return; } @@ -452,12 +459,12 @@ export default createRule({ if (hasTruthinessCheck) { return isTruthinessCheckEligibleForPreferNullish({ node, - testNode: identifierOrMemberExpression, + testNode: nullishCoalescingLeftNode, }); } const tsNode = parserServices.esTreeNodeToTSNodeMap.get( - identifierOrMemberExpression, + nullishCoalescingLeftNode, ); const type = checker.getTypeAtLocation(tsNode); const flags = getTypeFlags(type); @@ -503,15 +510,11 @@ export default createRule({ messageId: 'suggestNullish', data: { equals: '' }, fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { - const [left, right] = - operator === '===' || operator === '==' || operator === '!' - ? [identifierOrMemberExpression, node.consequent] - : [identifierOrMemberExpression, node.alternate]; return fixer.replaceText( node, - `${getTextWithParentheses(context.sourceCode, left)} ?? ${getTextWithParentheses( + `${getTextWithParentheses(context.sourceCode, nullishCoalescingLeftNode)} ?? ${getTextWithParentheses( context.sourceCode, - right, + getNullishBranchNode(node, operator), )}`, ); }, @@ -647,3 +650,51 @@ function isMixedLogicalExpression( return false; } + +function isTestNodeEquivalentToNonNullishBranchNode( + testNode: TSESTree.Node, + node: TSESTree.ConditionalExpression, + operator: NullishCheckOperator, +): boolean { + const consequentNode = getNonNullishBranchNode(node, operator); + if ( + testNode.type === AST_NODE_TYPES.ChainExpression && + consequentNode.type === AST_NODE_TYPES.MemberExpression + ) { + return isTestNodeEquivalentToNonNullishBranchNode( + testNode.expression, + node, + operator, + ); + } + return isNodeEqual(testNode, consequentNode); +} + +/** + * Returns the branch nodes of a conditional expression: + * - the `nonNullish` branch is the branch when test node is not nullish + * - the `nullish` branch is the branch when test node is nullish + */ +function getBranchNodes( + node: TSESTree.ConditionalExpression, + operator: NullishCheckOperator, +): { nonNullish: TSESTree.Expression; nullish: TSESTree.Expression } { + if (!operator || nullishInequalityOperators.has(operator)) { + return { nonNullish: node.consequent, nullish: node.alternate }; + } + return { nonNullish: node.alternate, nullish: node.consequent }; +} + +function getNonNullishBranchNode( + node: TSESTree.ConditionalExpression, + operator: NullishCheckOperator, +): TSESTree.Expression { + return getBranchNodes(node, operator).nonNullish; +} + +function getNullishBranchNode( + node: TSESTree.ConditionalExpression, + operator: NullishCheckOperator, +): TSESTree.Expression { + return getBranchNodes(node, operator).nullish; +} diff --git a/packages/eslint-plugin/src/util/isNodeEqual.ts b/packages/eslint-plugin/src/util/isNodeEqual.ts index 729b38b58888..d37952ad6bf0 100644 --- a/packages/eslint-plugin/src/util/isNodeEqual.ts +++ b/packages/eslint-plugin/src/util/isNodeEqual.ts @@ -29,5 +29,11 @@ export function isNodeEqual(a: TSESTree.Node, b: TSESTree.Node): boolean { isNodeEqual(a.property, b.property) && isNodeEqual(a.object, b.object) ); } + if ( + a.type === AST_NODE_TYPES.ChainExpression && + b.type === AST_NODE_TYPES.ChainExpression + ) { + return isNodeEqual(a.expression, b.expression); + } return false; } diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 99cca5c7369c..21b6f9562a21 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -581,6 +581,46 @@ declare let x: (${type} & { __brand?: any }) | undefined; declare let y: number; !x ? y : x; `, + ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b !== null ? defaultBoxOptional.a?.b : getFallbackBox(); + `, + ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | null } }; + +defaultBoxOptional.a?.b !== null ? defaultBoxOptional.a?.b : getFallbackBox(); + `, + ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | null } }; + +defaultBoxOptional.a?.b !== undefined + ? defaultBoxOptional.a?.b + : getFallbackBox(); + `, + ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | null } }; + +defaultBoxOptional.a?.b !== undefined + ? defaultBoxOptional.a.b + : getFallbackBox(); + `, { code: ` declare let x: 0 | 1 | 0n | 1n | undefined; @@ -4698,5 +4738,269 @@ defaultBox ?? getFallbackBox(); options: [{ ignoreTernaryTests: false }], output: null, }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b != null ? defaultBoxOptional.a?.b : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b != null ? defaultBoxOptional.a.b : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ? defaultBoxOptional.a?.b : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ? defaultBoxOptional.a.b : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b !== undefined + ? defaultBoxOptional.a?.b + : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b !== undefined + ? defaultBoxOptional.a.b + : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b !== undefined && defaultBoxOptional.a?.b !== null + ? defaultBoxOptional.a?.b + : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b !== undefined && defaultBoxOptional.a?.b !== null + ? defaultBoxOptional.a.b + : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBoxOptional: { a?: { b?: Box | undefined } }; + +defaultBoxOptional.a?.b ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, ], }); diff --git a/packages/eslint-plugin/tests/util/isNodeEqual.test.ts b/packages/eslint-plugin/tests/util/isNodeEqual.test.ts index 1177b1ac44bb..f1eb23fea674 100644 --- a/packages/eslint-plugin/tests/util/isNodeEqual.test.ts +++ b/packages/eslint-plugin/tests/util/isNodeEqual.test.ts @@ -95,6 +95,11 @@ ruleTester.run('isNodeEqual', rule, { errors: [{ messageId: 'removeExpression' }], output: 'x.z[1][this[this.o]]["3"][a.b.c]', }, + { + code: 'a?.b || a?.b', + errors: [{ messageId: 'removeExpression' }], + output: 'a?.b', + }, ], valid: [ { code: 'a || b' }, From d0a06a75da1512fa34b4a8415a1cb14fed992a6d Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:51:41 +0100 Subject: [PATCH 02/23] simplify --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 078b5c9bfce9..686db9a2cf8d 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -27,8 +27,6 @@ const isIdentifierOrMemberOrChainExpression = isNodeOfTypes([ type NullishCheckOperator = '!' | '!=' | '!==' | '==' | '===' | undefined; -const nullishInequalityOperators = new Set(['!=', '!==']); - export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -679,7 +677,7 @@ function getBranchNodes( node: TSESTree.ConditionalExpression, operator: NullishCheckOperator, ): { nonNullish: TSESTree.Expression; nullish: TSESTree.Expression } { - if (!operator || nullishInequalityOperators.has(operator)) { + if (!operator || ['!=', '!=='].includes(operator)) { return { nonNullish: node.consequent, nullish: node.alternate }; } return { nonNullish: node.alternate, nullish: node.consequent }; From 58eabb3b93e5f94701932f0563971a52332fb02d Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:00:38 +0100 Subject: [PATCH 03/23] naming --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 686db9a2cf8d..41a74fe30203 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -654,10 +654,10 @@ function isTestNodeEquivalentToNonNullishBranchNode( node: TSESTree.ConditionalExpression, operator: NullishCheckOperator, ): boolean { - const consequentNode = getNonNullishBranchNode(node, operator); + const nonNullishBranchNode = getNonNullishBranchNode(node, operator); if ( testNode.type === AST_NODE_TYPES.ChainExpression && - consequentNode.type === AST_NODE_TYPES.MemberExpression + nonNullishBranchNode.type === AST_NODE_TYPES.MemberExpression ) { return isTestNodeEquivalentToNonNullishBranchNode( testNode.expression, @@ -665,7 +665,7 @@ function isTestNodeEquivalentToNonNullishBranchNode( operator, ); } - return isNodeEqual(testNode, consequentNode); + return isNodeEqual(testNode, nonNullishBranchNode); } /** From 5af7ed9f249e0bf30657f22e00240cbe394268e2 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:04:13 +0100 Subject: [PATCH 04/23] Move block which comes too early --- .../src/rules/prefer-nullish-coalescing.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 41a74fe30203..19a34fea8a08 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -461,12 +461,6 @@ export default createRule({ }); } - const tsNode = parserServices.esTreeNodeToTSNodeMap.get( - nullishCoalescingLeftNode, - ); - const type = checker.getTypeAtLocation(tsNode); - const flags = getTypeFlags(type); - // it is fixable if we check for both null and undefined, or not if neither if ( hasUndefinedCheckWithoutTruthinessCheck === @@ -480,6 +474,12 @@ export default createRule({ return true; } + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + nullishCoalescingLeftNode, + ); + const type = checker.getTypeAtLocation(tsNode); + const flags = getTypeFlags(type); + if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { return false; } From dd90ef8d79e96c374177cc8388076f3122f329a4 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:16:21 +0100 Subject: [PATCH 05/23] add tests --- .../rules/prefer-nullish-coalescing.test.ts | 97 ++++++++++++++++--- 1 file changed, 85 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 21b6f9562a21..d9cab57c09f5 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -339,51 +339,51 @@ declare let x: { n: object }; `, ` declare let x: { n: string[] }; -x ? x : y; +x.n ? x.n : y; `, ` declare let x: { n: string[] }; -!x ? y : x; +!x.n ? y : x.n; `, ` declare let x: { n: Function }; -x ? x : y; +x.n ? x.n : y; `, ` declare let x: { n: Function }; -!x ? y : x; +!x.n ? y : x.n; `, ` declare let x: { n: () => string }; -x ? x : y; +x.n ? x.n : y; `, ` declare let x: { n: () => string }; -!x ? y : x; +!x.n ? y : x.n; `, ` declare let x: { n: () => string | null }; -x ? x : y; +x.n ? x.n : y; `, ` declare let x: { n: () => string | null }; -!x ? y : x; +!x.n ? y : x.n; `, ` declare let x: { n: () => string | undefined }; -x ? x : y; +x.n ? x.n : y; `, ` declare let x: { n: () => string | undefined }; -!x ? y : x; +!x.n ? y : x.n; `, ` declare let x: { n: () => string | null | undefined }; -x ? x : y; +x.n ? x.n : y; `, ` declare let x: { n: () => string | null | undefined }; -!x ? y : x; +!x.n ? y : x.n; `, ].map(code => ({ code, @@ -2030,6 +2030,79 @@ x.n ?? y; output: null, })), + ...[ + ` +declare let x: { n?: { a?: string } }; +x?.n?.a ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a !== undefined ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a !== undefined ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a !== undefined ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a != undefined ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a != undefined ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a != undefined ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a != null ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a != null ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x?.n?.a != null ? x.n.a : y; + `, + ].map(code => ({ + code, + errors: [ + { + column: 1, + endColumn: code.split('\n')[2].length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverTernary' as const, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: ` +${code.split('\n')[1]} +x?.n?.a ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }] as const, + output: null, + })), + // noStrictNullCheck { code: ` From bc3ff36f8b487259826bac2ee2f2b22e6a569689 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:05:58 +0100 Subject: [PATCH 06/23] handle use case where 2 conditions --- .../src/rules/prefer-nullish-coalescing.ts | 40 ++++++++++--------- .../rules/prefer-nullish-coalescing.test.ts | 8 ++++ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 19a34fea8a08..fb5f0242fda4 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -423,7 +423,7 @@ export default createRule({ if ( testNode && - isTestNodeEquivalentToNonNullishBranchNode(testNode, node, operator) + isNodeEquivalent(testNode, getNonNullishBranchNode(node, operator)) ) { nullishCoalescingLeftNode = testNode; } @@ -435,13 +435,14 @@ export default createRule({ } else if (isUndefinedIdentifier(testNode)) { hasUndefinedCheckWithoutTruthinessCheck = true; } else if ( - isTestNodeEquivalentToNonNullishBranchNode( + isNodeEquivalent( testNode, - node, - operator, + getNonNullishBranchNode(node, operator), ) ) { - nullishCoalescingLeftNode = testNode; + if (!nullishCoalescingLeftNode) { + nullishCoalescingLeftNode = testNode; + } } else { return; } @@ -649,23 +650,24 @@ function isMixedLogicalExpression( return false; } -function isTestNodeEquivalentToNonNullishBranchNode( - testNode: TSESTree.Node, - node: TSESTree.ConditionalExpression, - operator: NullishCheckOperator, -): boolean { - const nonNullishBranchNode = getNonNullishBranchNode(node, operator); +/** + * Returns whether two nodes, comparing with the expression in case of chain, + * are equal + */ +function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { if ( - testNode.type === AST_NODE_TYPES.ChainExpression && - nonNullishBranchNode.type === AST_NODE_TYPES.MemberExpression + a.type === AST_NODE_TYPES.ChainExpression || + b.type === AST_NODE_TYPES.ChainExpression ) { - return isTestNodeEquivalentToNonNullishBranchNode( - testNode.expression, - node, - operator, - ); + if (a.type === AST_NODE_TYPES.ChainExpression) { + a = a.expression; + } + if (b.type === AST_NODE_TYPES.ChainExpression) { + b = b.expression; + } + return isNodeEquivalent(a, b); } - return isNodeEqual(testNode, nonNullishBranchNode); + return isNodeEqual(a, b); } /** diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index d9cab57c09f5..a60704b299cd 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -2079,6 +2079,14 @@ x?.n?.a != null ? x?.n.a : y; declare let x: { n?: { a?: string } }; x?.n?.a != null ? x.n.a : y; `, + ` +declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? x.n.a : y; + `, ].map(code => ({ code, errors: [ From 6a207b6f9c6a7027980836a5ea6500c8d2729bfa Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:09:05 +0100 Subject: [PATCH 07/23] renaming --- .../src/rules/prefer-nullish-coalescing.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index fb5f0242fda4..26ed62ccfc18 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -423,7 +423,10 @@ export default createRule({ if ( testNode && - isNodeEquivalent(testNode, getNonNullishBranchNode(node, operator)) + isNodeOrNodeExpressionEqual( + testNode, + getNonNullishBranchNode(node, operator), + ) ) { nullishCoalescingLeftNode = testNode; } @@ -435,7 +438,7 @@ export default createRule({ } else if (isUndefinedIdentifier(testNode)) { hasUndefinedCheckWithoutTruthinessCheck = true; } else if ( - isNodeEquivalent( + isNodeOrNodeExpressionEqual( testNode, getNonNullishBranchNode(node, operator), ) @@ -650,11 +653,10 @@ function isMixedLogicalExpression( return false; } -/** - * Returns whether two nodes, comparing with the expression in case of chain, - * are equal - */ -function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { +function isNodeOrNodeExpressionEqual( + a: TSESTree.Node, + b: TSESTree.Node, +): boolean { if ( a.type === AST_NODE_TYPES.ChainExpression || b.type === AST_NODE_TYPES.ChainExpression @@ -665,7 +667,7 @@ function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { if (b.type === AST_NODE_TYPES.ChainExpression) { b = b.expression; } - return isNodeEquivalent(a, b); + return isNodeOrNodeExpressionEqual(a, b); } return isNodeEqual(a, b); } From aee2f2360e825036a317f4ad36ed460546d0e7d6 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:27:34 +0100 Subject: [PATCH 08/23] changes after review --- .../src/rules/prefer-nullish-coalescing.ts | 43 +++++----- .../eslint-plugin/src/util/isNodeEqual.ts | 6 -- .../rules/prefer-nullish-coalescing.test.ts | 80 +++++++++++++++++++ .../tests/util/isNodeEqual.test.ts | 5 -- 4 files changed, 98 insertions(+), 36 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 26ed62ccfc18..cc104e97364d 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -425,7 +425,7 @@ export default createRule({ testNode && isNodeOrNodeExpressionEqual( testNode, - getNonNullishBranchNode(node, operator), + getBranchNodes(node, operator).nonNullishBranch, ) ) { nullishCoalescingLeftNode = testNode; @@ -440,12 +440,16 @@ export default createRule({ } else if ( isNodeOrNodeExpressionEqual( testNode, - getNonNullishBranchNode(node, operator), + getBranchNodes(node, operator).nonNullishBranch, ) ) { - if (!nullishCoalescingLeftNode) { - nullishCoalescingLeftNode = testNode; - } + // Only consider the first expression in a multi-part nullish check, + // as subsequent expressions might not require all the optional chaining operators. + // For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo'; + // This works because `node.test` is always evaluated first in the loop + // and has the same or more necessary optional chaining operators + // than `node.alternate` or `node.consequent`. + nullishCoalescingLeftNode ??= testNode; } else { return; } @@ -516,7 +520,7 @@ export default createRule({ node, `${getTextWithParentheses(context.sourceCode, nullishCoalescingLeftNode)} ?? ${getTextWithParentheses( context.sourceCode, - getNullishBranchNode(node, operator), + getBranchNodes(node, operator).nullishBranch, )}`, ); }, @@ -674,29 +678,18 @@ function isNodeOrNodeExpressionEqual( /** * Returns the branch nodes of a conditional expression: - * - the `nonNullish` branch is the branch when test node is not nullish - * - the `nullish` branch is the branch when test node is nullish + * - the "nonNullish branch" is the branch when test node is not nullish + * - the "nullish branch" is the branch when test node is nullish */ function getBranchNodes( node: TSESTree.ConditionalExpression, operator: NullishCheckOperator, -): { nonNullish: TSESTree.Expression; nullish: TSESTree.Expression } { +): { + nonNullishBranch: TSESTree.Expression; + nullishBranch: TSESTree.Expression; +} { if (!operator || ['!=', '!=='].includes(operator)) { - return { nonNullish: node.consequent, nullish: node.alternate }; + return { nonNullishBranch: node.consequent, nullishBranch: node.alternate }; } - return { nonNullish: node.alternate, nullish: node.consequent }; -} - -function getNonNullishBranchNode( - node: TSESTree.ConditionalExpression, - operator: NullishCheckOperator, -): TSESTree.Expression { - return getBranchNodes(node, operator).nonNullish; -} - -function getNullishBranchNode( - node: TSESTree.ConditionalExpression, - operator: NullishCheckOperator, -): TSESTree.Expression { - return getBranchNodes(node, operator).nullish; + return { nonNullishBranch: node.alternate, nullishBranch: node.consequent }; } diff --git a/packages/eslint-plugin/src/util/isNodeEqual.ts b/packages/eslint-plugin/src/util/isNodeEqual.ts index d37952ad6bf0..729b38b58888 100644 --- a/packages/eslint-plugin/src/util/isNodeEqual.ts +++ b/packages/eslint-plugin/src/util/isNodeEqual.ts @@ -29,11 +29,5 @@ export function isNodeEqual(a: TSESTree.Node, b: TSESTree.Node): boolean { isNodeEqual(a.property, b.property) && isNodeEqual(a.object, b.object) ); } - if ( - a.type === AST_NODE_TYPES.ChainExpression && - b.type === AST_NODE_TYPES.ChainExpression - ) { - return isNodeEqual(a.expression, b.expression); - } return false; } diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index a60704b299cd..5e3b356e263e 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -2033,6 +2033,86 @@ x.n ?? y; ...[ ` declare let x: { n?: { a?: string } }; +x.n?.a ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a !== undefined ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a !== undefined ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a !== undefined ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a != undefined ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a != undefined ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a != undefined ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a != null ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a != null ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string } }; +x.n?.a != null ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +x.n?.a !== undefined && x.n.a !== null ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +x.n?.a !== undefined && x.n.a !== null ? x.n.a : y; + `, + ].map(code => ({ + code, + errors: [ + { + column: 1, + endColumn: code.split('\n')[2].length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverTernary' as const, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: ` +${code.split('\n')[1]} +x.n?.a ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }] as const, + output: null, + })), + ...[ + ` +declare let x: { n?: { a?: string } }; x?.n?.a ? x?.n?.a : y; `, ` diff --git a/packages/eslint-plugin/tests/util/isNodeEqual.test.ts b/packages/eslint-plugin/tests/util/isNodeEqual.test.ts index f1eb23fea674..1177b1ac44bb 100644 --- a/packages/eslint-plugin/tests/util/isNodeEqual.test.ts +++ b/packages/eslint-plugin/tests/util/isNodeEqual.test.ts @@ -95,11 +95,6 @@ ruleTester.run('isNodeEqual', rule, { errors: [{ messageId: 'removeExpression' }], output: 'x.z[1][this[this.o]]["3"][a.b.c]', }, - { - code: 'a?.b || a?.b', - errors: [{ messageId: 'removeExpression' }], - output: 'a?.b', - }, ], valid: [ { code: 'a || b' }, From 833b9678760b0b4355586c422d327087d16e19fb Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 16 Feb 2025 01:09:07 +0100 Subject: [PATCH 09/23] use new API --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index cc104e97364d..0f08c1f2da7f 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -169,7 +169,6 @@ export default createRule({ const parserServices = getParserServices(context); const compilerOptions = parserServices.program.getCompilerOptions(); - const checker = parserServices.program.getTypeChecker(); const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled( compilerOptions, 'strictNullChecks', @@ -482,10 +481,9 @@ export default createRule({ return true; } - const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + const type = parserServices.getTypeAtLocation( nullishCoalescingLeftNode, ); - const type = checker.getTypeAtLocation(tsNode); const flags = getTypeFlags(type); if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { From 628c69dc9468ccea369294cd6e8d711bb7debf32 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 16 Feb 2025 01:19:28 +0100 Subject: [PATCH 10/23] move skipChainExpression to utils --- .../src/rules/prefer-nullish-coalescing.ts | 15 ++------------- .../src/rules/prefer-promise-reject-errors.ts | 9 +-------- packages/eslint-plugin/src/util/index.ts | 1 + .../eslint-plugin/src/util/skipChainExpression.ts | 9 +++++++++ 4 files changed, 13 insertions(+), 21 deletions(-) create mode 100644 packages/eslint-plugin/src/util/skipChainExpression.ts diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 0f08c1f2da7f..606493318a16 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -17,6 +17,7 @@ import { isUndefinedIdentifier, nullThrows, NullThrowsReasons, + skipChainExpression, } from '../util'; const isIdentifierOrMemberOrChainExpression = isNodeOfTypes([ @@ -659,19 +660,7 @@ function isNodeOrNodeExpressionEqual( a: TSESTree.Node, b: TSESTree.Node, ): boolean { - if ( - a.type === AST_NODE_TYPES.ChainExpression || - b.type === AST_NODE_TYPES.ChainExpression - ) { - if (a.type === AST_NODE_TYPES.ChainExpression) { - a = a.expression; - } - if (b.type === AST_NODE_TYPES.ChainExpression) { - b = b.expression; - } - return isNodeOrNodeExpressionEqual(a, b); - } - return isNodeEqual(a, b); + return isNodeEqual(skipChainExpression(a), skipChainExpression(b)); } /** diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index 8997b4ef7448..534af6cd2be5 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -14,6 +14,7 @@ import { isPromiseLike, isReadonlyErrorLike, isStaticMemberAccessOfValue, + skipChainExpression, } from '../util'; export type MessageIds = 'rejectAnError'; @@ -102,14 +103,6 @@ export default createRule({ }); } - function skipChainExpression( - node: T, - ): T | TSESTree.ChainElement { - return node.type === AST_NODE_TYPES.ChainExpression - ? node.expression - : node; - } - function typeAtLocationIsLikePromise(node: TSESTree.Node): boolean { const type = services.getTypeAtLocation(node); return ( diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index c8a0927b162b..08c8b5a97a9e 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -26,6 +26,7 @@ export * from './types'; export * from './getConstraintInfo'; export * from './getValueOfLiteralType'; export * from './truthinessAndNullishUtils'; +export * from './skipChainExpression'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; diff --git a/packages/eslint-plugin/src/util/skipChainExpression.ts b/packages/eslint-plugin/src/util/skipChainExpression.ts new file mode 100644 index 000000000000..87ac37cc3415 --- /dev/null +++ b/packages/eslint-plugin/src/util/skipChainExpression.ts @@ -0,0 +1,9 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +export function skipChainExpression( + node: T, +): T | TSESTree.ChainElement { + return node.type === AST_NODE_TYPES.ChainExpression ? node.expression : node; +} From 3c14cf651f7e0d25ac58ae24b423a046c4bfd892 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 16 Feb 2025 01:55:22 +0100 Subject: [PATCH 11/23] use skipChainExpression util for no-floating-promises as well --- packages/eslint-plugin/src/rules/no-floating-promises.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 880ae86cb838..8e91c1987b62 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -15,6 +15,7 @@ import { OperatorPrecedence, readonlynessOptionsDefaults, readonlynessOptionsSchema, + skipChainExpression, typeMatchesSomeSpecifier, } from '../util'; @@ -135,11 +136,7 @@ export default createRule({ return; } - let expression = node.expression; - - if (expression.type === AST_NODE_TYPES.ChainExpression) { - expression = expression.expression; - } + const expression = skipChainExpression(node.expression); if (isKnownSafePromiseReturn(expression)) { return; From 201e19661d7f679cc72a603130209a2b3fb57eb0 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 17 Feb 2025 01:11:26 +0100 Subject: [PATCH 12/23] use skipChainExpression wherever it's possible --- .../src/rules/no-inferrable-types.ts | 17 ++++++++------ .../eslint-plugin/src/rules/prefer-find.ts | 22 ++++++------------- .../rules/prefer-string-starts-ends-with.ts | 11 +++++----- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-inferrable-types.ts b/packages/eslint-plugin/src/rules/no-inferrable-types.ts index 4d8496a7b288..a8d26ad2eb00 100644 --- a/packages/eslint-plugin/src/rules/no-inferrable-types.ts +++ b/packages/eslint-plugin/src/rules/no-inferrable-types.ts @@ -3,7 +3,12 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule, nullThrows, NullThrowsReasons } from '../util'; +import { + createRule, + nullThrows, + NullThrowsReasons, + skipChainExpression, +} from '../util'; export type Options = [ { @@ -55,14 +60,12 @@ export default createRule({ init: TSESTree.Expression, callName: string, ): boolean { - if (init.type === AST_NODE_TYPES.ChainExpression) { - return isFunctionCall(init.expression, callName); - } + const node = skipChainExpression(init); return ( - init.type === AST_NODE_TYPES.CallExpression && - init.callee.type === AST_NODE_TYPES.Identifier && - init.callee.name === callName + node.type === AST_NODE_TYPES.CallExpression && + node.callee.type === AST_NODE_TYPES.Identifier && + node.callee.name === callName ); } function isLiteral(init: TSESTree.Expression, typeName: string): boolean { diff --git a/packages/eslint-plugin/src/rules/prefer-find.ts b/packages/eslint-plugin/src/rules/prefer-find.ts index 0a7c4d099542..13867be30778 100644 --- a/packages/eslint-plugin/src/rules/prefer-find.ts +++ b/packages/eslint-plugin/src/rules/prefer-find.ts @@ -12,6 +12,7 @@ import { getStaticValue, isStaticMemberAccessOfValue, nullThrows, + skipChainExpression, } from '../util'; export default createRule({ @@ -56,23 +57,17 @@ export default createRule({ return parseArrayFilterExpressions(lastExpression); } - if (expression.type === AST_NODE_TYPES.ChainExpression) { - return parseArrayFilterExpressions(expression.expression); - } + const node = skipChainExpression(expression); // This is the only reason we're returning a list rather than a single value. - if (expression.type === AST_NODE_TYPES.ConditionalExpression) { + if (node.type === AST_NODE_TYPES.ConditionalExpression) { // Both branches of the ternary _must_ return results. - const consequentResult = parseArrayFilterExpressions( - expression.consequent, - ); + const consequentResult = parseArrayFilterExpressions(node.consequent); if (consequentResult.length === 0) { return []; } - const alternateResult = parseArrayFilterExpressions( - expression.alternate, - ); + const alternateResult = parseArrayFilterExpressions(node.alternate); if (alternateResult.length === 0) { return []; } @@ -82,11 +77,8 @@ export default createRule({ } // Check if it looks like <>(...), but not <>?.(...) - if ( - expression.type === AST_NODE_TYPES.CallExpression && - !expression.optional - ) { - const callee = expression.callee; + if (node.type === AST_NODE_TYPES.CallExpression && !node.optional) { + const callee = node.callee; // Check if it looks like <>.filter(...) or <>['filter'](...), // or the optional chaining variants. if (callee.type === AST_NODE_TYPES.MemberExpression) { diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index 29742391d807..d95dd991c7c7 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -13,6 +13,7 @@ import { isStaticMemberAccessOfValue, nullThrows, NullThrowsReasons, + skipChainExpression, } from '../util'; const EQ_OPERATORS = /^[=!]=/; @@ -308,15 +309,13 @@ export default createRule({ function getLeftNode( node: TSESTree.Expression | TSESTree.PrivateIdentifier, ): TSESTree.MemberExpression { - if (node.type === AST_NODE_TYPES.ChainExpression) { - return getLeftNode(node.expression); - } + const skippedChainExpressionNode = skipChainExpression(node); let leftNode; - if (node.type === AST_NODE_TYPES.CallExpression) { - leftNode = node.callee; + if (skippedChainExpressionNode.type === AST_NODE_TYPES.CallExpression) { + leftNode = skippedChainExpressionNode.callee; } else { - leftNode = node; + leftNode = skippedChainExpressionNode; } if (leftNode.type !== AST_NODE_TYPES.MemberExpression) { From 0318385f9046d2ef9916a9567531022df1129e13 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 17 Feb 2025 09:53:14 +0100 Subject: [PATCH 13/23] move line --- packages/eslint-plugin/src/rules/prefer-find.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-find.ts b/packages/eslint-plugin/src/rules/prefer-find.ts index 13867be30778..ce4c963cb508 100644 --- a/packages/eslint-plugin/src/rules/prefer-find.ts +++ b/packages/eslint-plugin/src/rules/prefer-find.ts @@ -48,17 +48,17 @@ export default createRule({ function parseArrayFilterExpressions( expression: TSESTree.Expression, ): FilterExpressionData[] { - if (expression.type === AST_NODE_TYPES.SequenceExpression) { + const node = skipChainExpression(expression); + + if (node.type === AST_NODE_TYPES.SequenceExpression) { // Only the last expression in (a, b, [1, 2, 3].filter(condition))[0] matters const lastExpression = nullThrows( - expression.expressions.at(-1), + node.expressions.at(-1), 'Expected to have more than zero expressions in a sequence expression', ); return parseArrayFilterExpressions(lastExpression); } - const node = skipChainExpression(expression); - // This is the only reason we're returning a list rather than a single value. if (node.type === AST_NODE_TYPES.ConditionalExpression) { // Both branches of the ternary _must_ return results. From 259a4a19e543d442bcf89f26b90e03b428ae21af Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 17 Feb 2025 09:55:52 +0100 Subject: [PATCH 14/23] simplify --- .../src/rules/prefer-string-starts-ends-with.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index d95dd991c7c7..ee946a4f85a0 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -307,16 +307,11 @@ export default createRule({ } function getLeftNode( - node: TSESTree.Expression | TSESTree.PrivateIdentifier, + init: TSESTree.Expression | TSESTree.PrivateIdentifier, ): TSESTree.MemberExpression { - const skippedChainExpressionNode = skipChainExpression(node); - - let leftNode; - if (skippedChainExpressionNode.type === AST_NODE_TYPES.CallExpression) { - leftNode = skippedChainExpressionNode.callee; - } else { - leftNode = skippedChainExpressionNode; - } + const node = skipChainExpression(init); + const leftNode = + node.type === AST_NODE_TYPES.CallExpression ? node.callee : node; if (leftNode.type !== AST_NODE_TYPES.MemberExpression) { throw new Error(`Expected a MemberExpression, got ${leftNode.type}`); From 97fa2499b041dc6d9f0d9114eb99b283d582fc48 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:13:55 +0100 Subject: [PATCH 15/23] add tests --- .../src/rules/prefer-nullish-coalescing.ts | 25 ++- .../rules/prefer-nullish-coalescing.test.ts | 145 ++++++++++++++++++ 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 606493318a16..494e81050726 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -423,7 +423,7 @@ export default createRule({ if ( testNode && - isNodeOrNodeExpressionEqual( + isNodeEquivalent( testNode, getBranchNodes(node, operator).nonNullishBranch, ) @@ -438,7 +438,7 @@ export default createRule({ } else if (isUndefinedIdentifier(testNode)) { hasUndefinedCheckWithoutTruthinessCheck = true; } else if ( - isNodeOrNodeExpressionEqual( + isNodeEquivalent( testNode, getBranchNodes(node, operator).nonNullishBranch, ) @@ -656,10 +656,23 @@ function isMixedLogicalExpression( return false; } -function isNodeOrNodeExpressionEqual( - a: TSESTree.Node, - b: TSESTree.Node, -): boolean { +function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { + if ( + a.type === AST_NODE_TYPES.MemberExpression && + a.object.type === AST_NODE_TYPES.ChainExpression && + b.type === AST_NODE_TYPES.MemberExpression + ) { + return ( + isNodeEqual(a.property, b.property) && + isNodeEquivalent(a.object.expression, b.object) + ); + } + if ( + a.type === AST_NODE_TYPES.ChainExpression || + b.type === AST_NODE_TYPES.ChainExpression + ) { + return isNodeEquivalent(skipChainExpression(a), skipChainExpression(b)); + } return isNodeEqual(skipChainExpression(a), skipChainExpression(b)); } diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 5e3b356e263e..1aa35f847af4 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -2117,6 +2117,10 @@ x?.n?.a ? x?.n?.a : y; `, ` declare let x: { n?: { a?: string } }; +x?.n?.a ? x.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; x?.n?.a ? x?.n.a : y; `, ` @@ -2129,6 +2133,10 @@ x?.n?.a !== undefined ? x?.n?.a : y; `, ` declare let x: { n?: { a?: string } }; +x?.n?.a !== undefined ? x.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; x?.n?.a !== undefined ? x?.n.a : y; `, ` @@ -2141,6 +2149,10 @@ x?.n?.a != undefined ? x?.n?.a : y; `, ` declare let x: { n?: { a?: string } }; +x?.n?.a != undefined ? x.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; x?.n?.a != undefined ? x?.n.a : y; `, ` @@ -2153,6 +2165,10 @@ x?.n?.a != null ? x?.n?.a : y; `, ` declare let x: { n?: { a?: string } }; +x?.n?.a != null ? x.n?.a : y; + `, + ` +declare let x: { n?: { a?: string } }; x?.n?.a != null ? x?.n.a : y; `, ` @@ -2165,8 +2181,32 @@ x?.n?.a !== undefined && x.n.a !== null ? x?.n?.a : y; `, ` declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? x.n?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; x?.n?.a !== undefined && x.n.a !== null ? x.n.a : y; `, + ` +declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? (x?.n)?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? (x.n)?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? (x?.n).a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +x?.n?.a !== undefined && x.n.a !== null ? (x.n).a : y; + `, ].map(code => ({ code, errors: [ @@ -2190,6 +2230,111 @@ x?.n?.a ?? y; options: [{ ignoreTernaryTests: false }] as const, output: null, })), + ...[ + ` +declare let x: { n?: { a?: string | null } }; +(x?.n)?.a ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x?.n)?.a ? x.n?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x?.n)?.a ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x?.n)?.a ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x?.n)?.a ? (x?.n)?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x?.n)?.a ? (x.n)?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x?.n)?.a ? (x?.n).a : y; + `, + ].map(code => ({ + code, + errors: [ + { + column: 1, + endColumn: code.split('\n')[2].length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverTernary' as const, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: ` +${code.split('\n')[1]} +(x?.n)?.a ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }] as const, + output: null, + })), + + ...[ + ` +declare let x: { n?: { a?: string | null } }; +(x.n)?.a ? x?.n?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x.n)?.a ? x.n?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x.n)?.a ? x?.n.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x.n)?.a ? x.n.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x.n)?.a ? (x?.n)?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x.n)?.a ? (x.n)?.a : y; + `, + ` +declare let x: { n?: { a?: string | null } }; +(x.n)?.a ? (x?.n).a : y; + `, + ].map(code => ({ + code, + errors: [ + { + column: 1, + endColumn: code.split('\n')[2].length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverTernary' as const, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: ` +${code.split('\n')[1]} +(x.n)?.a ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }] as const, + output: null, + })), // noStrictNullCheck { From 1eba720b9581f1a5d4c74d242dcb16fc22634621 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:16:02 +0100 Subject: [PATCH 16/23] add symetric node equivalence --- .../src/rules/prefer-nullish-coalescing.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 494e81050726..b48ada6b4144 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -667,6 +667,16 @@ function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { isNodeEquivalent(a.object.expression, b.object) ); } + if ( + b.type === AST_NODE_TYPES.MemberExpression && + b.object.type === AST_NODE_TYPES.ChainExpression && + a.type === AST_NODE_TYPES.MemberExpression + ) { + return ( + isNodeEqual(a.property, b.property) && + isNodeEquivalent(a.object, b.object.expression) + ); + } if ( a.type === AST_NODE_TYPES.ChainExpression || b.type === AST_NODE_TYPES.ChainExpression From 23d52a57f6d87b9f9cdea1cd25f3acc755a0aaa3 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:23:39 +0100 Subject: [PATCH 17/23] simplify --- .../src/rules/prefer-nullish-coalescing.ts | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index b48ada6b4144..4efd24abf09b 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -659,23 +659,17 @@ function isMixedLogicalExpression( function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { if ( a.type === AST_NODE_TYPES.MemberExpression && - a.object.type === AST_NODE_TYPES.ChainExpression && b.type === AST_NODE_TYPES.MemberExpression ) { - return ( - isNodeEqual(a.property, b.property) && - isNodeEquivalent(a.object.expression, b.object) - ); - } - if ( - b.type === AST_NODE_TYPES.MemberExpression && - b.object.type === AST_NODE_TYPES.ChainExpression && - a.type === AST_NODE_TYPES.MemberExpression - ) { - return ( - isNodeEqual(a.property, b.property) && - isNodeEquivalent(a.object, b.object.expression) - ); + if (!isNodeEqual(a.property, b.property)) { + return false; + } + if (a.object.type === AST_NODE_TYPES.ChainExpression) { + return isNodeEquivalent(a.object.expression, b.object); + } + if (b.object.type === AST_NODE_TYPES.ChainExpression) { + return isNodeEquivalent(a.object, b.object.expression); + } } if ( a.type === AST_NODE_TYPES.ChainExpression || From 53dd427944a14e46492c94aaa461347ba806c07c Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:35:37 +0100 Subject: [PATCH 18/23] simplify --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 4efd24abf09b..5c8d492f8904 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -664,12 +664,7 @@ function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { if (!isNodeEqual(a.property, b.property)) { return false; } - if (a.object.type === AST_NODE_TYPES.ChainExpression) { - return isNodeEquivalent(a.object.expression, b.object); - } - if (b.object.type === AST_NODE_TYPES.ChainExpression) { - return isNodeEquivalent(a.object, b.object.expression); - } + return isNodeEquivalent(a.object, b.object); } if ( a.type === AST_NODE_TYPES.ChainExpression || From 7114958105007a7afa9cea26991c6fafc61feff5 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:02:53 +0100 Subject: [PATCH 19/23] Simplify --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 5c8d492f8904..c486f0b27660 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -661,10 +661,10 @@ function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression ) { - if (!isNodeEqual(a.property, b.property)) { - return false; - } - return isNodeEquivalent(a.object, b.object); + return ( + isNodeEqual(a.property, b.property)) && + isNodeEquivalent(a.object, b.object) + ); } if ( a.type === AST_NODE_TYPES.ChainExpression || From 8eed250d52f7bbf07ffdc5a6aa909ec92da4e45a Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:10:10 +0100 Subject: [PATCH 20/23] Typo --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index c486f0b27660..390e3de9c84d 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -662,7 +662,7 @@ function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { b.type === AST_NODE_TYPES.MemberExpression ) { return ( - isNodeEqual(a.property, b.property)) && + isNodeEqual(a.property, b.property) && isNodeEquivalent(a.object, b.object) ); } From a1674cfc940db482b6b1382d6122772951a2cd8c Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:16:00 +0100 Subject: [PATCH 21/23] Simplify --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 390e3de9c84d..60cd4ddcef11 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -672,7 +672,7 @@ function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { ) { return isNodeEquivalent(skipChainExpression(a), skipChainExpression(b)); } - return isNodeEqual(skipChainExpression(a), skipChainExpression(b)); + return isNodeEqual(a, b); } /** From b31b29e38a29e9face19bacb605f593ac9ea3e27 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:09:08 +0100 Subject: [PATCH 22/23] renaming and adding JS doc --- .../src/rules/prefer-nullish-coalescing.ts | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 60cd4ddcef11..8ff517de9535 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -423,7 +423,7 @@ export default createRule({ if ( testNode && - isNodeEquivalent( + areNodesSimilarMemberAccess( testNode, getBranchNodes(node, operator).nonNullishBranch, ) @@ -438,7 +438,7 @@ export default createRule({ } else if (isUndefinedIdentifier(testNode)) { hasUndefinedCheckWithoutTruthinessCheck = true; } else if ( - isNodeEquivalent( + areNodesSimilarMemberAccess( testNode, getBranchNodes(node, operator).nonNullishBranch, ) @@ -656,21 +656,40 @@ function isMixedLogicalExpression( return false; } -function isNodeEquivalent(a: TSESTree.Node, b: TSESTree.Node): boolean { +/** + * Checks if two TSESTree nodes have similar member access orders, + * ignoring optional chaining differences. + * + * Note: This doesn't mean the nodes are runtime-equivalent, + * it simply verifies that the member access sequence is the same. + * + * Example: `a.b.c`, `a?.b.c`, `a.b?.c`, `(a?.b).c`, `(a.b)?.c` are considered similar. + * + * @param a First TSESTree node. + * @param b Second TSESTree node. + * @returns `true` if the nodes access members in the same order; otherwise, `false`. + */ +function areNodesSimilarMemberAccess( + a: TSESTree.Node, + b: TSESTree.Node, +): boolean { if ( a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression ) { return ( isNodeEqual(a.property, b.property) && - isNodeEquivalent(a.object, b.object) + areNodesSimilarMemberAccess(a.object, b.object) ); } if ( a.type === AST_NODE_TYPES.ChainExpression || b.type === AST_NODE_TYPES.ChainExpression ) { - return isNodeEquivalent(skipChainExpression(a), skipChainExpression(b)); + return areNodesSimilarMemberAccess( + skipChainExpression(a), + skipChainExpression(b), + ); } return isNodeEqual(a, b); } From c131cadfd3b37ec7df3bed8687690a7b0044cc68 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:14:26 +0100 Subject: [PATCH 23/23] simplify js doc --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 8ff517de9535..54d7b060dcb8 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -657,11 +657,10 @@ function isMixedLogicalExpression( } /** - * Checks if two TSESTree nodes have similar member access orders, - * ignoring optional chaining differences. + * Checks if two TSESTree nodes have the same member access sequence, + * regardless of optional chaining differences. * - * Note: This doesn't mean the nodes are runtime-equivalent, - * it simply verifies that the member access sequence is the same. + * Note: This does not imply that the nodes are runtime-equivalent. * * Example: `a.b.c`, `a?.b.c`, `a.b?.c`, `(a?.b).c`, `(a.b)?.c` are considered similar. *