diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx
index 438cc97d6af6..904c7f40b7fe 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.
@@ -255,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/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 caa193b36bdd..8f49f74089a8 100644
--- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts
+++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts
@@ -20,13 +20,20 @@ import {
skipChainExpression,
} from '../util';
-const isIdentifierOrMemberOrChainExpression = isNodeOfTypes([
+const isMemberAccessLike = isNodeOfTypes([
AST_NODE_TYPES.ChainExpression,
AST_NODE_TYPES.Identifier,
AST_NODE_TYPES.MemberExpression,
] as const);
-type NullishCheckOperator = '!' | '!=' | '!==' | '==' | '===' | undefined;
+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 = [
{
@@ -48,6 +55,7 @@ export type Options = [
export type MessageIds =
| 'noStrictNullCheck'
+ | 'preferNullishOverAssignment'
| 'preferNullishOverOr'
| 'preferNullishOverTernary'
| 'suggestNullish';
@@ -66,6 +74,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:
@@ -263,6 +273,7 @@ export default createRule({
node:
| TSESTree.AssignmentExpression
| TSESTree.ConditionalExpression
+ | TSESTree.IfStatement
| TSESTree.LogicalExpression;
testNode: TSESTree.Node;
}): boolean {
@@ -351,179 +362,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 (!nodesInsideTestExpression.length) {
+ 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: true, nullishCoalescingLeftNode }
+ : { isFixable: false };
+ }
+
+ return {
+ 'AssignmentExpression[operator = "||="]'(
+ node: TSESTree.AssignmentExpression,
+ ): void {
+ checkAndFixWithPreferNullishOverOr(node, 'assignment', '=');
+ },
+ ConditionalExpression(node: TSESTree.ConditionalExpression): void {
+ if (ignoreTernaryTests) {
+ return;
+ }
- const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0;
+ const { nodesInsideTestExpression, operator } =
+ getOperatorAndNodesInsideTestExpression(node);
- // it is fixable if we check for null and the type can't be undefined
- return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType;
- })();
+ if (operator == null) {
+ return;
+ }
+
+ const nullishCoalescingParams = getNullishCoalescingParams(
+ node,
+ getBranchNodes(node, operator).nonNullishBranch,
+ nodesInsideTestExpression,
+ operator,
+ );
- if (isFixableWithPreferNullishOverTernary) {
+ if (nullishCoalescingParams.isFixable) {
context.report({
node,
messageId: 'preferNullishOverTernary',
@@ -536,7 +501,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,
)}`,
@@ -547,6 +515,111 @@ export default createRule({
});
}
},
+ IfStatement(node: TSESTree.IfStatement): void {
+ if (node.alternate != null) {
+ return;
+ }
+
+ let assignmentExpression: TSESTree.Expression | undefined;
+ if (
+ 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 ||
+ !isMemberAccessLike(assignmentExpression.left)
+ ) {
+ return;
+ }
+
+ const nullishCoalescingLeftNode = assignmentExpression.left;
+ const nullishCoalescingRightNode = assignmentExpression.right;
+
+ const { nodesInsideTestExpression, operator } =
+ getOperatorAndNodesInsideTestExpression(node);
+
+ if (operator == null || !['!', '==', '==='].includes(operator)) {
+ return;
+ }
+
+ const nullishCoalescingParams = getNullishCoalescingParams(
+ node,
+ nullishCoalescingLeftNode,
+ nodesInsideTestExpression,
+ operator,
+ );
+
+ if (nullishCoalescingParams.isFixable) {
+ // Handle comments
+ const isConsequentNodeBlockStatement =
+ node.consequent.type === AST_NODE_TYPES.BlockStatement;
+
+ const commentsBefore = formatComments(
+ context.sourceCode.getCommentsBefore(assignmentExpression),
+ isConsequentNodeBlockStatement ? '\n' : ' ',
+ );
+ const commentsAfter = isConsequentNodeBlockStatement
+ ? formatComments(
+ context.sourceCode.getCommentsAfter(
+ assignmentExpression.parent,
+ ),
+ '\n',
+ )
+ : '';
+
+ context.report({
+ node,
+ messageId: 'preferNullishOverAssignment',
+ data: { equals: '=' },
+ suggest: [
+ {
+ messageId: 'suggestNullish',
+ data: { equals: '=' },
+ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] {
+ const fixes: TSESLint.RuleFix[] = [];
+
+ if (commentsBefore) {
+ fixes.push(fixer.insertTextBefore(node, commentsBefore));
+ }
+
+ fixes.push(
+ fixer.replaceText(
+ node,
+ `${getTextWithParentheses(
+ context.sourceCode,
+ nullishCoalescingLeftNode,
+ )} ??= ${getTextWithParentheses(
+ context.sourceCode,
+ nullishCoalescingRightNode,
+ )};`,
+ ),
+ );
+
+ if (commentsAfter) {
+ fixes.push(
+ fixer.insertTextAfter(
+ node,
+ ` ${commentsAfter.slice(0, -1)}`,
+ ),
+ );
+ }
+
+ return fixes;
+ },
+ },
+ ],
+ });
+ }
+ },
'LogicalExpression[operator = "||"]'(
node: TSESTree.LogicalExpression,
): void {
@@ -695,10 +768,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 ||
@@ -724,8 +813,114 @@ 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 | null;
+} {
+ let operator: NullishCheckOperator | null = null;
+ let nodesInsideTestExpression: TSESTree.Node[] = [];
+
+ if (
+ isMemberAccessLike(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 === '==' ||
+ 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
+ ) {
+ if (
+ isNodeNullishComparison(node.test.left) ||
+ isNodeNullishComparison(node.test.right)
+ ) {
+ return { nodesInsideTestExpression, operator };
+ }
+ 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 { nodesInsideTestExpression, operator };
+}
+
+function getNonBinaryNodeOperator(
+ node:
+ | TSESTree.ChainExpression
+ | TSESTree.Identifier
+ | TSESTree.MemberExpression
+ | TSESTree.UnaryExpression,
+): NullishCheckOperator | null {
+ if (node.type !== AST_NODE_TYPES.UnaryExpression) {
+ return '';
+ }
+ if (isMemberAccessLike(node.argument) && node.operator === '!') {
+ return '!';
+ }
+ 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/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot
index babba8f3ad55..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,7 +100,7 @@ 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 }
@@ -77,7 +126,7 @@ 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 }
@@ -96,7 +145,7 @@ 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 }
@@ -120,7 +169,7 @@ 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 }
@@ -137,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 } }
@@ -148,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 } }
@@ -158,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 }
@@ -170,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 a57db7e341be..b17d4753f074 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 {
@@ -258,6 +258,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;
`,
@@ -353,6 +371,149 @@ x.n ? x.n : y;
declare let x: { n: () => string | null | undefined };
!x.n ? y : x.n;
`,
+ `
+declare let foo: string;
+declare function makeFoo(): 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;
+ }
+}
+ `,
+ `
+declare let foo: { a: string } | null;
+declare function makeFoo(): { a: string };
+
+function lazyInitialize() {
+ if (foo == null) {
+ foo = makeFoo();
+ } else {
+ return 'bar';
+ }
+}
+ `,
+ `
+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();
+ }
+}
+ `,
+ `
+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
+ ? 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;
+ `,
+ `
+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,
@@ -5403,5 +5564,665 @@ x.n ?? y;
},
],
},
+ {
+ 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 };
+
+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 } | 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;
+}
+ `,
+ },
+ ],
+ },
+ ],
+ 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 };
+
+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,
+ },
+ {
+ code: `
+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 */
+ /* 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 };
+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();
+}
+ `,
+ },
+ ],
+ },
+ ],
+ 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;
}