diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index b05e43d0431e..9cf96444b25a 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -14,6 +14,7 @@ import { isNodeOfTypes, isNullLiteral, isNullableType, + isTypeFlagSet, isUndefinedIdentifier, nullThrows, NullThrowsReasons, @@ -211,7 +212,10 @@ export default createRule({ * a nullishness check, taking into account the rule's configuration. */ function isTypeEligibleForPreferNullish(type: ts.Type): boolean { - if (!isNullableType(type)) { + if ( + !isNullableType(type) && + !isTypeFlagSet(type, ts.TypeFlags.TypeParameter) + ) { return false; } @@ -235,16 +239,7 @@ export default createRule({ return true; } - // if the type is `any` or `unknown` we can't make any assumptions - // about the value, so it could be any primitive, even though the flags - // won't be set. - // - // technically, this is true of `void` as well, however, it's a TS error - // to test `void` for truthiness, so we don't need to bother checking for - // it in valid code. - if ( - tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown) - ) { + if (isIndeterminateType(type)) { return false; } @@ -254,7 +249,7 @@ export default createRule({ .some(t => tsutils .intersectionTypeParts(t) - .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), + .some(t => isTypeFlagSet(t, ignorableFlags)), ) ) { return false; @@ -447,12 +442,12 @@ export default createRule({ const type = parserServices.getTypeAtLocation( nullishCoalescingLeftNode, ); - const flags = getTypeFlags(type); - if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + if (isIndeterminateType(type)) { return false; } + const flags = getTypeFlags(type); const hasNullType = (flags & ts.TypeFlags.Null) !== 0; // it is fixable if we check for undefined and the type is not nullable @@ -932,3 +927,28 @@ function formatComments( ) .join(''); } + +/** + * Returns `true` if the provided type is `any`, `unknown`, + * or a union type that includes a type parameter. + * + * In these cases, no assumptions can be made about the value's structure or behavior, + * as it might represent `undefined`, `null`, or any primitive, + * even if the standard type flags are not set. + * + * Note: This could technically include `void` as well, but testing for `void` is unnecessary, + * since evaluating `void` for truthiness results in a TypeScript error. + */ +function isIndeterminateType(type: ts.Type): boolean { + if (isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + return true; + } + + return tsutils + .typeParts(type) + .some(t => + tsutils + .unionTypeParts(t) + .some(t => isTypeFlagSet(t, ts.TypeFlags.TypeParameter)), + ); +} 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 0926a839ee38..89a597c5004a 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -514,6 +514,41 @@ declare let y: { a: string } | null x?.a ? y?.a : 'foo' `, + ` +function test(val: T | undefined) { + return val !== undefined ? val : 'foo'; +} + `, + ` +function test(val: T | null) { + return val !== null ? val : 'foo'; +} + `, + ` +function test(val: T | string | undefined) { + return val !== undefined ? val : 'foo'; +} + `, + ` +function test(val: T | string | null) { + return val !== null ? val : 'foo'; +} + `, + ` +function test(val: T & string) { + return val !== undefined ? val : 'foo'; +} + `, + ` +function test(val: T & string) { + return val !== null ? val : 'foo'; +} + `, + ` +function test(val: T & string) { + return val ? val : 'foo'; +} + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -1541,6 +1576,60 @@ const b = a || 'bar'; }, ], }, + + { + code: ` +function test(val: T) { + return val || 'foo'; +} + `, + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: false, + string: false, + }, + }, + ], + }, + + { + code: ` +function test(val: T | string) { + return val || 'foo'; +} + `, + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: false, + string: false, + }, + }, + ], + }, + + { + code: ` +function test(val: T & string) { + return val || 'foo'; +} + `, + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: false, + string: false, + }, + }, + ], + }, ], invalid: [ @@ -6240,6 +6329,351 @@ declare function makeString(): string; function weirdParens() { ((foo).a) ??= makeString(); +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T) { + return val !== null && val !== undefined ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T) { + return val ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T) { + return val || 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string) { + return val !== null && val !== undefined ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string) { + return val ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string) { + return val || 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | null) { + return val !== null && val !== undefined ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | null) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | null) { + return val ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | null) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | null) { + return val || 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | null) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | undefined) { + return val !== null && val !== undefined ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | undefined) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | undefined) { + return val ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | undefined) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | undefined) { + return val || 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | undefined) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | null | undefined) { + return val !== null && val !== undefined ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | null | undefined) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | null | undefined) { + return val ? val : 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | null | undefined) { + return val ?? 'foo'; +} + `, + }, + ], + }, + ], + output: null, + }, + { + code: ` +function test(val: T | string | null | undefined) { + return val || 'foo'; +} + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +function test(val: T | string | null | undefined) { + return val ?? 'foo'; } `, },