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();