diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md index d2fc95f4066a..146e96a19fa3 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md @@ -131,6 +131,29 @@ a ?? (b && c && d); **_NOTE:_** Errors for this specific case will be presented as suggestions (see below), instead of fixes. This is because it is not always safe to automatically convert `||` to `??` within a mixed logical expression, as we cannot tell the intended precedence of the operator. Note that by design, `??` requires parentheses when used with `&&` or `||` in the same expression. +### `ignorePrimitives` + +If you would like to ignore certain primitive types that can be falsy then you may pass an object containing a boolean value for each primitive: + +- `string: true`, ignores `null` or `undefined` unions with `string` (default: false). +- `number: true`, ignores `null` or `undefined` unions with `number` (default: false). +- `bigint: true`, ignores `null` or `undefined` unions with `bigint` (default: false). +- `boolean: true`, ignores `null` or `undefined` unions with `boolean` (default: false). + +Incorrect code for `ignorePrimitives: { string: true }`, and correct code for `ignorePrimitives: { string: false }`: + +```ts +const foo: string | undefined = 'bar'; +foo || 'a string'; +``` + +Correct code for `ignorePrimitives: { string: true }`: + +```ts +const foo: string | undefined = 'bar'; +foo ?? 'a string'; +``` + ## When Not To Use It If you are not using TypeScript 3.7 (or greater), then you will not be able to use this rule, as the operator is not supported. diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 14157427efa0..5a1a33d386fb 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -7,10 +7,16 @@ import * as util from '../util'; export type Options = [ { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; ignoreConditionalTests?: boolean; - ignoreTernaryTests?: boolean; ignoreMixedLogicalExpressions?: boolean; - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; + ignorePrimitives?: { + bigint?: boolean; + boolean?: boolean; + number?: boolean; + string?: boolean; + }; + ignoreTernaryTests?: boolean; }, ]; @@ -44,16 +50,25 @@ export default util.createRule({ { type: 'object', properties: { - ignoreConditionalTests: { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { type: 'boolean', }, - ignoreTernaryTests: { + ignoreConditionalTests: { type: 'boolean', }, ignoreMixedLogicalExpressions: { type: 'boolean', }, - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { + ignorePrimitives: { + type: 'object', + properties: { + bigint: { type: 'boolean' }, + boolean: { type: 'boolean' }, + number: { type: 'boolean' }, + string: { type: 'boolean' }, + }, + }, + ignoreTernaryTests: { type: 'boolean', }, }, @@ -63,20 +78,27 @@ export default util.createRule({ }, defaultOptions: [ { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, ignoreConditionalTests: true, ignoreTernaryTests: true, ignoreMixedLogicalExpressions: true, - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + ignorePrimitives: { + bigint: false, + boolean: false, + number: false, + string: false, + }, }, ], create( context, [ { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, ignoreConditionalTests, - ignoreTernaryTests, ignoreMixedLogicalExpressions, - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, + ignorePrimitives, + ignoreTernaryTests, }, ], ) { @@ -279,6 +301,22 @@ export default util.createRule({ return; } + const ignorableFlags = [ + ignorePrimitives!.bigint && ts.TypeFlags.BigInt, + ignorePrimitives!.boolean && ts.TypeFlags.BooleanLiteral, + ignorePrimitives!.number && ts.TypeFlags.Number, + ignorePrimitives!.string && ts.TypeFlags.String, + ] + .filter((flag): flag is number => flag !== undefined) + .reduce((previous, flag) => previous | flag, 0); + if ( + (type as ts.UnionOrIntersectionType).types.some(t => + tsutils.isTypeFlagSet(t, ignorableFlags), + ) + ) { + return; + } + const barBarOperator = util.nullThrows( sourceCode.getTokenAfter( node.left, 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 49e50e741a89..a2f91d436aaa 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -20,6 +20,7 @@ const ruleTester = new RuleTester({ const types = ['string', 'number', 'boolean', 'object']; const nullishTypes = ['null', 'undefined', 'null | undefined']; +const ignorablePrimitiveTypes = ['string', 'number', 'boolean', 'bigint']; function typeValidTest( cb: (type: string) => TSESLint.ValidTestCase | string, @@ -206,6 +207,13 @@ a && b || c || d; `, options: [{ ignoreMixedLogicalExpressions: true }], })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare const x: ${type} | undefined; +x || y; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), ], invalid: [ ...nullishTypeInvalidTest((nullish, type) => ({ @@ -751,5 +759,453 @@ declare const c: ${type}; }, ], })), + // default for missing option + { + code: ` +declare const x: string | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { number: true, boolean: true, bigint: true }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: number | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { string: true, boolean: true, bigint: true }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: boolean | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { string: true, number: true, bigint: true }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: bigint | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { string: true, number: true, boolean: true }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + // falsy + { + code: ` +declare const x: '' | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: false, + number: true, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: \`\` | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: false, + number: true, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 0 | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: false, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 0n | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: true, + bigint: false, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: false | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: false, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + // truthy + { + code: ` +declare const x: 'a' | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: false, + number: true, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: \`hello\${'string'}\` | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: false, + number: true, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 1 | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: false, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 1n | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: true, + bigint: false, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: true | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: false, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + // Unions of same primitive + { + code: ` +declare const x: 'a' | 'b' | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: false, + number: true, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 'a' | \`b\` | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: false, + number: true, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 0 | 1 | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: false, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 1 | 2 | 3 | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: false, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 0n | 1n | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: true, + bigint: false, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 1n | 2n | 3n | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: true, + bigint: false, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: true | false | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: false, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + // Mixed unions + { + code: ` +declare const x: 0 | 1 | 0n | 1n | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: false, + boolean: true, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 0 | 1 | 0n | 1n | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: true, + bigint: false, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 0 | 1 | 0n | 1n | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: false, + boolean: true, + bigint: false, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: true | false | null | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + string: true, + number: true, + boolean: false, + bigint: true, + }, + }, + ], + errors: [{ messageId: 'preferNullishOverOr' }], + }, + { + code: ` +declare const x: 0 | 'foo' | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + number: true, + string: true, + }, + }, + ], + errors: [ + { + messageId: 'preferNullishOverOr', + }, + ], + }, + { + code: ` +declare const x: 0 | 'foo' | undefined; +x || y; + `, + options: [ + { + ignorePrimitives: { + number: true, + string: false, + }, + }, + ], + errors: [ + { + messageId: 'preferNullishOverOr', + }, + ], + }, ], });