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; }