diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx index 87cf88527046..438cc97d6af6 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx @@ -84,7 +84,7 @@ Examples of code for this rule with `{ ignoreConditionalTests: false }`: ```ts option='{ "ignoreConditionalTests": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a || b) { @@ -102,7 +102,7 @@ a || b ? true : false; ```ts option='{ "ignoreConditionalTests": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a ?? b) { @@ -133,7 +133,7 @@ Examples of code for this rule with `{ ignoreMixedLogicalExpressions: false }`: ```ts option='{ "ignoreMixedLogicalExpressions": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; @@ -149,7 +149,7 @@ a || (b && c && d); ```ts option='{ "ignoreMixedLogicalExpressions": false }' -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index cb93dfae3ba3..19e734179127 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -12,12 +12,10 @@ import { getTypeName, getTypeOfPropertyOfName, getValueOfLiteralType, - isAlwaysNullish, isArrayMethodCallWithPredicate, isIdentifier, isNullableType, isPossiblyFalsy, - isPossiblyNullish, isPossiblyTruthy, isTypeAnyType, isTypeFlagSet, @@ -31,6 +29,25 @@ import { } from '../util/assertionFunctionUtils'; // #region + +const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; + +function isNullishType(type: ts.Type): boolean { + return tsutils.isTypeFlagSet(type, nullishFlag); +} + +function isAlwaysNullish(type: ts.Type): boolean { + return tsutils.unionTypeParts(type).every(isNullishType); +} + +/** + * Note that this differs from {@link isNullableType} in that it doesn't consider + * `any` or `unknown` to be nullable. + */ +function isPossiblyNullish(type: ts.Type): boolean { + return tsutils.unionTypeParts(type).some(isNullishType); +} + function toStaticValue( type: ts.Type, ): diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 54d7b060dcb8..caa193b36bdd 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -13,7 +13,7 @@ import { isNodeEqual, isNodeOfTypes, isNullLiteral, - isPossiblyNullish, + isNullableType, isUndefinedIdentifier, nullThrows, NullThrowsReasons, @@ -193,7 +193,7 @@ export default createRule({ * a nullishness check, taking into account the rule's configuration. */ function isTypeEligibleForPreferNullish(type: ts.Type): boolean { - if (!isPossiblyNullish(type)) { + if (!isNullableType(type)) { return false; } @@ -211,14 +211,33 @@ export default createRule({ ] .filter((flag): flag is number => typeof flag === 'number') .reduce((previous, flag) => previous | flag, 0); + + if (ignorableFlags === 0) { + // any types are eligible for conversion. + 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) + ) { + return false; + } + if ( - type.flags !== ts.TypeFlags.Null && - type.flags !== ts.TypeFlags.Undefined && - (type as ts.UnionOrIntersectionType).types.some(t => - tsutils - .intersectionTypeParts(t) - .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), - ) + tsutils + .typeParts(type) + .some(t => + tsutils + .intersectionTypeParts(t) + .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), + ) ) { return false; } diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 6bd8189c5a51..035fca0cec4a 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -28,7 +28,7 @@ export * from './getConstraintInfo'; export * from './getValueOfLiteralType'; export * from './isHigherPrecedenceThanAwait'; export * from './skipChainExpression'; -export * from './truthinessAndNullishUtils'; +export * from './truthinessUtils'; // 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/truthinessUtils.ts similarity index 74% rename from packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts rename to packages/eslint-plugin/src/util/truthinessUtils.ts index b35e0334719d..048a7a60500e 100644 --- a/packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts +++ b/packages/eslint-plugin/src/util/truthinessUtils.ts @@ -28,14 +28,3 @@ export const isPossiblyTruthy = (type: ts.Type): boolean => // 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 124ed091013c..babba8f3ad55 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 @@ -55,17 +55,19 @@ exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint "Incorrect Options: { "ignoreConditionalTests": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a || b) { ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. } if ((a ||= b)) { + ~~~ Prefer using nullish coalescing operator (\`??=\`) instead of a logical assignment (\`||=\`), as it is a safer operator. } while (a || b) {} ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. while ((a ||= b)) {} + ~~~ Prefer using nullish coalescing operator (\`??=\`) instead of a logical assignment (\`||=\`), as it is a safer operator. do {} while (a || b); ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. for (let i = 0; a || b; i += 1) {} @@ -79,7 +81,7 @@ exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint "Correct Options: { "ignoreConditionalTests": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; if (a ?? b) { @@ -98,7 +100,7 @@ exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint "Incorrect Options: { "ignoreMixedLogicalExpressions": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; @@ -106,6 +108,7 @@ declare const d: string | null; a || (b && c); ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. a ||= b && c; + ~~~ Prefer using nullish coalescing operator (\`??=\`) instead of a logical assignment (\`||=\`), as it is a safer operator. (a && b) || c || d; ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. ~~ Prefer using nullish coalescing operator (\`??\`) instead of a logical or (\`||\`), as it is a safer operator. @@ -121,7 +124,7 @@ exports[`Validating rule docs prefer-nullish-coalescing.mdx code examples ESLint "Correct Options: { "ignoreMixedLogicalExpressions": false } -declare const a: string | null; +declare let a: string | null; declare const b: string | null; declare const c: string | null; declare const d: string | null; 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 1aa35f847af4..a57db7e341be 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -199,22 +199,6 @@ 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; `, ` @@ -311,22 +295,6 @@ 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; `, ` @@ -536,41 +504,11 @@ declare let x: (${type} & { __brand?: any }) | undefined; `, options: [{ ignorePrimitives: true }], })), - ` - declare let x: any; - declare let y: number; - x || y; - `, - ` - declare let x: unknown; - declare let y: number; - x || y; - `, ` declare let x: never; 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; @@ -1373,7 +1311,53 @@ if (c || (!a ? b : a)) { }, ], }, + + { + code: ` +declare const a: any; +declare const b: any; +a ? a : b; + `, + options: [ + { + ignorePrimitives: true, + }, + ], + }, + + { + code: ` +declare const a: any; +declare const b: any; +a ? a : b; + `, + options: [ + { + ignorePrimitives: { + number: true, + }, + }, + ], + }, + + { + code: ` +declare const a: unknown; +const b = a || 'bar'; + `, + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: false, + string: false, + }, + }, + ], + }, ], + invalid: [ ...nullishTypeTest((nullish, type, equals) => ({ code: ` @@ -5078,6 +5062,51 @@ defaultBoxOptional.a?.b ?? getFallbackBox(); }, { code: ` +declare const x: any; +declare const y: any; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: any; +declare const y: any; +x ?? y; + `, + }, + ], + }, + ], + }, + + { + code: ` +declare const x: unknown; +declare const y: any; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: unknown; +declare const y: any; +x ?? y; + `, + }, + ], + }, + ], + }, + { + code: ` interface Box { value: string; } @@ -5308,5 +5337,71 @@ defaultBoxOptional.a?.b ?? getFallbackBox(); options: [{ ignoreTernaryTests: false }], output: null, }, + { + code: ` +declare let x: unknown; +declare let y: number; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: unknown; +declare let y: number; +x ?? y; + `, + }, + ], + }, + ], + }, + + { + code: ` +declare let x: unknown; +declare let y: number; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: unknown; +declare let y: number; +x ?? y; + `, + }, + ], + }, + ], + }, + + { + code: ` +declare let x: { n: unknown }; +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: { n: unknown }; +x.n ?? y; + `, + }, + ], + }, + ], + }, ], });