diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx b/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx index 777c80068e69..6fa0720ba2c3 100644 --- a/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx @@ -265,6 +265,37 @@ thing?.toString(); +### `checkVoid` + +{/* insert option description */} + +Examples of code for this rule with `{ checkVoid: true }`: + + + + +```ts option='{ "checkVoid": true }' +declare const thing: { + method: undefined | (() => void); +}; + +thing.method && thing.method(); +``` + + + + +```ts option='{ "checkVoid": true }' +declare const thing: { + method: undefined | (() => void); +}; + +thing.method?.(); +``` + + + + ### `requireNullish` {/* insert option description */} diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts index de1147b54447..c799268477ee 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts @@ -10,5 +10,6 @@ export interface PreferOptionalChainOptions { checkNumber?: boolean; checkString?: boolean; checkUnknown?: boolean; + checkVoid?: boolean; requireNullish?: boolean; } diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 174f8982cad8..ab6f90b121a9 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -59,8 +59,6 @@ export interface InvalidOperand { type: OperandValidity.Invalid; } type Operand = InvalidOperand | ValidOperand; - -const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( node: TSESTree.Node, disallowFalseyLiteral: boolean, @@ -92,26 +90,30 @@ function isValidFalseBooleanCheckType( return false; } - let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object; - if (options.checkAny === true) { - allowedFlags |= ts.TypeFlags.Any; + let flagsToExcludeFromCheck = 0; + if (options.checkAny !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.Any; + } + if (options.checkUnknown !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.Unknown; } - if (options.checkUnknown === true) { - allowedFlags |= ts.TypeFlags.Unknown; + if (options.checkString !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.StringLike; } - if (options.checkString === true) { - allowedFlags |= ts.TypeFlags.StringLike; + if (options.checkNumber !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.NumberLike; } - if (options.checkNumber === true) { - allowedFlags |= ts.TypeFlags.NumberLike; + if (options.checkBoolean !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.BooleanLike; } - if (options.checkBoolean === true) { - allowedFlags |= ts.TypeFlags.BooleanLike; + if (options.checkBigInt !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.BigIntLike; } - if (options.checkBigInt === true) { - allowedFlags |= ts.TypeFlags.BigIntLike; + if (options.checkVoid !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.Void; } - return types.every(t => isTypeFlagSet(t, allowedFlags)); + + return types.every(t => !isTypeFlagSet(t, flagsToExcludeFromCheck)); } export function gatherLogicalOperands( diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 241f4240e78d..ba58a314f75a 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -82,6 +82,11 @@ export default createRule< description: 'Check operands that are typed as `unknown` when inspecting "loose boolean" operands.', }, + checkVoid: { + type: 'boolean', + description: + 'Check operands that are typed as `void` when inspecting "loose boolean" operands.', + }, requireNullish: { type: 'boolean', description: @@ -100,6 +105,7 @@ export default createRule< checkNumber: true, checkString: true, checkUnknown: true, + checkVoid: true, requireNullish: false, }, ], diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot index 8607ddca5bca..032898689ebb 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot @@ -149,6 +149,25 @@ declare const thing: bigint; thing?.toString(); +Incorrect +Options: { "checkVoid": true } + +declare const thing: { + method: undefined | (() => void); +}; + +thing.method && thing.method(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Prefer using an optional chain expression instead, as it's more concise and easier to read. + +Correct +Options: { "checkVoid": true } + +declare const thing: { + method: undefined | (() => void); +}; + +thing.method?.(); + Incorrect Options: { "requireNullish": true } diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index c05f6218b24c..caf2a232b554 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1680,6 +1680,48 @@ describe('hand-crafted cases', () => { ], output: 'a?.prop;', }, + // check void + { + code: ` +declare const foo: { + method: undefined | (() => void); +}; +foo.method && foo.method(); + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` +declare const foo: { + method: undefined | (() => void); +}; +foo.method?.(); + `, + }, + // Exclude for everything else, an error occurs + { + code: noFormat`declare const foo: { x: { y: string } } | null; foo && foo.x;`, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: `declare const foo: { x: { y: string } } | null; foo?.x;`, + }, + ], + }, + ], + options: [ + { + checkAny: false, + checkBigInt: false, + checkBoolean: false, + checkNumber: false, + checkString: false, + checkUnknown: false, + checkVoid: false, + }, + ], + }, ], valid: [ '!a || !b;', @@ -1878,6 +1920,15 @@ describe('hand-crafted cases', () => { `, options: [{ checkUnknown: false }], }, + { + code: ` +declare const foo: { + method: undefined | (() => void); +}; +foo.method && foo.method(); + `, + options: [{ checkVoid: false }], + }, '(x = {}) && (x.y = true) != null && x.y.toString();', "('x' as `${'x'}`) && ('x' as `${'x'}`).length;", '`x` && `x`.length;', diff --git a/packages/eslint-plugin/tests/schema-snapshots/prefer-optional-chain.shot b/packages/eslint-plugin/tests/schema-snapshots/prefer-optional-chain.shot index 7d300bdbdfdd..8ec3cc46b7b5 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/prefer-optional-chain.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/prefer-optional-chain.shot @@ -33,6 +33,10 @@ "description": "Check operands that are typed as `unknown` when inspecting \"loose boolean\" operands.", "type": "boolean" }, + "checkVoid": { + "description": "Check operands that are typed as `void` when inspecting \"loose boolean\" operands.", + "type": "boolean" + }, "requireNullish": { "description": "Skip operands that are not typed with `null` and/or `undefined` when inspecting \"loose boolean\" operands.", "type": "boolean" @@ -61,6 +65,8 @@ type Options = [ checkString?: boolean; /** Check operands that are typed as `unknown` when inspecting "loose boolean" operands. */ checkUnknown?: boolean; + /** Check operands that are typed as `void` when inspecting "loose boolean" operands. */ + checkVoid?: boolean; /** Skip operands that are not typed with `null` and/or `undefined` when inspecting "loose boolean" operands. */ requireNullish?: boolean; },