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. * pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy