From aec9ef56aec63c6de4cc5fc8b9190dfb84ddd4f0 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 16:11:29 +0900 Subject: [PATCH 1/9] feat: add check void option --- .../PreferOptionalChainOptions.ts | 1 + packages/eslint-plugin/src/rules/prefer-optional-chain.ts | 6 ++++++ 2 files changed, 7 insertions(+) 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.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index ffbcd213fedd..670c5e9c4713 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, }, ], From e35a6f28d02d484d4a962ff7c328f4923bad5c20 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 16:11:42 +0900 Subject: [PATCH 2/9] test: add test case for check void --- .../prefer-optional-chain.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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..936268ace96d 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,22 @@ 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?.(); + `, + }, ], valid: [ '!a || !b;', From eacc6225bf1711a9d0a7f21be3edebd8921e3d38 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 16:25:59 +0900 Subject: [PATCH 3/9] feat: check void type --- .../prefer-optional-chain-utils/gatherLogicalOperands.ts | 5 +++++ 1 file changed, 5 insertions(+) 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..4e8ae6244ea1 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 @@ -111,6 +111,11 @@ function isValidFalseBooleanCheckType( if (options.checkBigInt === true) { allowedFlags |= ts.TypeFlags.BigIntLike; } + + if (options.checkVoid === true) { + allowedFlags |= ts.TypeFlags.Void; + } + return types.every(t => isTypeFlagSet(t, allowedFlags)); } From 88decc1ebd0e51be701f774e2b0425c5164a7b04 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 16:32:35 +0900 Subject: [PATCH 4/9] chore: add snapshot --- .../tests/schema-snapshots/prefer-optional-chain.shot | 6 ++++++ 1 file changed, 6 insertions(+) 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; }, From 4171c5e4f536824127cd5b49a6818b8ed6f43b79 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 16:51:48 +0900 Subject: [PATCH 5/9] docs: add checkvoid section --- .../docs/rules/prefer-optional-chain.mdx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 */} From 791461b8f14991f41acfeae54b9167323ab83c20 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 17:11:22 +0900 Subject: [PATCH 6/9] chore: update snapshot --- .../prefer-optional-chain.shot | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 } From b3d2a4fc9dc7b8f310d32f234afa67bbb924543b Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 7 Jun 2025 17:22:41 +0900 Subject: [PATCH 7/9] refactor: inject option based on flagsToExcludeFromCheck --- .../gatherLogicalOperands.ts | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) 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 4e8ae6244ea1..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,31 +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) { - allowedFlags |= ts.TypeFlags.Unknown; + if (options.checkUnknown !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.Unknown; } - if (options.checkString === true) { - allowedFlags |= ts.TypeFlags.StringLike; + if (options.checkString !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.StringLike; } - if (options.checkNumber === true) { - allowedFlags |= ts.TypeFlags.NumberLike; + if (options.checkNumber !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.NumberLike; } - if (options.checkBoolean === true) { - allowedFlags |= ts.TypeFlags.BooleanLike; + if (options.checkBoolean !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.BooleanLike; } - if (options.checkBigInt === true) { - allowedFlags |= ts.TypeFlags.BigIntLike; + if (options.checkBigInt !== true) { + flagsToExcludeFromCheck |= ts.TypeFlags.BigIntLike; } - - if (options.checkVoid === true) { - allowedFlags |= ts.TypeFlags.Void; + 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( From 656ec507805e8d67a23653389de4c93986ca7f48 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 7 Jun 2025 17:23:05 +0900 Subject: [PATCH 8/9] test: add test for checkVoid = false --- .../prefer-optional-chain/prefer-optional-chain.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) 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 936268ace96d..dd68bcfd6ac1 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 @@ -1894,6 +1894,15 @@ foo.method?.(); `, 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;', From 4b92ad4340a6128d29f50f9b92fc68baf645a794 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 12 Jun 2025 17:20:44 +0900 Subject: [PATCH 9/9] test: add tc for all checks are excluded but invaild --- .../prefer-optional-chain.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 dd68bcfd6ac1..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 @@ -1696,6 +1696,32 @@ declare const foo: { 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;',