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, + }, ], }); 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