From fcc9bea7925895052665ec242446c8991ae2a624 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:19:45 +0100 Subject: [PATCH 01/18] support if statement assignment --- .../docs/rules/prefer-nullish-coalescing.mdx | 56 ++- .../src/rules/consistent-type-exports.ts | 10 +- .../src/rules/no-unsafe-return.ts | 4 +- .../src/rules/prefer-nullish-coalescing.ts | 414 +++++++++++------- .../prefer-nullish-coalescing.shot | 78 +++- .../rules/prefer-nullish-coalescing.test.ts | 338 ++++++++++++++ .../components/lib/createCompilerOptions.ts | 4 +- 7 files changed, 717 insertions(+), 187 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx index 87cf88527046..d162eba17933 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx @@ -17,6 +17,54 @@ This rule reports when you may consider replacing: - An `||` operator with `??` - An `||=` operator with `??=` - Ternary expressions (`?:`) that are equivalent to `||` or `??` with `??` +- Assignment expressions (`=`) that can be safely replaced by `??=` + +## Examples + + + + +```ts +declare const a: string | null; +declare const b: string | null; + +const c = a || b; + +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitializeFooByTruthiness() { + if (!foo) { + foo = makeFoo(); + } +} + +function lazyInitializeFooByNullCheck() { + if (foo == null) { + foo = makeFoo(); + } +} +``` + + + + +```ts +declare const a: string | null; +declare const b: string | null; + +const c = a ?? b; + +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitializeFoo() { + foo ??= makeFoo(); +} +``` + + + :::caution This rule will not work as expected if [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) is not enabled. @@ -84,7 +132,7 @@ Examples of code for this rule with `{ ignoreConditionalTests: false }`: ```ts option='{ "ignoreConditionalTests": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a || b) { @@ -102,7 +150,7 @@ a || b ? true : false; ```ts option='{ "ignoreConditionalTests": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a ?? b) { @@ -133,7 +181,7 @@ Examples of code for this rule with `{ ignoreMixedLogicalExpressions: false }`: ```ts option='{ "ignoreMixedLogicalExpressions": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; @@ -149,7 +197,7 @@ a || (b && c && d); ```ts option='{ "ignoreMixedLogicalExpressions": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; diff --git a/packages/eslint-plugin/src/rules/consistent-type-exports.ts b/packages/eslint-plugin/src/rules/consistent-type-exports.ts index 214d4402ced4..324a622255d7 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-exports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-exports.ts @@ -203,13 +203,11 @@ export default createRule({ // Cache the first encountered exports for the package. We will need to come // back to these later when fixing the problems. if (node.exportKind === 'type') { - if (sourceExports.typeOnlyNamedExport == null) { - // The export is a type export - sourceExports.typeOnlyNamedExport = node; - } - } else if (sourceExports.valueOnlyNamedExport == null) { + // The export is a type export + sourceExports.typeOnlyNamedExport ??= node; + } else { // The export is a value export - sourceExports.valueOnlyNamedExport = node; + sourceExports.valueOnlyNamedExport ??= node; } // Next for the current export, we will separate type/value specifiers. diff --git a/packages/eslint-plugin/src/rules/no-unsafe-return.ts b/packages/eslint-plugin/src/rules/no-unsafe-return.ts index 838f84eff6af..e247b67bbad0 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-return.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-return.ts @@ -80,9 +80,7 @@ export default createRule({ ts.isArrowFunction(functionTSNode) ? getContextualType(checker, functionTSNode) : services.getTypeAtLocation(functionNode); - if (!functionType) { - functionType = services.getTypeAtLocation(functionNode); - } + functionType ??= services.getTypeAtLocation(functionNode); const callSignatures = tsutils.getCallSignaturesOfType(functionType); // If there is an explicit type annotation *and* that type matches the actual // function return type, we shouldn't complain (it's intentional, even if unsafe) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 54d7b060dcb8..7e1b5e5e0afc 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -26,7 +26,7 @@ const isIdentifierOrMemberOrChainExpression = isNodeOfTypes([ AST_NODE_TYPES.MemberExpression, ] as const); -type NullishCheckOperator = '!' | '!=' | '!==' | '==' | '===' | undefined; +type NullishCheckOperator = '!' | '!=' | '!==' | '' | '==' | '==='; export type Options = [ { @@ -48,6 +48,7 @@ export type Options = [ export type MessageIds = | 'noStrictNullCheck' + | 'preferNullishOverAssignment' | 'preferNullishOverOr' | 'preferNullishOverTernary' | 'suggestNullish'; @@ -66,6 +67,8 @@ export default createRule({ messages: { noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', + preferNullishOverAssignment: + 'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of an assignment expression, as it is simpler to read.', preferNullishOverOr: 'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of a logical {{ description }} (`||{{ equals }}`), as it is a safer operator.', preferNullishOverTernary: @@ -244,6 +247,7 @@ export default createRule({ node: | TSESTree.AssignmentExpression | TSESTree.ConditionalExpression + | TSESTree.IfStatement | TSESTree.LogicalExpression; testNode: TSESTree.Node; }): boolean { @@ -332,179 +336,133 @@ export default createRule({ }); } - return { - 'AssignmentExpression[operator = "||="]'( - node: TSESTree.AssignmentExpression, - ): void { - checkAndFixWithPreferNullishOverOr(node, 'assignment', '='); - }, - ConditionalExpression(node: TSESTree.ConditionalExpression): void { - if (ignoreTernaryTests) { - return; - } - - let operator: NullishCheckOperator; - let nodesInsideTestExpression: TSESTree.Node[] = []; - if (node.test.type === AST_NODE_TYPES.BinaryExpression) { - nodesInsideTestExpression = [node.test.left, node.test.right]; - if ( - node.test.operator === '==' || - node.test.operator === '!=' || - node.test.operator === '===' || - node.test.operator === '!==' - ) { - operator = node.test.operator; - } - } else if ( - node.test.type === AST_NODE_TYPES.LogicalExpression && - node.test.left.type === AST_NODE_TYPES.BinaryExpression && - node.test.right.type === AST_NODE_TYPES.BinaryExpression + function getNullishCoalescingParams( + node: TSESTree.ConditionalExpression | TSESTree.IfStatement, + nonNullishNode: TSESTree.Expression, + nodesInsideTestExpression: TSESTree.Node[], + operator: NullishCheckOperator, + ): + | { isFixable: false } + | { isFixable: true; nullishCoalescingLeftNode: TSESTree.Node } { + let nullishCoalescingLeftNode: TSESTree.Node | undefined; + let hasTruthinessCheck = false; + let hasNullCheckWithoutTruthinessCheck = false; + let hasUndefinedCheckWithoutTruthinessCheck = false; + + if (['!', ''].includes(operator)) { + hasTruthinessCheck = true; + nullishCoalescingLeftNode = + node.test.type === AST_NODE_TYPES.UnaryExpression + ? node.test.argument + : node.test; + + if ( + !areNodesSimilarMemberAccess( + nullishCoalescingLeftNode, + nonNullishNode, + ) ) { - nodesInsideTestExpression = [ - node.test.left.left, - node.test.left.right, - node.test.right.left, - node.test.right.right, - ]; - if (['||', '||='].includes(node.test.operator)) { - if ( - node.test.left.operator === '===' && - node.test.right.operator === '===' - ) { - operator = '==='; - } else if ( - ((node.test.left.operator === '===' || - node.test.right.operator === '===') && - (node.test.left.operator === '==' || - node.test.right.operator === '==')) || - (node.test.left.operator === '==' && - node.test.right.operator === '==') - ) { - operator = '=='; - } - } else if (node.test.operator === '&&') { - if ( - node.test.left.operator === '!==' && - node.test.right.operator === '!==' - ) { - operator = '!=='; - } else if ( - ((node.test.left.operator === '!==' || - node.test.right.operator === '!==') && - (node.test.left.operator === '!=' || - node.test.right.operator === '!=')) || - (node.test.left.operator === '!=' && - node.test.right.operator === '!=') - ) { - operator = '!='; - } + return { isFixable: false }; + } + } else { + // we check that the test only contains null, undefined and the identifier + for (const testNode of nodesInsideTestExpression) { + if (isNullLiteral(testNode)) { + hasNullCheckWithoutTruthinessCheck = true; + } else if (isUndefinedIdentifier(testNode)) { + hasUndefinedCheckWithoutTruthinessCheck = true; + } else if (areNodesSimilarMemberAccess(testNode, nonNullishNode)) { + // 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 { isFixable: false }; } } + } - let nullishCoalescingLeftNode: TSESTree.Node | undefined; - let hasTruthinessCheck = false; - let hasNullCheckWithoutTruthinessCheck = false; - let hasUndefinedCheckWithoutTruthinessCheck = false; - - if (!operator) { - let testNode: TSESTree.Node | undefined; - hasTruthinessCheck = true; - - if (isIdentifierOrMemberOrChainExpression(node.test)) { - testNode = node.test; - } else if ( - node.test.type === AST_NODE_TYPES.UnaryExpression && - isIdentifierOrMemberOrChainExpression(node.test.argument) && - node.test.operator === '!' - ) { - testNode = node.test.argument; - operator = '!'; - } + if (!nullishCoalescingLeftNode) { + return { isFixable: false }; + } - 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) { - if (isNullLiteral(testNode)) { - hasNullCheckWithoutTruthinessCheck = true; - } else if (isUndefinedIdentifier(testNode)) { - hasUndefinedCheckWithoutTruthinessCheck = true; - } else if ( - areNodesSimilarMemberAccess( - testNode, - getBranchNodes(node, operator).nonNullishBranch, - ) - ) { - // 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; - } - } + const isFixable = ((): boolean => { + if (hasTruthinessCheck) { + return isTruthinessCheckEligibleForPreferNullish({ + node, + testNode: nullishCoalescingLeftNode, + }); } - if (!nullishCoalescingLeftNode) { - return; + // it is fixable if we check for both null and undefined, or not if neither + if ( + hasUndefinedCheckWithoutTruthinessCheck === + hasNullCheckWithoutTruthinessCheck + ) { + return hasUndefinedCheckWithoutTruthinessCheck; } - const isFixableWithPreferNullishOverTernary = ((): boolean => { - // x ? x : y and !x ? y : x patterns - if (hasTruthinessCheck) { - return isTruthinessCheckEligibleForPreferNullish({ - node, - testNode: nullishCoalescingLeftNode, - }); - } + // it is fixable if we loosely check for either null or undefined + if (['==', '!='].includes(operator)) { + return true; + } - // it is fixable if we check for both null and undefined, or not if neither - if ( - hasUndefinedCheckWithoutTruthinessCheck === - hasNullCheckWithoutTruthinessCheck - ) { - return hasUndefinedCheckWithoutTruthinessCheck; - } + const type = parserServices.getTypeAtLocation( + nullishCoalescingLeftNode, + ); + const flags = getTypeFlags(type); - // it is fixable if we loosely check for either null or undefined - if (operator === '==' || operator === '!=') { - return true; - } + if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + return false; + } - const type = parserServices.getTypeAtLocation( - nullishCoalescingLeftNode, - ); - const flags = getTypeFlags(type); + const hasNullType = (flags & ts.TypeFlags.Null) !== 0; - if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { - return false; - } + // it is fixable if we check for undefined and the type is not nullable + if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) { + return true; + } - const hasNullType = (flags & ts.TypeFlags.Null) !== 0; + const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0; - // it is fixable if we check for undefined and the type is not nullable - if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) { - return true; - } + // it is fixable if we check for null and the type can't be undefined + return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType; + })(); + + return isFixable + ? { isFixable, nullishCoalescingLeftNode } + : { isFixable }; + } + + return { + 'AssignmentExpression[operator = "||="]'( + node: TSESTree.AssignmentExpression, + ): void { + checkAndFixWithPreferNullishOverOr(node, 'assignment', '='); + }, + ConditionalExpression(node: TSESTree.ConditionalExpression): void { + if (ignoreTernaryTests) { + return; + } + + const { nodesInsideTestExpression, operator } = + getOperatorAndNodesInsideTestExpression(node); - const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0; + if (operator == null) { + return; + } - // it is fixable if we check for null and the type can't be undefined - return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType; - })(); + const nullishCoalescingParams = getNullishCoalescingParams( + node, + getBranchNodes(node, operator).nonNullishBranch, + nodesInsideTestExpression, + operator, + ); - if (isFixableWithPreferNullishOverTernary) { + if (nullishCoalescingParams.isFixable) { context.report({ node, messageId: 'preferNullishOverTernary', @@ -517,7 +475,10 @@ export default createRule({ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { return fixer.replaceText( node, - `${getTextWithParentheses(context.sourceCode, nullishCoalescingLeftNode)} ?? ${getTextWithParentheses( + `${getTextWithParentheses( + context.sourceCode, + nullishCoalescingParams.nullishCoalescingLeftNode, + )} ?? ${getTextWithParentheses( context.sourceCode, getBranchNodes(node, operator).nullishBranch, )}`, @@ -528,6 +489,66 @@ export default createRule({ }); } }, + IfStatement(node: TSESTree.IfStatement): void { + if ( + node.alternate != null || + node.consequent.type !== AST_NODE_TYPES.BlockStatement || + node.consequent.body.length !== 1 || + node.consequent.body[0].type !== AST_NODE_TYPES.ExpressionStatement || + node.consequent.body[0].expression.type !== + AST_NODE_TYPES.AssignmentExpression || + (node.consequent.body[0].expression.left.type !== + AST_NODE_TYPES.Identifier && + node.consequent.body[0].expression.left.type !== + AST_NODE_TYPES.MemberExpression) + ) { + return; + } + + const assignmentLeftNode = node.consequent.body[0].expression.left; + const nullishCoalescingRightNode = + node.consequent.body[0].expression.right; + + const { nodesInsideTestExpression, operator } = + getOperatorAndNodesInsideTestExpression(node); + + if (operator == null || !['!', '==', '==='].includes(operator)) { + return; + } + + const nullishCoalescingParams = getNullishCoalescingParams( + node, + assignmentLeftNode, + nodesInsideTestExpression, + operator, + ); + + if (nullishCoalescingParams.isFixable) { + context.report({ + node, + messageId: 'preferNullishOverAssignment', + data: { equals: '=' }, + suggest: [ + { + messageId: 'suggestNullish', + data: { equals: '=' }, + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + return fixer.replaceText( + node, + `${getTextWithParentheses( + context.sourceCode, + assignmentLeftNode, + )} ??= ${getTextWithParentheses( + context.sourceCode, + nullishCoalescingRightNode, + )};`, + ); + }, + }, + ], + }); + } + }, 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { @@ -705,8 +726,85 @@ function getBranchNodes( nonNullishBranch: TSESTree.Expression; nullishBranch: TSESTree.Expression; } { - if (!operator || ['!=', '!=='].includes(operator)) { + if (['', '!=', '!=='].includes(operator)) { return { nonNullishBranch: node.consequent, nullishBranch: node.alternate }; } return { nonNullishBranch: node.alternate, nullishBranch: node.consequent }; } + +function getOperatorAndNodesInsideTestExpression( + node: TSESTree.ConditionalExpression | TSESTree.IfStatement, +): { + nodesInsideTestExpression: TSESTree.Node[]; + operator: NullishCheckOperator | undefined; +} { + let operator: NullishCheckOperator | undefined; + let nodesInsideTestExpression: TSESTree.Node[] = []; + if (node.test.type === AST_NODE_TYPES.BinaryExpression) { + nodesInsideTestExpression = [node.test.left, node.test.right]; + if ( + node.test.operator === '==' || + node.test.operator === '!=' || + node.test.operator === '===' || + node.test.operator === '!==' + ) { + operator = node.test.operator; + } + } else if ( + node.test.type === AST_NODE_TYPES.LogicalExpression && + node.test.left.type === AST_NODE_TYPES.BinaryExpression && + node.test.right.type === AST_NODE_TYPES.BinaryExpression + ) { + nodesInsideTestExpression = [ + node.test.left.left, + node.test.left.right, + node.test.right.left, + node.test.right.right, + ]; + if (['||', '||='].includes(node.test.operator)) { + if ( + node.test.left.operator === '===' && + node.test.right.operator === '===' + ) { + operator = '==='; + } else if ( + ((node.test.left.operator === '===' || + node.test.right.operator === '===') && + (node.test.left.operator === '==' || + node.test.right.operator === '==')) || + (node.test.left.operator === '==' && node.test.right.operator === '==') + ) { + operator = '=='; + } + } else if (node.test.operator === '&&') { + if ( + node.test.left.operator === '!==' && + node.test.right.operator === '!==' + ) { + operator = '!=='; + } else if ( + ((node.test.left.operator === '!==' || + node.test.right.operator === '!==') && + (node.test.left.operator === '!=' || + node.test.right.operator === '!=')) || + (node.test.left.operator === '!=' && node.test.right.operator === '!=') + ) { + operator = '!='; + } + } + } + + if (operator == null) { + if (isIdentifierOrMemberOrChainExpression(node.test)) { + operator = ''; + } else if ( + node.test.type === AST_NODE_TYPES.UnaryExpression && + isIdentifierOrMemberOrChainExpression(node.test.argument) && + node.test.operator === '!' + ) { + operator = '!'; + } + } + + return { nodesInsideTestExpression, operator }; +} diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot index 124ed091013c..46ff002cd623 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot @@ -2,6 +2,55 @@ exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 1`] = ` "Incorrect + +declare const a: string | null; +declare const b: string | null; + +const c = a || b; + ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. + +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitializeFooByTruthiness() { + if (!foo) { + ~~~~~~~~~~~ Prefer using nullish coalescing operator (\`??=\`) instead of an assignment expression, as it is simpler to read. + foo = makeFoo(); +~~~~~~~~~~~~~~~~~~~~ + } +~~~ +} + +function lazyInitializeFooByNullCheck() { + if (foo == null) { + ~~~~~~~~~~~~~~~~~~ Prefer using nullish coalescing operator (\`??=\`) instead of an assignment expression, as it is simpler to read. + foo = makeFoo(); +~~~~~~~~~~~~~~~~~~~~ + } +~~~ +} +" +`; + +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 2`] = ` +"Correct + +declare const a: string | null; +declare const b: string | null; + +const c = a ?? b; + +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitializeFoo() { + foo ??= makeFoo(); +} +" +`; + +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 3`] = ` +"Incorrect Options: { "ignoreTernaryTests": false } declare const a: any; @@ -36,7 +85,7 @@ c ? c : 'a string'; " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 2`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 4`] = ` "Correct Options: { "ignoreTernaryTests": false } @@ -51,21 +100,23 @@ c ?? 'a string'; " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 3`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 5`] = ` "Incorrect Options: { "ignoreConditionalTests": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a || b) { ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. } if ((a ||= b)) { + ~~~ Prefer using nullish coalescing operator (\`??=\`) instead of a logical assignment (\`||=\`), as it is a safer operator. } while (a || b) {} ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. while ((a ||= b)) {} + ~~~ Prefer using nullish coalescing operator (\`??=\`) instead of a logical assignment (\`||=\`), as it is a safer operator. do {} while (a || b); ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. for (let i = 0; a || b; i += 1) {} @@ -75,11 +126,11 @@ a || b ? true : false; " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 4`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 6`] = ` "Correct Options: { "ignoreConditionalTests": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a ?? b) { @@ -94,11 +145,11 @@ for (let i = 0; a ?? b; i += 1) {} " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 5`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 7`] = ` "Incorrect Options: { "ignoreMixedLogicalExpressions": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; @@ -106,6 +157,7 @@ declare const d: string | null; a || (b && c); ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. a ||= b && c; + ~~~ Prefer using nullish coalescing operator (\`??=\`) instead of a logical assignment (\`||=\`), as it is a safer operator. (a && b) || c || d; ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. @@ -117,11 +169,11 @@ a || (b && c && d); " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 6`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 8`] = ` "Correct Options: { "ignoreMixedLogicalExpressions": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; @@ -134,7 +186,7 @@ a ?? (b && c && d); " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 7`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 9`] = ` "Incorrect Options: { "ignorePrimitives": { "string": false } } @@ -145,7 +197,7 @@ foo || 'a string'; " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 8`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 10`] = ` "Correct Options: { "ignorePrimitives": { "string": false } } @@ -155,7 +207,7 @@ foo ?? 'a string'; " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 9`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 11`] = ` "Incorrect Options: { "ignoreBooleanCoercion": false } @@ -167,7 +219,7 @@ const x = Boolean(a || b); " `; -exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 10`] = ` +exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint output 12`] = ` "Correct Options: { "ignoreBooleanCoercion": 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 1aa35f847af4..409a7d9fd018 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -385,6 +385,47 @@ x.n ? x.n : y; declare let x: { n: () => string | null | undefined }; !x.n ? y : x.n; `, + ` +declare let foo: string; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (!foo) { + foo = makeFoo(); + } +} + `, + ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo) { + foo = makeFoo(); + } +} + `, + ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo != null) { + foo = makeFoo(); + } +} + `, + ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) { + foo = makeFoo(); + return foo; + } +} + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -5308,5 +5349,302 @@ defaultBoxOptional.a?.b ?? getFallbackBox(); options: [{ ignoreTernaryTests: false }], output: null, }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (!foo) { + foo = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) { + foo = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) { + foo ??= makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) { + foo ||= makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); +} + `, + }, + ], + }, + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) { + foo ??= makeFoo(); + } +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo === null) { + foo = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | undefined; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo === undefined) { + foo = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | undefined; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null | undefined; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo === undefined || foo === null) { + foo = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null | undefined; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): string; + +function lazyInitialize() { + if (foo.a == null) { + foo.a = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): string; + +function lazyInitialize() { + foo.a ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): string; + +function lazyInitialize() { + if (foo?.a == null) { + foo.a = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): string; + +function lazyInitialize() { + foo.a ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, ], }); diff --git a/packages/website/src/components/lib/createCompilerOptions.ts b/packages/website/src/components/lib/createCompilerOptions.ts index 9223920f981f..a11ed39a71b8 100644 --- a/packages/website/src/components/lib/createCompilerOptions.ts +++ b/packages/website/src/components/lib/createCompilerOptions.ts @@ -26,9 +26,7 @@ export function createCompilerOptions( const options = config.options; - if (!options.lib) { - options.lib = [window.ts.getDefaultLibFileName(options)]; - } + options.lib ??= [window.ts.getDefaultLibFileName(options)]; return options; } From 4e3a72310edd824abaa77de74e70a3dc95864caa Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:53:41 +0100 Subject: [PATCH 02/18] improve logic --- 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 7e1b5e5e0afc..a9c6a0cfb8a2 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -349,7 +349,7 @@ export default createRule({ let hasNullCheckWithoutTruthinessCheck = false; let hasUndefinedCheckWithoutTruthinessCheck = false; - if (['!', ''].includes(operator)) { + if (!nodesInsideTestExpression.length) { hasTruthinessCheck = true; nullishCoalescingLeftNode = node.test.type === AST_NODE_TYPES.UnaryExpression From 185667dedcbef1d76b22e3c5163f4bc42b7cc9a6 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:34:26 +0100 Subject: [PATCH 03/18] fix false positive --- .../src/rules/prefer-nullish-coalescing.ts | 14 +++++++ .../rules/prefer-nullish-coalescing.test.ts | 42 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index a9c6a0cfb8a2..a7e392dce1a4 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -26,6 +26,13 @@ const isIdentifierOrMemberOrChainExpression = isNodeOfTypes([ AST_NODE_TYPES.MemberExpression, ] as const); +const isNullLiteralOrUndefinedIdentifier = (node: TSESTree.Node): boolean => + isNullLiteral(node) || isUndefinedIdentifier(node); + +const isNodeNullishComparison = (node: TSESTree.BinaryExpression): boolean => + isNullLiteralOrUndefinedIdentifier(node.left) && + isNullLiteralOrUndefinedIdentifier(node.right); + type NullishCheckOperator = '!' | '!=' | '!==' | '' | '==' | '==='; export type Options = [ @@ -740,6 +747,7 @@ function getOperatorAndNodesInsideTestExpression( } { let operator: NullishCheckOperator | undefined; let nodesInsideTestExpression: TSESTree.Node[] = []; + if (node.test.type === AST_NODE_TYPES.BinaryExpression) { nodesInsideTestExpression = [node.test.left, node.test.right]; if ( @@ -755,6 +763,12 @@ function getOperatorAndNodesInsideTestExpression( node.test.left.type === AST_NODE_TYPES.BinaryExpression && node.test.right.type === AST_NODE_TYPES.BinaryExpression ) { + if ( + isNodeNullishComparison(node.test.left) || + isNodeNullishComparison(node.test.right) + ) { + return { nodesInsideTestExpression, operator }; + } nodesInsideTestExpression = [ node.test.left.left, node.test.left.right, 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 409a7d9fd018..db74e48e53f6 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -426,6 +426,48 @@ function lazyInitialize() { } } `, + ` +declare const nullOrObject: null | { a: string }; + +const test = nullOrObject !== undefined && null !== null + ? nullOrObject + : 42; + `, + ` +declare const nullOrObject: null | { a: string }; + +const test = nullOrObject !== undefined && null != null + ? nullOrObject + : 42; + `, + ` +declare const nullOrObject: null | { a: string }; + +const test = nullOrObject !== undefined && null != undefined + ? nullOrObject + : 42; + `, + ` +declare const nullOrObject: null | { a: string }; + +const test = nullOrObject === undefined || null === null + ? 42 + : nullOrObject; + `, + ` +declare const nullOrObject: null | { a: string }; + +const test = nullOrObject === undefined || null == null + ? 42 + : nullOrObject; + `, + ` +declare const nullOrObject: null | { a: string }; + +const test = nullOrObject === undefined || null == undefined + ? 42 + : nullOrObject; + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, From f69b588a1451a1d2cac58ba45248d06c4d2bf855 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 25 Feb 2025 23:37:36 +0100 Subject: [PATCH 04/18] handle computed expression (patch for existing issue) --- .../src/rules/prefer-nullish-coalescing.ts | 24 +++- .../rules/prefer-nullish-coalescing.test.ts | 112 ++++++++++++++++++ 2 files changed, 132 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 a7e392dce1a4..246086258ee7 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -704,10 +704,26 @@ function areNodesSimilarMemberAccess( a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression ) { - return ( - isNodeEqual(a.property, b.property) && - areNodesSimilarMemberAccess(a.object, b.object) - ); + if (!areNodesSimilarMemberAccess(a.object, b.object)) { + return false; + } + + if (a.computed === b.computed) { + return isNodeEqual(a.property, b.property); + } + if ( + a.property.type === AST_NODE_TYPES.Literal && + b.property.type === AST_NODE_TYPES.Identifier + ) { + return a.property.value === b.property.name; + } + if ( + a.property.type === AST_NODE_TYPES.Identifier && + b.property.type === AST_NODE_TYPES.Literal + ) { + return a.property.name === b.property.value; + } + return false; } if ( a.type === AST_NODE_TYPES.ChainExpression || 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 db74e48e53f6..c66bc6ba5f60 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -468,6 +468,24 @@ const test = nullOrObject === undefined || null == undefined ? 42 : nullOrObject; `, + ` +const a = 'b'; +declare let x: { a: string, b: string } | null + +x?.a != null ? x[a] : 'foo' + `, + ` +const a = 'b'; +declare let x: { a: string, b: string } | null + +x?.[a] != null ? x.a : 'foo' + `, + ` +declare let x: { a: string } | null +declare let y: { a: string } | null + +x?.a ? y?.a : 'foo' + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -5393,6 +5411,100 @@ defaultBoxOptional.a?.b ?? getFallbackBox(); }, { code: ` +declare let x: { a: string } | null; + +x?.['a'] != null ? x['a'] : 'foo'; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: { a: string } | null; + +x?.['a'] ?? 'foo'; + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let x: { a: string } | null; + +x?.['a'] != null ? x.a : 'foo'; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: { a: string } | null; + +x?.['a'] ?? 'foo'; + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let x: { a: string } | null; + +x?.a != null ? x['a'] : 'foo'; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: { a: string } | null; + +x?.a ?? 'foo'; + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +const a = 'b'; +declare let x: { a: string; b: string } | null; + +x?.[a] != null ? x[a] : 'foo'; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +const a = 'b'; +declare let x: { a: string; b: string } | null; + +x?.[a] ?? 'foo'; + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` declare let foo: { a: string } | null; declare function makeFoo(): { a: string }; From ed7be4cb8865f0a980fac770ccd17d608ccf965e Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:47:23 +0100 Subject: [PATCH 05/18] add tests --- .../rules/prefer-nullish-coalescing.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 c66bc6ba5f60..6dde2a0474ce 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -274,6 +274,24 @@ declare let x: () => string | null | undefined; !x ? y : x; `, ` +declare let x: () => string | null; +x() ? x() : y; + `, + ` +declare let x: () => string | null; +!x() ? y : x(); + `, + ` +const a = 'foo'; +declare let x: (a: string | null) => string | null; +x(a) ? x(a) : y; + `, + ` +const a = 'foo'; +declare let x: (a: string | null) => string | null; +!x(a) ? y : x(a); + `, + ` declare let x: { n: string }; x.n ? x.n : y; `, From 2db7af7e49cecc6671d3561390350c093cb9b1dc Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Fri, 28 Feb 2025 07:52:09 +0100 Subject: [PATCH 06/18] simplify and add test --- .../src/rules/prefer-nullish-coalescing.ts | 37 ++++++++++++------- .../rules/prefer-nullish-coalescing.test.ts | 12 ++++++ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 246086258ee7..c2cc7e9d856b 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -764,7 +764,12 @@ function getOperatorAndNodesInsideTestExpression( let operator: NullishCheckOperator | undefined; let nodesInsideTestExpression: TSESTree.Node[] = []; - if (node.test.type === AST_NODE_TYPES.BinaryExpression) { + if ( + isIdentifierOrMemberOrChainExpression(node.test) || + node.test.type === AST_NODE_TYPES.UnaryExpression + ) { + operator = getNonBinaryNodeOperator(node.test); + } else if (node.test.type === AST_NODE_TYPES.BinaryExpression) { nodesInsideTestExpression = [node.test.left, node.test.right]; if ( node.test.operator === '==' || @@ -824,17 +829,23 @@ function getOperatorAndNodesInsideTestExpression( } } - if (operator == null) { - if (isIdentifierOrMemberOrChainExpression(node.test)) { - operator = ''; - } else if ( - node.test.type === AST_NODE_TYPES.UnaryExpression && - isIdentifierOrMemberOrChainExpression(node.test.argument) && - node.test.operator === '!' - ) { - operator = '!'; - } - } - return { nodesInsideTestExpression, operator }; } + +function getNonBinaryNodeOperator( + node: + | TSESTree.ChainExpression + | TSESTree.Identifier + | TSESTree.MemberExpression + | TSESTree.UnaryExpression, +): NullishCheckOperator | undefined { + if (node.type !== AST_NODE_TYPES.UnaryExpression) { + return ''; + } + if ( + isIdentifierOrMemberOrChainExpression(node.argument) && + node.operator === '!' + ) { + return '!'; + } +} 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 6dde2a0474ce..d913ad97b2c3 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -445,6 +445,18 @@ function lazyInitialize() { } `, ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) { + return foo; + } else { + return 'bar'; + } +} + `, + ` declare const nullOrObject: null | { a: string }; const test = nullOrObject !== undefined && null !== null From fc478dcecd94c80362f4bbad04c0ebf8d254f9f9 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Fri, 28 Feb 2025 08:08:04 +0100 Subject: [PATCH 07/18] fix error --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 7 ++++--- 1 file changed, 4 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 c2cc7e9d856b..87e810ec2c16 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -759,9 +759,9 @@ function getOperatorAndNodesInsideTestExpression( node: TSESTree.ConditionalExpression | TSESTree.IfStatement, ): { nodesInsideTestExpression: TSESTree.Node[]; - operator: NullishCheckOperator | undefined; + operator: NullishCheckOperator | null; } { - let operator: NullishCheckOperator | undefined; + let operator: NullishCheckOperator | null = null; let nodesInsideTestExpression: TSESTree.Node[] = []; if ( @@ -838,7 +838,7 @@ function getNonBinaryNodeOperator( | TSESTree.Identifier | TSESTree.MemberExpression | TSESTree.UnaryExpression, -): NullishCheckOperator | undefined { +): NullishCheckOperator | null { if (node.type !== AST_NODE_TYPES.UnaryExpression) { return ''; } @@ -848,4 +848,5 @@ function getNonBinaryNodeOperator( ) { return '!'; } + return null; } From 5e594686568df450c57b58fb0bcb6d3d14cf0563 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:18:30 +0100 Subject: [PATCH 08/18] add "IfStatement without curly brackets" use case --- .../src/rules/prefer-nullish-coalescing.ts | 40 +++++---- .../rules/prefer-nullish-coalescing.test.ts | 84 +++++++++++++++++++ 2 files changed, 109 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 87e810ec2c16..b575c44cf1df 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -497,24 +497,34 @@ export default createRule({ } }, IfStatement(node: TSESTree.IfStatement): void { + if (node.alternate != null) { + return; + } + + let assignmentExpression: TSESTree.Expression | undefined; if ( - node.alternate != null || - node.consequent.type !== AST_NODE_TYPES.BlockStatement || - node.consequent.body.length !== 1 || - node.consequent.body[0].type !== AST_NODE_TYPES.ExpressionStatement || - node.consequent.body[0].expression.type !== - AST_NODE_TYPES.AssignmentExpression || - (node.consequent.body[0].expression.left.type !== - AST_NODE_TYPES.Identifier && - node.consequent.body[0].expression.left.type !== - AST_NODE_TYPES.MemberExpression) + node.consequent.type === AST_NODE_TYPES.BlockStatement && + node.consequent.body.length === 1 && + node.consequent.body[0].type === AST_NODE_TYPES.ExpressionStatement + ) { + assignmentExpression = node.consequent.body[0].expression; + } else if ( + node.consequent.type === AST_NODE_TYPES.ExpressionStatement + ) { + assignmentExpression = node.consequent.expression; + } + + if ( + !assignmentExpression || + assignmentExpression.type !== AST_NODE_TYPES.AssignmentExpression || + (assignmentExpression.left.type !== AST_NODE_TYPES.Identifier && + assignmentExpression.left.type !== AST_NODE_TYPES.MemberExpression) ) { return; } - const assignmentLeftNode = node.consequent.body[0].expression.left; - const nullishCoalescingRightNode = - node.consequent.body[0].expression.right; + const nullishCoalescingLeftNode = assignmentExpression.left; + const nullishCoalescingRightNode = assignmentExpression.right; const { nodesInsideTestExpression, operator } = getOperatorAndNodesInsideTestExpression(node); @@ -525,7 +535,7 @@ export default createRule({ const nullishCoalescingParams = getNullishCoalescingParams( node, - assignmentLeftNode, + nullishCoalescingLeftNode, nodesInsideTestExpression, operator, ); @@ -544,7 +554,7 @@ export default createRule({ node, `${getTextWithParentheses( context.sourceCode, - assignmentLeftNode, + nullishCoalescingLeftNode, )} ??= ${getTextWithParentheses( context.sourceCode, nullishCoalescingRightNode, 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 d913ad97b2c3..a985ed9c04dc 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -5708,6 +5708,90 @@ function lazyInitialize() { }, { code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) foo = makeFoo(); + const bar = 42; + return bar; +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); + const bar = 42; + return bar; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) foo ||= makeFoo(); + const bar = 42; + return bar; +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); + const bar = 42; + return bar; +} + `, + }, + ], + }, + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) foo ??= makeFoo(); + const bar = 42; + return bar; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` declare let foo: { a: string } | undefined; declare function makeFoo(): { a: string }; From 8e1a6a682658f2f3c7fe7e0cd3de0cef450f0a96 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:48:04 +0100 Subject: [PATCH 09/18] add test --- .../rules/prefer-nullish-coalescing.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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 a985ed9c04dc..09fe00bfd5cc 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -5744,6 +5744,39 @@ function lazyInitialize() { declare let foo: { a: string } | null; declare function makeFoo(): { a: string }; +function lazyInitialize() { + if (foo == null) foo ??= makeFoo(); + const bar = 42; + return bar; +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + +function lazyInitialize() { + foo ??= makeFoo(); + const bar = 42; + return bar; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; + function lazyInitialize() { if (foo == null) foo ||= makeFoo(); const bar = 42; From 168a228d6ede116f987eb010cac53d4274800183 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 19 Mar 2025 09:34:39 +0100 Subject: [PATCH 10/18] after review --- .../docs/rules/prefer-nullish-coalescing.mdx | 1 + .../src/rules/prefer-nullish-coalescing.ts | 16 ++--- .../rules/prefer-nullish-coalescing.test.ts | 67 ++++++++++++++++++- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx index d162eba17933..904c7f40b7fe 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx @@ -303,3 +303,4 @@ If you are not using TypeScript 3.7 (or greater), then you will not be able to u - [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html) - [Nullish Coalescing Operator Proposal](https://github.com/tc39/proposal-nullish-coalescing/) +- [`logical-assignment-operators`](https://eslint.org/docs/latest/rules/logical-assignment-operators) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index df231a4e93df..0e8846b79065 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -20,7 +20,7 @@ import { skipChainExpression, } from '../util'; -const isIdentifierOrMemberOrChainExpression = isNodeOfTypes([ +const isMemberAccessLike = isNodeOfTypes([ AST_NODE_TYPES.ChainExpression, AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression, @@ -459,8 +459,8 @@ export default createRule({ })(); return isFixable - ? { isFixable, nullishCoalescingLeftNode } - : { isFixable }; + ? { isFixable: true, nullishCoalescingLeftNode } + : { isFixable: false }; } return { @@ -536,8 +536,7 @@ export default createRule({ if ( !assignmentExpression || assignmentExpression.type !== AST_NODE_TYPES.AssignmentExpression || - (assignmentExpression.left.type !== AST_NODE_TYPES.Identifier && - assignmentExpression.left.type !== AST_NODE_TYPES.MemberExpression) + !isMemberAccessLike(assignmentExpression.left) ) { return; } @@ -794,7 +793,7 @@ function getOperatorAndNodesInsideTestExpression( let nodesInsideTestExpression: TSESTree.Node[] = []; if ( - isIdentifierOrMemberOrChainExpression(node.test) || + isMemberAccessLike(node.test) || node.test.type === AST_NODE_TYPES.UnaryExpression ) { operator = getNonBinaryNodeOperator(node.test); @@ -871,10 +870,7 @@ function getNonBinaryNodeOperator( if (node.type !== AST_NODE_TYPES.UnaryExpression) { return ''; } - if ( - isIdentifierOrMemberOrChainExpression(node.argument) && - node.operator === '!' - ) { + if (isMemberAccessLike(node.argument) && node.operator === '!') { return '!'; } return null; 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 1b4c6ef42b85..f125fd015558 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -373,7 +373,7 @@ declare let x: { n: () => string | null | undefined }; `, ` declare let foo: string; -declare function makeFoo(): { a: string }; +declare function makeFoo(): string; function lazyInitialize() { if (!foo) { @@ -425,6 +425,40 @@ function lazyInitialize() { } `, ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; +function shadowed() { + if (foo == null) { + const foo = makeFoo(); + } +} +declare let foo: { foo: string } | null; +declare function makeFoo(): { foo: { foo: string } }; +function weirdDestructuringAssignment() { + if (foo == null) { + ({ foo } = makeFoo()); + } +} + `, + ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; +function shadowed() { + if (foo == null) { + const foo = makeFoo(); + } +} + `, + ` +declare let foo: { foo: string } | null; +declare function makeFoo(): { foo: { foo: string } }; +function weirdDestructuringAssignment() { + if (foo == null) { + ({ foo } = makeFoo()); + } +} + `, + ` declare const nullOrObject: null | { a: string }; const test = nullOrObject !== undefined && null !== null @@ -6034,6 +6068,37 @@ declare function makeFoo(): string; function lazyInitialize() { foo.a ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: { a: string | null }; +declare function makeString(): string; + +function weirdParens() { + if (((((foo.a)) == null))) { + ((((((((foo).a))))) = makeString())); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: { a: string | null }; +declare function makeString(): string; + +function weirdParens() { + ((foo).a) ??= makeString(); } `, }, From 4440b11f5aac792d49d026a2ddd80700734448b6 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 19 Mar 2025 09:47:35 +0100 Subject: [PATCH 11/18] forgot the `noFormat` --- .../tests/rules/prefer-nullish-coalescing.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 f125fd015558..e6f9a9414bf7 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -3,7 +3,7 @@ import type { ValidTestCase, } from '@typescript-eslint/rule-tester'; -import { RuleTester } from '@typescript-eslint/rule-tester'; +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import * as path from 'node:path'; import type { @@ -6077,7 +6077,7 @@ function lazyInitialize() { output: null, }, { - code: ` + code: noFormat` declare let foo: { a: string | null }; declare function makeString(): string; From 50bf59e2176a4dfd87c8d6dcc1b5641035e79a2e Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:08:08 +0100 Subject: [PATCH 12/18] fix test --- .../tests/rules/prefer-nullish-coalescing.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 e6f9a9414bf7..c8dd5bda7d4f 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -418,7 +418,7 @@ declare function makeFoo(): { a: string }; function lazyInitialize() { if (foo == null) { - return foo; + foo = makeFoo(); } else { return 'bar'; } @@ -427,6 +427,18 @@ function lazyInitialize() { ` declare let foo: { a: string } | null; declare function makeFoo(): { a: string }; + +function lazyInitialize() { + if (foo == null) { + foo = makeFoo(); + } else if (foo.a) { + return 'bar'; + } +} + `, + ` +declare let foo: { a: string } | null; +declare function makeFoo(): { a: string }; function shadowed() { if (foo == null) { const foo = makeFoo(); From 92ef70121d544373cb193b37a7690407da2cb6cc Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:08:36 +0100 Subject: [PATCH 13/18] Handle comments --- .../src/rules/prefer-nullish-coalescing.ts | 34 ++++++- .../rules/prefer-nullish-coalescing.test.ts | 89 +++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 0e8846b79065..68fd78c32d3f 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -559,6 +559,23 @@ export default createRule({ ); if (nullishCoalescingParams.isFixable) { + // Handle comments + const isConsequenceNodeBlockStatement = + node.consequent.type === AST_NODE_TYPES.BlockStatement; + + const commentsBefore = formatComments( + context.sourceCode.getCommentsBefore(assignmentExpression), + isConsequenceNodeBlockStatement ? '\n' : ' ', + ); + const commentsAfter = isConsequenceNodeBlockStatement + ? formatComments( + context.sourceCode.getCommentsAfter( + assignmentExpression.parent, + ), + '\n', + ) + : ''; + context.report({ node, messageId: 'preferNullishOverAssignment', @@ -570,13 +587,13 @@ export default createRule({ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { return fixer.replaceText( node, - `${getTextWithParentheses( + `${commentsBefore}${getTextWithParentheses( context.sourceCode, nullishCoalescingLeftNode, )} ??= ${getTextWithParentheses( context.sourceCode, nullishCoalescingRightNode, - )};`, + )};${commentsAfter}`, ); }, }, @@ -875,3 +892,16 @@ function getNonBinaryNodeOperator( } return null; } + +function formatComments( + comments: TSESTree.Comment[], + separator: string, +): string { + return comments + .map(({ type, value }) => + type === AST_TOKEN_TYPES.Line + ? `//${value}${separator}` + : `/*${value}*/${separator}`, + ) + .join(''); +} 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 c8dd5bda7d4f..dc480c73333e 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -6088,6 +6088,95 @@ function lazyInitialize() { ], output: null, }, + { + code: ` +declare let foo: string | null; +declare function makeFoo(): string; + +if (foo == null) { + // comment before 1 + /* comment before 2 */ + /* comment before 3 + which is multiline + */ + /** + * comment before 4 + * which is also multiline + */ + foo = makeFoo(); // comment inline + // comment after 1 + /* comment after 2 */ + /* comment after 3 + which is multiline + */ + /** + * comment after 4 + * which is also multiline + */ +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: string | null; +declare function makeFoo(): string; + +// comment before 1 +/* comment before 2 */ +/* comment before 3 + which is multiline + */ +/** + * comment before 4 + * which is also multiline + */ +foo ??= makeFoo();// comment inline +// comment after 1 +/* comment after 2 */ +/* comment after 3 + which is multiline + */ +/** + * comment after 4 + * which is also multiline + */ + + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: string | null; +declare function makeFoo(): string; + +if (foo == null) /* comment before 1 */ /* comment before 2 */ foo = makeFoo(); // comment inline + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: string | null; +declare function makeFoo(): string; + +/* comment before 1 */ /* comment before 2 */ foo ??= makeFoo(); // comment inline + `, + }, + ], + }, + ], + output: null, + }, { code: noFormat` declare let foo: { a: string | null }; From 727109a05fd0d1d53e5a700f55115b893c0829ca Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:18:19 +0100 Subject: [PATCH 14/18] fix naming and add test --- .../src/rules/prefer-nullish-coalescing.ts | 6 ++-- .../rules/prefer-nullish-coalescing.test.ts | 33 +++++++++++++++++++ 2 files changed, 36 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 68fd78c32d3f..1d88943dc61c 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -560,14 +560,14 @@ export default createRule({ if (nullishCoalescingParams.isFixable) { // Handle comments - const isConsequenceNodeBlockStatement = + const isConsequentNodeBlockStatement = node.consequent.type === AST_NODE_TYPES.BlockStatement; const commentsBefore = formatComments( context.sourceCode.getCommentsBefore(assignmentExpression), - isConsequenceNodeBlockStatement ? '\n' : ' ', + isConsequentNodeBlockStatement ? '\n' : ' ', ); - const commentsAfter = isConsequenceNodeBlockStatement + const commentsAfter = isConsequentNodeBlockStatement ? formatComments( context.sourceCode.getCommentsAfter( assignmentExpression.parent, 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 dc480c73333e..d2a2ee7f624a 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -6093,6 +6093,39 @@ function lazyInitialize() { declare let foo: string | null; declare function makeFoo(): string; +function lazyInitialize() { + if (foo == null) { + // comment + foo = makeFoo(); + } +} + `, + errors: [ + { + messageId: 'preferNullishOverAssignment', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let foo: string | null; +declare function makeFoo(): string; + +function lazyInitialize() { + // comment +foo ??= makeFoo(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +declare let foo: string | null; +declare function makeFoo(): string; + if (foo == null) { // comment before 1 /* comment before 2 */ From 7035591feb2c8392edeacc670bbbad50d811fe44 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Thu, 20 Mar 2025 01:09:58 +0100 Subject: [PATCH 15/18] improve indentation --- .../src/rules/prefer-nullish-coalescing.ts | 29 ++++++++++++------- .../rules/prefer-nullish-coalescing.test.ts | 3 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 1d88943dc61c..72b0d00a22aa 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -585,16 +585,25 @@ export default createRule({ messageId: 'suggestNullish', data: { equals: '=' }, fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { - return fixer.replaceText( - node, - `${commentsBefore}${getTextWithParentheses( - context.sourceCode, - nullishCoalescingLeftNode, - )} ??= ${getTextWithParentheses( - context.sourceCode, - nullishCoalescingRightNode, - )};${commentsAfter}`, - ); + return [ + fixer.insertTextBefore(node, commentsBefore), + fixer.replaceText( + node, + `${getTextWithParentheses( + context.sourceCode, + nullishCoalescingLeftNode, + )} ??= ${getTextWithParentheses( + context.sourceCode, + nullishCoalescingRightNode, + )};`, + ), + fixer.insertTextAfter( + node, + `${ + commentsAfter.startsWith('/') ? ' ' : '' + }${commentsAfter.slice(0, -1)}`, + ), + ]; }, }, ], 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 d2a2ee7f624a..324493b6205c 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -6167,7 +6167,7 @@ declare function makeFoo(): string; * comment before 4 * which is also multiline */ -foo ??= makeFoo();// comment inline +foo ??= makeFoo(); // comment inline // comment after 1 /* comment after 2 */ /* comment after 3 @@ -6177,7 +6177,6 @@ foo ??= makeFoo();// comment inline * comment after 4 * which is also multiline */ - `, }, ], From fc806c558176306dccb88d661c6dfbd76e5bac8d Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Thu, 20 Mar 2025 01:21:18 +0100 Subject: [PATCH 16/18] fix type --- 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 72b0d00a22aa..6bfe04262185 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -584,7 +584,7 @@ export default createRule({ { messageId: 'suggestNullish', data: { equals: '=' }, - fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] { return [ fixer.insertTextBefore(node, commentsBefore), fixer.replaceText( From 885855910bbdfed0d8048f169a87c53e3a169089 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Thu, 20 Mar 2025 23:00:01 +0100 Subject: [PATCH 17/18] simplify --- .../src/rules/prefer-nullish-coalescing.ts | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 6bfe04262185..8f49f74089a8 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -585,8 +585,13 @@ export default createRule({ messageId: 'suggestNullish', data: { equals: '=' }, fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] { - return [ - fixer.insertTextBefore(node, commentsBefore), + const fixes: TSESLint.RuleFix[] = []; + + if (commentsBefore) { + fixes.push(fixer.insertTextBefore(node, commentsBefore)); + } + + fixes.push( fixer.replaceText( node, `${getTextWithParentheses( @@ -597,13 +602,18 @@ export default createRule({ nullishCoalescingRightNode, )};`, ), - fixer.insertTextAfter( - node, - `${ - commentsAfter.startsWith('/') ? ' ' : '' - }${commentsAfter.slice(0, -1)}`, - ), - ]; + ); + + if (commentsAfter) { + fixes.push( + fixer.insertTextAfter( + node, + ` ${commentsAfter.slice(0, -1)}`, + ), + ); + } + + return fixes; }, }, ], From 271db298f72bbe9016864822e5847589096011e5 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Sat, 22 Mar 2025 12:40:55 -0600 Subject: [PATCH 18/18] remove duplicated test case --- .../rules/prefer-nullish-coalescing.test.ts | 16 ---------------- 1 file changed, 16 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 324493b6205c..b17d4753f074 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -439,22 +439,6 @@ function lazyInitialize() { ` declare let foo: { a: string } | null; declare function makeFoo(): { a: string }; -function shadowed() { - if (foo == null) { - const foo = makeFoo(); - } -} -declare let foo: { foo: string } | null; -declare function makeFoo(): { foo: { foo: string } }; -function weirdDestructuringAssignment() { - if (foo == null) { - ({ foo } = makeFoo()); - } -} - `, - ` -declare let foo: { a: string } | null; -declare function makeFoo(): { a: string }; function shadowed() { if (foo == null) { const foo = makeFoo();