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; 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..ce4c963cb508 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({ @@ -47,32 +48,26 @@ 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); } - if (expression.type === AST_NODE_TYPES.ChainExpression) { - return parseArrayFilterExpressions(expression.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-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index e52f83543af7..54d7b060dcb8 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -17,13 +17,17 @@ import { isUndefinedIdentifier, nullThrows, NullThrowsReasons, + skipChainExpression, } 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; + export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -166,7 +170,6 @@ export default createRule({ const parserServices = getParserServices(context); const compilerOptions = parserServices.program.getCompilerOptions(); - const checker = parserServices.program.getTypeChecker(); const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled( compilerOptions, 'strictNullChecks', @@ -340,7 +343,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 +401,35 @@ 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 && + areNodesSimilarMemberAccess( + testNode, + getBranchNodes(node, operator).nonNullishBranch, + ) + ) { + nullishCoalescingLeftNode = testNode; + } } else { // we check that the test only contains null, undefined and the identifier for (const testNode of nodesInsideTestExpression) { @@ -428,22 +438,25 @@ export default createRule({ } else if (isUndefinedIdentifier(testNode)) { hasUndefinedCheckWithoutTruthinessCheck = true; } else if ( - (operator === '!==' || operator === '!=') && - isNodeEqual(testNode, node.consequent) + areNodesSimilarMemberAccess( + testNode, + getBranchNodes(node, operator).nonNullishBranch, + ) ) { - identifierOrMemberExpression = testNode; - } else if ( - (operator === '===' || operator === '==') && - isNodeEqual(testNode, node.alternate) - ) { - identifierOrMemberExpression = 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; } } } - if (!identifierOrMemberExpression) { + if (!nullishCoalescingLeftNode) { return; } @@ -452,16 +465,10 @@ export default createRule({ if (hasTruthinessCheck) { return isTruthinessCheckEligibleForPreferNullish({ node, - testNode: identifierOrMemberExpression, + testNode: nullishCoalescingLeftNode, }); } - const tsNode = parserServices.esTreeNodeToTSNodeMap.get( - identifierOrMemberExpression, - ); - 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 === @@ -475,6 +482,11 @@ export default createRule({ return true; } + const type = parserServices.getTypeAtLocation( + nullishCoalescingLeftNode, + ); + const flags = getTypeFlags(type); + if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { return false; } @@ -503,15 +515,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, + getBranchNodes(node, operator).nullishBranch, )}`, ); }, @@ -647,3 +655,58 @@ function isMixedLogicalExpression( return false; } + +/** + * Checks if two TSESTree nodes have the same member access sequence, + * regardless of optional chaining differences. + * + * 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. + * + * @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) && + areNodesSimilarMemberAccess(a.object, b.object) + ); + } + if ( + a.type === AST_NODE_TYPES.ChainExpression || + b.type === AST_NODE_TYPES.ChainExpression + ) { + return areNodesSimilarMemberAccess( + skipChainExpression(a), + skipChainExpression(b), + ); + } + return isNodeEqual(a, b); +} + +/** + * 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, +): { + nonNullishBranch: TSESTree.Expression; + nullishBranch: TSESTree.Expression; +} { + if (!operator || ['!=', '!=='].includes(operator)) { + return { nonNullishBranch: node.consequent, nullishBranch: node.alternate }; + } + return { nonNullishBranch: node.alternate, nullishBranch: node.consequent }; +} 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/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index 29742391d807..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 @@ -13,6 +13,7 @@ import { isStaticMemberAccessOfValue, nullThrows, NullThrowsReasons, + skipChainExpression, } from '../util'; const EQ_OPERATORS = /^[=!]=/; @@ -306,18 +307,11 @@ export default createRule({ } function getLeftNode( - node: TSESTree.Expression | TSESTree.PrivateIdentifier, + init: TSESTree.Expression | TSESTree.PrivateIdentifier, ): TSESTree.MemberExpression { - if (node.type === AST_NODE_TYPES.ChainExpression) { - return getLeftNode(node.expression); - } - - let leftNode; - if (node.type === AST_NODE_TYPES.CallExpression) { - leftNode = node.callee; - } else { - leftNode = node; - } + 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}`); 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; +} 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..1aa35f847af4 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, @@ -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; @@ -1990,6 +2030,312 @@ 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; + `, + ` +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; + `, + ` +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 != 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 } }; +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; + `, + ` +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 | 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 { code: ` @@ -4698,5 +5044,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, + }, ], });