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'); + }); +}); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy