diff --git a/.cspell.json b/.cspell.json index dd584625aa33..ec67987a3b5a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -141,6 +141,7 @@ "noninteractive", "Nrwl", "nullish", + "nullishness", "nx", "nx's", "onboarded", @@ -167,11 +168,11 @@ "redeclared", "reimplement", "resync", - "ronami", - "Ronen", "Ribaudo", "ROADMAP", "Romain", + "ronami", + "Ronen", "Rosenwasser", "ruleset", "rulesets", diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx index d5742e1a8730..be44bfe11d88 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx @@ -16,6 +16,7 @@ This rule reports when you may consider replacing: - An `||` operator with `??` - An `||=` operator with `??=` +- Ternary expressions (`?:`) that are equivalent to `||` or `??` with `??` :::caution This rule will not work as expected if [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) is not enabled. @@ -42,7 +43,9 @@ foo === undefined ? 'a string' : foo; const foo: string | null = 'bar'; foo !== null ? foo : 'a string'; +foo ? foo : 'a string'; foo === null ? 'a string' : foo; +!foo ? 'a string' : foo; ``` Correct code for `ignoreTernaryTests: false`: @@ -61,6 +64,8 @@ foo ?? 'a string'; const foo: string | null = 'bar'; foo ?? 'a string'; foo ?? 'a string'; +foo ?? 'a string'; +foo ?? 'a string'; ``` ### `ignoreConditionalTests` diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 1c9c76bfbacf..9cd756c8204f 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -11,9 +11,14 @@ import { getParserServices, getTypeName, getTypeOfPropertyOfName, + getValueOfLiteralType, + isAlwaysNullish, isArrayMethodCallWithPredicate, isIdentifier, isNullableType, + isPossiblyFalsy, + isPossiblyNullish, + isPossiblyTruthy, isTypeAnyType, isTypeFlagSet, isTypeUnknownType, @@ -25,59 +30,7 @@ import { findTypeGuardAssertedArgument, } from '../util/assertionFunctionUtils'; -// Truthiness utilities // #region -const valueIsPseudoBigInt = ( - value: number | string | ts.PseudoBigInt, -): value is ts.PseudoBigInt => { - return typeof value === 'object'; -}; - -const getValueOfLiteralType = ( - type: ts.LiteralType, -): bigint | number | string => { - if (valueIsPseudoBigInt(type.value)) { - return pseudoBigIntToBigInt(type.value); - } - return type.value; -}; - -const isTruthyLiteral = (type: ts.Type): boolean => - tsutils.isTrueLiteralType(type) || - (type.isLiteral() && !!getValueOfLiteralType(type)); - -const isPossiblyFalsy = (type: ts.Type): boolean => - tsutils - .unionTypeParts(type) - // Intersections like `string & {}` can also be possibly falsy, - // requiring us to look into the intersection. - .flatMap(type => tsutils.intersectionTypeParts(type)) - // PossiblyFalsy flag includes literal values, so exclude ones that - // are definitely truthy - .filter(t => !isTruthyLiteral(t)) - .some(type => isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy)); - -const isPossiblyTruthy = (type: ts.Type): boolean => - tsutils - .unionTypeParts(type) - .map(type => tsutils.intersectionTypeParts(type)) - .some(intersectionParts => - // It is possible to define intersections that are always falsy, - // like `"" & { __brand: string }`. - intersectionParts.every(type => !tsutils.isFalsyType(type)), - ); - -// Nullish utilities -const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; -const isNullishType = (type: ts.Type): boolean => - isTypeFlagSet(type, nullishFlag); - -const isPossiblyNullish = (type: ts.Type): boolean => - tsutils.unionTypeParts(type).some(isNullishType); - -const isAlwaysNullish = (type: ts.Type): boolean => - tsutils.unionTypeParts(type).every(isNullishType); - function toStaticValue( type: ts.Type, ): @@ -100,10 +53,6 @@ function toStaticValue( return undefined; } -function pseudoBigIntToBigInt(value: ts.PseudoBigInt): bigint { - return BigInt((value.negative ? '-' : '') + value.base10Value); -} - const BOOL_OPERATORS = new Set([ '<', '>', @@ -151,7 +100,6 @@ function booleanComparison( return left >= right; } } - // #endregion export type Options = [ diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index fabea709e28e..bb506b38a6ac 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -11,13 +11,19 @@ import { getTypeFlags, isLogicalOrOperator, isNodeEqual, + isNodeOfTypes, isNullLiteral, - isTypeFlagSet, + isPossiblyNullish, isUndefinedIdentifier, nullThrows, NullThrowsReasons, } from '../util'; +const isIdentifierOrMemberExpression = isNodeOfTypes([ + AST_NODE_TYPES.Identifier, + AST_NODE_TYPES.MemberExpression, +] as const); + export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -179,32 +185,17 @@ export default createRule({ }); } - // todo: rename to something more specific? - function checkAssignmentOrLogicalExpression( - node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, - description: string, - equals: string, - ): void { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(tsNode.left); - if (!isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined)) { - return; - } - - if (ignoreConditionalTests === true && isConditionalTest(node)) { - return; - } - - if ( - ignoreMixedLogicalExpressions === true && - isMixedLogicalExpression(node) - ) { - return; + /** + * Checks whether a type tested for truthiness is eligible for conversion to + * a nullishness check, taking into account the rule's configuration. + */ + function isTypeEligibleForPreferNullish(type: ts.Type): boolean { + if (!isPossiblyNullish(type)) { + return false; } - // https://github.com/typescript-eslint/typescript-eslint/issues/5439 - /* eslint-disable @typescript-eslint/no-non-null-assertion */ const ignorableFlags = [ + /* eslint-disable @typescript-eslint/no-non-null-assertion */ (ignorePrimitives === true || ignorePrimitives!.bigint) && ts.TypeFlags.BigIntLike, (ignorePrimitives === true || ignorePrimitives!.boolean) && @@ -213,6 +204,7 @@ export default createRule({ ts.TypeFlags.NumberLike, (ignorePrimitives === true || ignorePrimitives!.string) && ts.TypeFlags.StringLike, + /* eslint-enable @typescript-eslint/no-non-null-assertion */ ] .filter((flag): flag is number => typeof flag === 'number') .reduce((previous, flag) => previous | flag, 0); @@ -224,10 +216,73 @@ export default createRule({ .intersectionTypeParts(t) .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), ) + ) { + return false; + } + + return true; + } + + /** + * Determines whether a control flow construct that uses the truthiness of + * a test expression is eligible for conversion to the nullish coalescing + * operator, taking into account (both dependent on the rule's configuration): + * 1. Whether the construct is in a permitted syntactic context + * 2. Whether the type of the test expression is deemed eligible for + * conversion + * + * @param node The overall node to be converted (e.g. `a || b` or `a ? a : b`) + * @param testNode The node being tested (i.e. `a`) + */ + function isTruthinessCheckEligibleForPreferNullish({ + node, + testNode, + }: { + node: + | TSESTree.AssignmentExpression + | TSESTree.ConditionalExpression + | TSESTree.LogicalExpression; + testNode: TSESTree.Node; + }): boolean { + const testType = parserServices.getTypeAtLocation(testNode); + if (!isTypeEligibleForPreferNullish(testType)) { + return false; + } + + if (ignoreConditionalTests === true && isConditionalTest(node)) { + return false; + } + + if ( + ignoreBooleanCoercion === true && + isBooleanConstructorContext(node, context) + ) { + return false; + } + + return true; + } + + function checkAndFixWithPreferNullishOverOr( + node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, + description: string, + equals: string, + ): void { + if ( + !isTruthinessCheckEligibleForPreferNullish({ + node, + testNode: node.left, + }) + ) { + return; + } + + if ( + ignoreMixedLogicalExpressions === true && + isMixedLogicalExpression(node) ) { return; } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ const barBarOperator = nullThrows( context.sourceCode.getTokenAfter( @@ -278,14 +333,14 @@ export default createRule({ 'AssignmentExpression[operator = "||="]'( node: TSESTree.AssignmentExpression, ): void { - checkAssignmentOrLogicalExpression(node, 'assignment', '='); + checkAndFixWithPreferNullishOverOr(node, 'assignment', '='); }, ConditionalExpression(node: TSESTree.ConditionalExpression): void { if (ignoreTernaryTests) { return; } - let operator: '!=' | '!==' | '==' | '===' | undefined; + let operator: '!' | '!=' | '!==' | '==' | '===' | undefined; let nodesInsideTestExpression: TSESTree.Node[] = []; if (node.test.type === AST_NODE_TYPES.BinaryExpression) { nodesInsideTestExpression = [node.test.left, node.test.right]; @@ -343,43 +398,74 @@ export default createRule({ } } - if (!operator) { - return; - } + let identifierOrMemberExpression: TSESTree.Node | undefined; + let hasTruthinessCheck = false; + let hasNullCheckWithoutTruthinessCheck = false; + let hasUndefinedCheckWithoutTruthinessCheck = false; - let identifier: TSESTree.Node | undefined; - let hasUndefinedCheck = false; - let hasNullCheck = false; + if (!operator) { + hasTruthinessCheck = true; - // we check that the test only contains null, undefined and the identifier - for (const testNode of nodesInsideTestExpression) { - if (isNullLiteral(testNode)) { - hasNullCheck = true; - } else if (isUndefinedIdentifier(testNode)) { - hasUndefinedCheck = true; - } else if ( - (operator === '!==' || operator === '!=') && - isNodeEqual(testNode, node.consequent) + if ( + isIdentifierOrMemberExpression(node.test) && + isNodeEqual(node.test, node.consequent) ) { - identifier = testNode; + identifierOrMemberExpression = node.test; } else if ( - (operator === '===' || operator === '==') && - isNodeEqual(testNode, node.alternate) + node.test.type === AST_NODE_TYPES.UnaryExpression && + node.test.operator === '!' && + isIdentifierOrMemberExpression(node.test.argument) && + isNodeEqual(node.test.argument, node.alternate) ) { - identifier = testNode; - } else { - return; + identifierOrMemberExpression = node.test.argument; + operator = '!'; + } + } 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 ( + (operator === '!==' || operator === '!=') && + isNodeEqual(testNode, node.consequent) + ) { + identifierOrMemberExpression = testNode; + } else if ( + (operator === '===' || operator === '==') && + isNodeEqual(testNode, node.alternate) + ) { + identifierOrMemberExpression = testNode; + } } } - if (!identifier) { + if (!identifierOrMemberExpression) { return; } - const isFixable = ((): boolean => { + const isFixableWithPreferNullishOverTernary = ((): boolean => { + // x ? x : y and !x ? y : x patterns + if (hasTruthinessCheck) { + return isTruthinessCheckEligibleForPreferNullish({ + node, + testNode: identifierOrMemberExpression, + }); + } + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + identifierOrMemberExpression, + ); + const type = checker.getTypeAtLocation(tsNode); + const flags = getTypeFlags(type); + // it is fixable if we check for both null and undefined, or not if neither - if (hasUndefinedCheck === hasNullCheck) { - return hasUndefinedCheck; + if ( + hasUndefinedCheckWithoutTruthinessCheck === + hasNullCheckWithoutTruthinessCheck + ) { + return hasUndefinedCheckWithoutTruthinessCheck; } // it is fixable if we loosely check for either null or undefined @@ -387,10 +473,6 @@ export default createRule({ return true; } - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(identifier); - const type = checker.getTypeAtLocation(tsNode); - const flags = getTypeFlags(type); - if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { return false; } @@ -398,17 +480,17 @@ export default createRule({ const hasNullType = (flags & ts.TypeFlags.Null) !== 0; // it is fixable if we check for undefined and the type is not nullable - if (hasUndefinedCheck && !hasNullType) { + if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) { return true; } const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0; // it is fixable if we check for null and the type can't be undefined - return hasNullCheck && !hasUndefinedType; + return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType; })(); - if (isFixable) { + if (isFixableWithPreferNullishOverTernary) { context.report({ node, messageId: 'preferNullishOverTernary', @@ -420,9 +502,9 @@ export default createRule({ data: { equals: '' }, fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = - operator === '===' || operator === '==' - ? [node.alternate, node.consequent] - : [node.consequent, node.alternate]; + operator === '===' || operator === '==' || operator === '!' + ? [identifierOrMemberExpression, node.consequent] + : [identifierOrMemberExpression, node.alternate]; return fixer.replaceText( node, `${getTextWithParentheses(context.sourceCode, left)} ?? ${getTextWithParentheses( @@ -439,14 +521,7 @@ export default createRule({ 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { - if ( - ignoreBooleanCoercion === true && - isBooleanConstructorContext(node, context) - ) { - return; - } - - checkAssignmentOrLogicalExpression(node, 'or', ''); + checkAndFixWithPreferNullishOverOr(node, 'or', ''); }, }; }, diff --git a/packages/eslint-plugin/src/util/getValueOfLiteralType.ts b/packages/eslint-plugin/src/util/getValueOfLiteralType.ts new file mode 100644 index 000000000000..78f61407ffe7 --- /dev/null +++ b/packages/eslint-plugin/src/util/getValueOfLiteralType.ts @@ -0,0 +1,20 @@ +import type * as ts from 'typescript'; + +const valueIsPseudoBigInt = ( + value: number | string | ts.PseudoBigInt, +): value is ts.PseudoBigInt => { + return typeof value === 'object'; +}; + +const pseudoBigIntToBigInt = (value: ts.PseudoBigInt): bigint => { + return BigInt((value.negative ? '-' : '') + value.base10Value); +}; + +export const getValueOfLiteralType = ( + type: ts.LiteralType, +): bigint | number | string => { + if (valueIsPseudoBigInt(type.value)) { + return pseudoBigIntToBigInt(type.value); + } + return type.value; +}; diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 64a83b38dae0..c8a0927b162b 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -24,6 +24,8 @@ export * from './needsToBeAwaited'; export * from './scopeUtils'; export * from './types'; export * from './getConstraintInfo'; +export * from './getValueOfLiteralType'; +export * from './truthinessAndNullishUtils'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; diff --git a/packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts b/packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts new file mode 100644 index 000000000000..b35e0334719d --- /dev/null +++ b/packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts @@ -0,0 +1,41 @@ +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import { getValueOfLiteralType } from './getValueOfLiteralType'; + +// Truthiness utilities +const isTruthyLiteral = (type: ts.Type): boolean => + tsutils.isTrueLiteralType(type) || + (type.isLiteral() && !!getValueOfLiteralType(type)); + +export const isPossiblyFalsy = (type: ts.Type): boolean => + tsutils + .unionTypeParts(type) + // Intersections like `string & {}` can also be possibly falsy, + // requiring us to look into the intersection. + .flatMap(type => tsutils.intersectionTypeParts(type)) + // PossiblyFalsy flag includes literal values, so exclude ones that + // are definitely truthy + .filter(t => !isTruthyLiteral(t)) + .some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy)); + +export const isPossiblyTruthy = (type: ts.Type): boolean => + tsutils + .unionTypeParts(type) + .map(type => tsutils.intersectionTypeParts(type)) + .some(intersectionParts => + // It is possible to define intersections that are always falsy, + // like `"" & { __brand: string }`. + intersectionParts.every(type => !tsutils.isFalsyType(type)), + ); + +// Nullish utilities +const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; +const isNullishType = (type: ts.Type): boolean => + tsutils.isTypeFlagSet(type, nullishFlag); + +export const isPossiblyNullish = (type: ts.Type): boolean => + tsutils.unionTypeParts(type).some(isNullishType); + +export const isAlwaysNullish = (type: ts.Type): boolean => + tsutils.unionTypeParts(type).every(isNullishType); 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 915762f419b3..aeaefe9dc5ff 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 @@ -19,7 +19,9 @@ foo === undefined ? 'a string' : foo; const foo: string | null = 'bar'; foo !== null ? foo : 'a string'; +foo ? foo : 'a string'; foo === null ? 'a string' : foo; +!foo ? 'a string' : foo; " `; @@ -39,6 +41,8 @@ foo ?? 'a string'; const foo: string | null = 'bar'; foo ?? 'a string'; foo ?? 'a string'; +foo ?? 'a string'; +foo ?? 'a string'; " `; 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 9e0bbdfab720..51442dbc7467 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -125,6 +125,230 @@ x === null ? x : y; declare let x: string | null | unknown; x === null ? x : y; `, + ` +declare let x: string; +x ? x : y; + `, + ` +declare let x: string; +!x ? y : x; + `, + ` +declare let x: string | object; +x ? x : y; + `, + ` +declare let x: string | object; +!x ? y : x; + `, + ` +declare let x: number; +x ? x : y; + `, + ` +declare let x: number; +!x ? y : x; + `, + ` +declare let x: bigint; +x ? x : y; + `, + ` +declare let x: bigint; +!x ? y : x; + `, + ` +declare let x: boolean; +x ? x : y; + `, + ` +declare let x: boolean; +!x ? y : x; + `, + ` +declare let x: any; +x ? x : y; + `, + ` +declare let x: any; +!x ? y : x; + `, + ` +declare let x: unknown; +x ? x : y; + `, + ` +declare let x: unknown; +!x ? y : x; + `, + ` +declare let x: object; +x ? x : y; + `, + ` +declare let x: object; +!x ? y : x; + `, + ` +declare let x: string[]; +x ? x : y; + `, + ` +declare let x: string[]; +!x ? y : x; + `, + ` +declare let x: Function; +x ? x : y; + `, + ` +declare let x: Function; +!x ? y : x; + `, + ` +declare let x: () => string; +x ? x : y; + `, + ` +declare let x: () => string; +!x ? y : x; + `, + ` +declare let x: () => string | null; +x ? x : y; + `, + ` +declare let x: () => string | null; +!x ? y : x; + `, + ` +declare let x: () => string | undefined; +x ? x : y; + `, + ` +declare let x: () => string | undefined; +!x ? y : x; + `, + ` +declare let x: () => string | null | undefined; +x ? x : y; + `, + ` +declare let x: () => string | null | undefined; +!x ? y : x; + `, + ` +declare let x: { n: string }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: any }; +x.n ? x.n : y; + `, + ` +declare let x: { n: any }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: unknown }; +x.n ? x.n : y; + `, + ` +declare let x: { n: unknown }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: object }; +x ? x : y; + `, + ` +declare let x: { n: object }; +!x ? y : x; + `, + ` +declare let x: { n: string[] }; +x ? x : y; + `, + ` +declare let x: { n: string[] }; +!x ? y : x; + `, + ` +declare let x: { n: Function }; +x ? x : y; + `, + ` +declare let x: { n: Function }; +!x ? y : x; + `, + ` +declare let x: { n: () => string }; +x ? x : y; + `, + ` +declare let x: { n: () => string }; +!x ? y : x; + `, + ` +declare let x: { n: () => string | null }; +x ? x : y; + `, + ` +declare let x: { n: () => string | null }; +!x ? y : x; + `, + ` +declare let x: { n: () => string | undefined }; +x ? x : y; + `, + ` +declare let x: { n: () => string | undefined }; +!x ? y : x; + `, + ` +declare let x: { n: () => string | null | undefined }; +x ? x : y; + `, + ` +declare let x: { n: () => string | null | undefined }; +!x ? y : x; + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -220,6 +444,62 @@ x || y; `, options: [{ ignorePrimitives: true }], })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: true }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: true }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: true }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: true }], + })), ` declare let x: any; declare let y: number; @@ -235,6 +515,36 @@ x || y; declare let y: number; x || y; `, + ` + declare let x: any; + declare let y: number; + x ? x : y; + `, + ` + declare let x: any; + declare let y: number; + !x ? y : x; + `, + ` + declare let x: unknown; + declare let y: number; + x ? x : y; + `, + ` + declare let x: unknown; + declare let y: number; + !x ? y : x; + `, + ` + declare let x: never; + declare let y: number; + x ? x : y; + `, + ` + declare let x: never; + declare let y: number; + !x ? y : x; + `, { code: ` declare let x: 0 | 1 | 0n | 1n | undefined; @@ -369,85 +679,279 @@ x || y; }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(a || b); +declare let x: 0 | 1 | 0n | 1n | undefined; +x ? x : y; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a || b || c); +declare let x: 0 | 1 | 0n | 1n | undefined; +!x ? y : x; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a || (b && c)); +declare let x: 0 | 1 | 0n | 1n | undefined; +x ? x : y; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean((a || b) ?? c); +declare let x: 0 | 1 | 0n | 1n | undefined; +!x ? y : x; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a ?? (b || c)); +declare let x: 0 | 'foo' | undefined; +x ? x : y; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + number: true, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a ? b || c : 'fail'); +declare let x: 0 | 'foo' | undefined; +!x ? y : x; `, options: [ { - ignoreBooleanCoercion: true, - }, + ignorePrimitives: { + number: true, + string: true, + }, + }, + ], + }, + { + code: ` +declare let x: 0 | 'foo' | undefined; +x ? x : y; + `, + options: [ + { + ignorePrimitives: { + number: true, + string: false, + }, + }, + ], + }, + { + code: ` +declare let x: 0 | 'foo' | undefined; +!x ? y : x; + `, + options: [ + { + ignorePrimitives: { + number: true, + string: false, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum | undefined; +x ? x : y; + `, + options: [ + { + ignorePrimitives: { + number: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum | undefined; +!x ? y : x; + `, + options: [ + { + ignorePrimitives: { + number: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum.A | Enum.B | undefined; +x ? x : y; + `, + options: [ + { + ignorePrimitives: { + number: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum.A | Enum.B | undefined; +!x ? y : x; + `, + options: [ + { + ignorePrimitives: { + number: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum | undefined; +x ? x : y; + `, + options: [ + { + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum | undefined; +!x ? y : x; + `, + options: [ + { + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum.A | Enum.B | undefined; +x ? x : y; + `, + options: [ + { + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum.A | Enum.B | undefined; +!x ? y : x; + `, + options: [ + { + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +let a: string | true | undefined; +let b: string | boolean | undefined; + +const x = Boolean(a || b); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, ], }, { @@ -456,7 +960,7 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -const test = Boolean(a ? 'success' : b || c); +const test = Boolean(a || b || c); `, options: [ { @@ -470,7 +974,7 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -const test = Boolean(((a = b), b || c)); +const test = Boolean(a || (b && c)); `, options: [ { @@ -484,12 +988,11 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -if (a || b || c) { -} +const test = Boolean((a || b) ?? c); `, options: [ { - ignoreConditionalTests: true, + ignoreBooleanCoercion: true, }, ], }, @@ -499,12 +1002,11 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -if (a || (b && c)) { -} +const test = Boolean(a ?? (b || c)); `, options: [ { - ignoreConditionalTests: true, + ignoreBooleanCoercion: true, }, ], }, @@ -514,12 +1016,11 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -if ((a || b) ?? c) { -} +const test = Boolean(a ? b || c : 'fail'); `, options: [ { - ignoreConditionalTests: true, + ignoreBooleanCoercion: true, }, ], }, @@ -529,12 +1030,11 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -if (a ?? (b || c)) { -} +const test = Boolean(a ? 'success' : b || c); `, options: [ { - ignoreConditionalTests: true, + ignoreBooleanCoercion: true, }, ], }, @@ -544,12 +1044,37 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -if (a ? b || c : 'fail') { -} +const test = Boolean(((a = b), b || c)); `, options: [ { - ignoreConditionalTests: true, + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | true | undefined; +let b: string | boolean | undefined; + +const x = Boolean(a ? a : b); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; + +const test = Boolean(!a ? b : a); + `, + options: [ + { + ignoreBooleanCoercion: true, }, ], }, @@ -559,12 +1084,11 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -if (a ? 'success' : b || c) { -} +const test = Boolean((a ? a : b) || c); `, options: [ { - ignoreConditionalTests: true, + ignoreBooleanCoercion: true, }, ], }, @@ -574,21 +1098,21 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -if (((a = b), b || c)) { -} +const test = Boolean(c || (!a ? b : a)); `, options: [ { - ignoreConditionalTests: true, + ignoreBooleanCoercion: true, }, ], }, { code: ` -let a: string | undefined; -let b: string | undefined; +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; -if (!(a || b)) { +if (a || b || c) { } `, options: [ @@ -599,10 +1123,11 @@ if (!(a || b)) { }, { code: ` -let a: string | undefined; -let b: string | undefined; +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; -if (!!(a || b)) { +if (a || (b && c)) { } `, options: [ @@ -611,7 +1136,168 @@ if (!!(a || b)) { }, ], }, - ], + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if ((a || b) ?? c) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (a ?? (b || c)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (a ? b || c : 'fail') { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (a ? 'success' : b || c) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (((a = b), b || c)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | undefined; +let b: string | undefined; + +if (!(a || b)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | undefined; +let b: string | undefined; + +if (!!(a || b)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | true | undefined; +let b: string | boolean | undefined; + +if (a ? a : b) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; + +if (!a ? b : a) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if ((a ? a : b) || c) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (c || (!a ? b : a)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + ], invalid: [ ...nullishTypeTest((nullish, type, equals) => ({ code: ` @@ -787,400 +1473,1967 @@ x === null ? y : x; declare let x: string | null; null === x ? y : x; `, - ].map(code => ({ - code, - errors: [ - { - column: 1, - endColumn: code.split('\n')[2].length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverTernary' as const, - suggestions: [ - { - messageId: 'suggestNullish' as const, - output: ` -${code.split('\n')[1]} -x ?? y; + ` +declare let x: string | null; +x ? x : y; `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }] as const, - output: null, - })), - - // noStrictNullCheck - { - code: ` -declare let x: string[] | null; -if (x) { -} + ` +declare let x: string | null; +!x ? y : x; `, - errors: [ - { - column: 1, - line: 0, - messageId: 'noStrictNullCheck', - }, - ], - languageOptions: { - parserOptions: { - tsconfigRootDir: path.join(rootPath, 'unstrict'), - }, - }, - output: null, - }, - - // ignoreConditionalTests - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -(x ||${equals} 'foo') ? null : null; + ` +declare let x: string | undefined; +x ? x : y; `, - errors: [ - { - column: 4, - endColumn: 6 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -(x ??${equals} 'foo') ? null : null; + ` +declare let x: string | undefined; +!x ? y : x; `, - }, - ], - }, - ], - options: [{ ignoreConditionalTests: false }], - output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -if ((x ||${equals} 'foo')) {} + ` +declare let x: string | null | undefined; +x ? x : y; `, - errors: [ - { - column: 8, - endColumn: 10 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -if ((x ??${equals} 'foo')) {} + ` +declare let x: string | null | undefined; +!x ? y : x; `, - }, - ], + ` +declare let x: string | object | null; +x ? x : y; + `, + ` +declare let x: string | object | null; +!x ? y : x; + `, + ` +declare let x: string | object | undefined; +x ? x : y; + `, + ` +declare let x: string | object | undefined; +!x ? y : x; + `, + ` +declare let x: string | object | null | undefined; +x ? x : y; + `, + ` +declare let x: string | object | null | undefined; +!x ? y : x; + `, + ` +declare let x: number | null; +x ? x : y; + `, + ` +declare let x: number | null; +!x ? y : x; + `, + ` +declare let x: number | undefined; +x ? x : y; + `, + ` +declare let x: number | undefined; +!x ? y : x; + `, + ` +declare let x: number | null | undefined; +x ? x : y; + `, + ` +declare let x: number | null | undefined; +!x ? y : x; + `, + ` +declare let x: bigint | null; +x ? x : y; + `, + ` +declare let x: bigint | null; +!x ? y : x; + `, + ` +declare let x: bigint | undefined; +x ? x : y; + `, + ` +declare let x: bigint | undefined; +!x ? y : x; + `, + ` +declare let x: bigint | null | undefined; +x ? x : y; + `, + ` +declare let x: bigint | null | undefined; +!x ? y : x; + `, + ` +declare let x: boolean | null; +x ? x : y; + `, + ` +declare let x: boolean | null; +!x ? y : x; + `, + ` +declare let x: boolean | undefined; +x ? x : y; + `, + ` +declare let x: boolean | undefined; +!x ? y : x; + `, + ` +declare let x: boolean | null | undefined; +x ? x : y; + `, + ` +declare let x: boolean | null | undefined; +!x ? y : x; + `, + ` +declare let x: string[] | null; +x ? x : y; + `, + ` +declare let x: string[] | null; +!x ? y : x; + `, + ` +declare let x: string[] | undefined; +x ? x : y; + `, + ` +declare let x: string[] | undefined; +!x ? y : x; + `, + ` +declare let x: string[] | null | undefined; +x ? x : y; + `, + ` +declare let x: string[] | null | undefined; +!x ? y : x; + `, + ` +declare let x: object | null; +x ? x : y; + `, + ` +declare let x: object | null; +!x ? y : x; + `, + ` +declare let x: object | undefined; +x ? x : y; + `, + ` +declare let x: object | undefined; +!x ? y : x; + `, + ` +declare let x: object | null | undefined; +x ? x : y; + `, + ` +declare let x: object | null | undefined; +!x ? y : x; + `, + ` +declare let x: Function | null; +x ? x : y; + `, + ` +declare let x: Function | null; +!x ? y : x; + `, + ` +declare let x: Function | undefined; +x ? x : y; + `, + ` +declare let x: Function | undefined; +!x ? y : x; + `, + ` +declare let x: Function | null | undefined; +x ? x : y; + `, + ` +declare let x: Function | null | undefined; +!x ? y : x; + `, + ` +declare let x: (() => string) | null; +x ? x : y; + `, + ` +declare let x: (() => string) | null; +!x ? y : x; + `, + ` +declare let x: (() => string) | undefined; +x ? x : y; + `, + ` +declare let x: (() => string) | undefined; +!x ? y : x; + `, + ` +declare let x: (() => string) | null | undefined; +x ? x : y; + `, + ` +declare let x: (() => string) | null | undefined; +!x ? y : x; + `, + ].map(code => ({ + code, + errors: [ + { + column: 1, + endColumn: code.split('\n')[2].length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverTernary' as const, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: ` +${code.split('\n')[1]} +x ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }] as const, + output: null, + })), + + ...[ + ` +declare let x: { n: string | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string[] | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string[] | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string[] | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string[] | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string[] | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string[] | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: object | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: object | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: object | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: object | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: object | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: object | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: Function | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: Function | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: Function | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: Function | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: Function | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: Function | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: (() => string) | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: (() => string) | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: (() => string) | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: (() => string) | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: (() => string) | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: (() => string) | null | undefined }; +!x.n ? y : x.n; + `, + ].map(code => ({ + code, + errors: [ + { + column: 1, + endColumn: code.split('\n')[2].length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverTernary' as const, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: ` +${code.split('\n')[1]} +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }] as const, + output: null, + })), + + // noStrictNullCheck + { + code: ` +declare let x: string[] | null; +if (x) { +} + `, + errors: [ + { + column: 1, + line: 0, + messageId: 'noStrictNullCheck', + }, + ], + languageOptions: { + parserOptions: { + tsconfigRootDir: path.join(rootPath, 'unstrict'), + }, + }, + output: null, + }, + + // ignoreConditionalTests + ...nullishTypeTest((nullish, type, equals) => ({ + code: ` +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo') ? null : null; + `, + errors: [ + { + column: 4, + endColumn: 6 + equals.length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: ${type} | ${nullish}; +(x ??${equals} 'foo') ? null : null; + `, + }, + ], + }, + ], + options: [{ ignoreConditionalTests: false }], + output: null, + })), + ...nullishTypeTest((nullish, type, equals) => ({ + code: ` +declare let x: ${type} | ${nullish}; +if ((x ||${equals} 'foo')) {} + `, + errors: [ + { + column: 8, + endColumn: 10 + equals.length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: ${type} | ${nullish}; +if ((x ??${equals} 'foo')) {} + `, + }, + ], + }, + ], + options: [{ ignoreConditionalTests: false }], + output: null, + })), + ...nullishTypeTest((nullish, type, equals) => ({ + code: ` +declare let x: ${type} | ${nullish}; +do {} while ((x ||${equals} 'foo')) + `, + errors: [ + { + column: 17, + endColumn: 19 + equals.length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: ${type} | ${nullish}; +do {} while ((x ??${equals} 'foo')) + `, + }, + ], + }, + ], + options: [{ ignoreConditionalTests: false }], + output: null, + })), + ...nullishTypeTest((nullish, type, equals) => ({ + code: ` +declare let x: ${type} | ${nullish}; +for (;(x ||${equals} 'foo');) {} + `, + errors: [ + { + column: 10, + endColumn: 12 + equals.length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: ${type} | ${nullish}; +for (;(x ??${equals} 'foo');) {} + `, + }, + ], + }, + ], + options: [{ ignoreConditionalTests: false }], + output: null, + })), + ...nullishTypeTest((nullish, type, equals) => ({ + code: ` +declare let x: ${type} | ${nullish}; +while ((x ||${equals} 'foo')) {} + `, + errors: [ + { + column: 11, + endColumn: 13 + equals.length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: ${type} | ${nullish}; +while ((x ??${equals} 'foo')) {} + `, + }, + ], + }, + ], + options: [{ ignoreConditionalTests: false }], + output: null, + })), + + // ignoreMixedLogicalExpressions + ...nullishTypeTest((nullish, type) => ({ + code: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +a || b && c; + `, + errors: [ + { + column: 3, + endColumn: 5, + endLine: 5, + line: 5, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +a ?? b && c; + `, + }, + ], + }, + ], + options: [{ ignoreMixedLogicalExpressions: false }], + })), + ...nullishTypeTest((nullish, type) => ({ + code: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a || b || c && d; + `, + errors: [ + { + column: 3, + endColumn: 5, + endLine: 6, + line: 6, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +(a ?? b) || c && d; + `, + }, + ], + }, + { + column: 8, + endColumn: 10, + endLine: 6, + line: 6, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a || b ?? c && d; + `, + }, + ], + }, + ], + options: [{ ignoreMixedLogicalExpressions: false }], + })), + ...nullishTypeTest((nullish, type) => ({ + code: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a && b || c || d; + `, + errors: [ + { + column: 8, + endColumn: 10, + endLine: 6, + line: 6, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a && (b ?? c) || d; + `, + }, + ], + }, + { + column: 13, + endColumn: 15, + endLine: 6, + line: 6, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a && b || c ?? d; + `, + }, + ], + }, + ], + options: [{ ignoreMixedLogicalExpressions: false }], + })), + + // should not false positive for functions inside conditional tests + ...nullishTypeTest((nullish, type, equals) => ({ + code: ` +declare let x: ${type} | ${nullish}; +if (() => (x ||${equals} 'foo')) {} + `, + errors: [ + { + column: 14, + endColumn: 16 + equals.length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: ${type} | ${nullish}; +if (() => (x ??${equals} 'foo')) {} + `, + }, + ], + }, + ], + output: null, + })), + ...nullishTypeTest((nullish, type, equals) => ({ + code: ` +declare let x: ${type} | ${nullish}; +if (function weird() { return (x ||${equals} 'foo') }) {} + `, + errors: [ + { + column: 34, + endColumn: 36 + equals.length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: ${type} | ${nullish}; +if (function weird() { return (x ??${equals} 'foo') }) {} + `, + }, + ], + }, + ], + output: null, + })), + // https://github.com/typescript-eslint/typescript-eslint/issues/1290 + ...nullishTypeTest((nullish, type) => ({ + code: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type}; +declare let c: ${type}; +a || b || c; + `, + errors: [ + { + column: 3, + endColumn: 5, + endLine: 5, + line: 5, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type}; +declare let c: ${type}; +(a ?? b) || c; + `, + }, + ], + }, + ], + output: null, + })), + // default for missing option + { + code: ` +declare let x: string | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: string | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, number: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: number | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: number | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: boolean | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: boolean | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, number: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: bigint | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: bigint | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { boolean: true, number: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: string | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: string | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, number: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: number | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: number | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: boolean | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: boolean | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, number: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: bigint | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: bigint | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { boolean: true, number: true, string: true }, + }, + ], + output: null, + }, + // falsy + { + code: ` +declare let x: '' | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: '' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`\` | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0 | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0n | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: false | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: false | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: '' | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: '' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`\` | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0 | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0n | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: false | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: false | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + // truthy + { + code: ` +declare let x: 'a' | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 'a' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`hello\${'string'}\` | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`hello\${'string'}\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, }, ], - options: [{ ignoreConditionalTests: false }], output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ + }, + { code: ` -declare let x: ${type} | ${nullish}; -do {} while ((x ||${equals} 'foo')) +declare let x: 1 | undefined; +x || y; `, errors: [ { - column: 17, - endColumn: 19 + equals.length, - endLine: 3, - line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: ${type} | ${nullish}; -do {} while ((x ??${equals} 'foo')) +declare let x: 1 | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreConditionalTests: false }], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ + }, + { code: ` -declare let x: ${type} | ${nullish}; -for (;(x ||${equals} 'foo');) {} +declare let x: 1n | undefined; +x || y; `, errors: [ { - column: 10, - endColumn: 12 + equals.length, - endLine: 3, - line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: ${type} | ${nullish}; -for (;(x ??${equals} 'foo');) {} +declare let x: 1n | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreConditionalTests: false }], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ + }, + { code: ` -declare let x: ${type} | ${nullish}; -while ((x ||${equals} 'foo')) {} +declare let x: true | undefined; +x || y; `, errors: [ { - column: 11, - endColumn: 13 + equals.length, - endLine: 3, - line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: ${type} | ${nullish}; -while ((x ??${equals} 'foo')) {} +declare let x: true | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 'a' | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 'a' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 'a' | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 'a' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`hello\${'string'}\` | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`hello\${'string'}\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`hello\${'string'}\` | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`hello\${'string'}\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 1 | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 1 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 1 | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 1 | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreConditionalTests: false }], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, - })), - - // ignoreMixedLogicalExpressions - ...nullishTypeTest((nullish, type) => ({ + }, + { code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -a || b && c; +declare let x: 1n | undefined; +x ? x : y; `, errors: [ { - column: 3, - endColumn: 5, - endLine: 5, - line: 5, - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -a ?? b && c; +declare let x: 1n | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreMixedLogicalExpressions: false }], - })), - ...nullishTypeTest((nullish, type) => ({ + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a || b || c && d; +declare let x: 1n | undefined; +!x ? y : x; `, errors: [ { - column: 3, - endColumn: 5, - endLine: 6, - line: 6, - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -(a ?? b) || c && d; +declare let x: 1n | undefined; +x ?? y; `, }, ], }, + ], + options: [ { - column: 8, - endColumn: 10, - endLine: 6, - line: 6, - messageId: 'preferNullishOverOr', + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: true | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a || b ?? c && d; +declare let x: true | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreMixedLogicalExpressions: false }], - })), - ...nullishTypeTest((nullish, type) => ({ + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a && b || c || d; +declare let x: true | undefined; +!x ? y : x; `, errors: [ { - column: 8, - endColumn: 10, - endLine: 6, - line: 6, - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a && (b ?? c) || d; +declare let x: true | undefined; +x ?? y; `, }, ], }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + // Unions of same primitive + { + code: ` +declare let x: 'a' | 'b' | undefined; +x || y; + `, + errors: [ { - column: 13, - endColumn: 15, - endLine: 6, - line: 6, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a && b || c ?? d; +declare let x: 'a' | 'b' | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreMixedLogicalExpressions: false }], - })), - - // should not false positive for functions inside conditional tests - ...nullishTypeTest((nullish, type, equals) => ({ + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { code: ` -declare let x: ${type} | ${nullish}; -if (() => (x ||${equals} 'foo')) {} +declare let x: 'a' | \`b\` | undefined; +x || y; `, errors: [ { - column: 14, - endColumn: 16 + equals.length, - endLine: 3, - line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: ${type} | ${nullish}; -if (() => (x ??${equals} 'foo')) {} +declare let x: 'a' | \`b\` | undefined; +x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ + }, + { code: ` -declare let x: ${type} | ${nullish}; -if (function weird() { return (x ||${equals} 'foo') }) {} +declare let x: 0 | 1 | undefined; +x || y; `, errors: [ { - column: 34, - endColumn: 36 + equals.length, - endLine: 3, - line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: ${type} | ${nullish}; -if (function weird() { return (x ??${equals} 'foo') }) {} +declare let x: 0 | 1 | undefined; +x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, - })), - // https://github.com/typescript-eslint/typescript-eslint/issues/1290 - ...nullishTypeTest((nullish, type) => ({ + }, + { code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type}; -declare let c: ${type}; -a || b || c; +declare let x: 1 | 2 | 3 | undefined; +x || y; `, errors: [ { - column: 3, - endColumn: 5, - endLine: 5, - line: 5, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type}; -declare let c: ${type}; -(a ?? b) || c; +declare let x: 1 | 2 | 3 | undefined; +x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, - })), - // default for missing option + }, { code: ` -declare let x: string | undefined; +declare let x: 0n | 1n | undefined; x || y; `, errors: [ @@ -1190,7 +3443,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: string | undefined; +declare let x: 0n | 1n | undefined; x ?? y; `, }, @@ -1199,14 +3452,19 @@ x ?? y; ], options: [ { - ignorePrimitives: { bigint: true, boolean: true, number: true }, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], output: null, }, { code: ` -declare let x: number | undefined; +declare let x: 1n | 2n | 3n | undefined; x || y; `, errors: [ @@ -1216,7 +3474,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: number | undefined; +declare let x: 1n | 2n | 3n | undefined; x ?? y; `, }, @@ -1225,14 +3483,19 @@ x ?? y; ], options: [ { - ignorePrimitives: { bigint: true, boolean: true, string: true }, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], output: null, }, { code: ` -declare let x: boolean | undefined; +declare let x: true | false | undefined; x || y; `, errors: [ @@ -1242,7 +3505,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: boolean | undefined; +declare let x: true | false | undefined; x ?? y; `, }, @@ -1251,24 +3514,29 @@ x ?? y; ], options: [ { - ignorePrimitives: { bigint: true, number: true, string: true }, + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, }, ], output: null, }, { code: ` -declare let x: bigint | undefined; -x || y; +declare let x: 'a' | 'b' | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: bigint | undefined; +declare let x: 'a' | 'b' | undefined; x ?? y; `, }, @@ -1277,25 +3545,29 @@ x ?? y; ], options: [ { - ignorePrimitives: { boolean: true, number: true, string: true }, + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, }, ], output: null, }, - // falsy { code: ` -declare let x: '' | undefined; -x || y; +declare let x: 'a' | 'b' | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: '' | undefined; +declare let x: 'a' | 'b' | undefined; x ?? y; `, }, @@ -1316,17 +3588,17 @@ x ?? y; }, { code: ` -declare let x: \`\` | undefined; -x || y; +declare let x: 'a' | \`b\` | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: \`\` | undefined; +declare let x: 'a' | \`b\` | undefined; x ?? y; `, }, @@ -1347,17 +3619,17 @@ x ?? y; }, { code: ` -declare let x: 0 | undefined; -x || y; +declare let x: 'a' | \`b\` | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0 | undefined; +declare let x: 'a' | \`b\` | undefined; x ?? y; `, }, @@ -1369,8 +3641,8 @@ x ?? y; ignorePrimitives: { bigint: true, boolean: true, - number: false, - string: true, + number: true, + string: false, }, }, ], @@ -1378,17 +3650,17 @@ x ?? y; }, { code: ` -declare let x: 0n | undefined; -x || y; +declare let x: 0 | 1 | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0n | undefined; +declare let x: 0 | 1 | undefined; x ?? y; `, }, @@ -1398,9 +3670,9 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: false, + bigint: true, boolean: true, - number: true, + number: false, string: true, }, }, @@ -1409,17 +3681,17 @@ x ?? y; }, { code: ` -declare let x: false | undefined; -x || y; +declare let x: 0 | 1 | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: false | undefined; +declare let x: 0 | 1 | undefined; x ?? y; `, }, @@ -1430,28 +3702,27 @@ x ?? y; { ignorePrimitives: { bigint: true, - boolean: false, - number: true, + boolean: true, + number: false, string: true, }, }, ], output: null, }, - // truthy { code: ` -declare let x: 'a' | undefined; -x || y; +declare let x: 1 | 2 | 3 | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 'a' | undefined; +declare let x: 1 | 2 | 3 | undefined; x ?? y; `, }, @@ -1463,8 +3734,8 @@ x ?? y; ignorePrimitives: { bigint: true, boolean: true, - number: true, - string: false, + number: false, + string: true, }, }, ], @@ -1472,17 +3743,17 @@ x ?? y; }, { code: ` -declare let x: \`hello\${'string'}\` | undefined; -x || y; +declare let x: 1 | 2 | 3 | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: \`hello\${'string'}\` | undefined; +declare let x: 1 | 2 | 3 | undefined; x ?? y; `, }, @@ -1494,8 +3765,8 @@ x ?? y; ignorePrimitives: { bigint: true, boolean: true, - number: true, - string: false, + number: false, + string: true, }, }, ], @@ -1503,17 +3774,17 @@ x ?? y; }, { code: ` -declare let x: 1 | undefined; -x || y; +declare let x: 0n | 1n | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 1 | undefined; +declare let x: 0n | 1n | undefined; x ?? y; `, }, @@ -1523,9 +3794,9 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: true, + bigint: false, boolean: true, - number: false, + number: true, string: true, }, }, @@ -1534,17 +3805,17 @@ x ?? y; }, { code: ` -declare let x: 1n | undefined; -x || y; +declare let x: 0n | 1n | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 1n | undefined; +declare let x: 0n | 1n | undefined; x ?? y; `, }, @@ -1565,17 +3836,17 @@ x ?? y; }, { code: ` -declare let x: true | undefined; -x || y; +declare let x: 1n | 2n | 3n | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: true | undefined; +declare let x: 1n | 2n | 3n | undefined; x ?? y; `, }, @@ -1585,8 +3856,8 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: true, - boolean: false, + bigint: false, + boolean: true, number: true, string: true, }, @@ -1594,20 +3865,19 @@ x ?? y; ], output: null, }, - // Unions of same primitive { code: ` -declare let x: 'a' | 'b' | undefined; -x || y; +declare let x: 1n | 2n | 3n | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 'a' | 'b' | undefined; +declare let x: 1n | 2n | 3n | undefined; x ?? y; `, }, @@ -1617,10 +3887,10 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: true, + bigint: false, boolean: true, number: true, - string: false, + string: true, }, }, ], @@ -1628,17 +3898,17 @@ x ?? y; }, { code: ` -declare let x: 'a' | \`b\` | undefined; -x || y; +declare let x: true | false | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 'a' | \`b\` | undefined; +declare let x: true | false | undefined; x ?? y; `, }, @@ -1649,9 +3919,9 @@ x ?? y; { ignorePrimitives: { bigint: true, - boolean: true, + boolean: false, number: true, - string: false, + string: true, }, }, ], @@ -1659,17 +3929,17 @@ x ?? y; }, { code: ` -declare let x: 0 | 1 | undefined; -x || y; +declare let x: true | false | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0 | 1 | undefined; +declare let x: true | false | undefined; x ?? y; `, }, @@ -1680,17 +3950,18 @@ x ?? y; { ignorePrimitives: { bigint: true, - boolean: true, - number: false, + boolean: false, + number: true, string: true, }, }, ], output: null, }, + // Mixed unions { code: ` -declare let x: 1 | 2 | 3 | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x || y; `, errors: [ @@ -1700,7 +3971,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: 1 | 2 | 3 | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x ?? y; `, }, @@ -1710,7 +3981,7 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: true, + bigint: false, boolean: true, number: false, string: true, @@ -1721,7 +3992,7 @@ x ?? y; }, { code: ` -declare let x: 0n | 1n | undefined; +declare let x: true | false | null | undefined; x || y; `, errors: [ @@ -1731,7 +4002,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: 0n | 1n | undefined; +declare let x: true | false | null | undefined; x ?? y; `, }, @@ -1741,8 +4012,8 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: false, - boolean: true, + bigint: true, + boolean: false, number: true, string: true, }, @@ -1752,17 +4023,17 @@ x ?? y; }, { code: ` -declare let x: 1n | 2n | 3n | undefined; -x || y; +declare let x: 0 | 1 | 0n | 1n | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 1n | 2n | 3n | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x ?? y; `, }, @@ -1774,7 +4045,7 @@ x ?? y; ignorePrimitives: { bigint: false, boolean: true, - number: true, + number: false, string: true, }, }, @@ -1783,17 +4054,17 @@ x ?? y; }, { code: ` -declare let x: true | false | undefined; -x || y; +declare let x: 0 | 1 | 0n | 1n | undefined; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: true | false | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x ?? y; `, }, @@ -1803,29 +4074,28 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: true, - boolean: false, - number: true, + bigint: false, + boolean: true, + number: false, string: true, }, }, ], output: null, }, - // Mixed unions { code: ` -declare let x: 0 | 1 | 0n | 1n | undefined; -x || y; +declare let x: true | false | null | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0 | 1 | 0n | 1n | undefined; +declare let x: true | false | null | undefined; x ?? y; `, }, @@ -1835,9 +4105,9 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: false, - boolean: true, - number: false, + bigint: true, + boolean: false, + number: true, string: true, }, }, @@ -1847,11 +4117,11 @@ x ?? y; { code: ` declare let x: true | false | null | undefined; -x || y; +!x ? y : x; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2329,5 +4599,68 @@ if (+(a ?? b)) { }, ], }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox || getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox ?? getFallbackBox(); + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox ? defaultBox : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, ], }); diff --git a/packages/eslint-plugin/tests/util/getValueOfLiteralType.test.ts b/packages/eslint-plugin/tests/util/getValueOfLiteralType.test.ts new file mode 100644 index 000000000000..35a79242dc79 --- /dev/null +++ b/packages/eslint-plugin/tests/util/getValueOfLiteralType.test.ts @@ -0,0 +1,55 @@ +import type * as ts from 'typescript'; + +import { getValueOfLiteralType } from '../../src/util/getValueOfLiteralType'; + +describe('getValueOfLiteralType', () => { + it('returns a string for a string literal type', () => { + const stringLiteralType = { + value: 'hello' satisfies string, + } as ts.LiteralType; + + const result = getValueOfLiteralType(stringLiteralType); + + expect(result).toBe('hello'); + expect(typeof result).toBe('string'); + }); + + it('returns a number for a numeric literal type', () => { + const numberLiteralType = { + value: 42 satisfies number, + } as ts.LiteralType; + + const result = getValueOfLiteralType(numberLiteralType); + + expect(result).toBe(42); + expect(typeof result).toBe('number'); + }); + + it('returns a bigint for a pseudo-bigint literal type', () => { + const pseudoBigIntLiteralType = { + value: { + base10Value: '12345678901234567890', + negative: false, + } satisfies ts.PseudoBigInt, + } as ts.LiteralType; + + const result = getValueOfLiteralType(pseudoBigIntLiteralType); + + expect(result).toBe(BigInt('12345678901234567890')); + expect(typeof result).toBe('bigint'); + }); + + it('returns a negative bigint for a pseudo-bigint with negative=true', () => { + const negativePseudoBigIntLiteralType = { + value: { + base10Value: '98765432109876543210', + negative: true, + } satisfies ts.PseudoBigInt, + } as ts.LiteralType; + + const result = getValueOfLiteralType(negativePseudoBigIntLiteralType); + + expect(result).toBe(BigInt('-98765432109876543210')); + expect(typeof result).toBe('bigint'); + }); +});