From aec9ef56aec63c6de4cc5fc8b9190dfb84ddd4f0 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 16:11:29 +0900 Subject: [PATCH 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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;', From b3bf6b7298c60eaaced163d7f09eedec6511888a Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 12 Jul 2025 21:40:39 +0900 Subject: [PATCH 10/20] test: add test cases --- .../prefer-optional-chain.test.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) 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 caf2a232b554..aeef33d21238 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 @@ -1683,17 +1683,26 @@ describe('hand-crafted cases', () => { // check void { code: ` -declare const foo: { - method: undefined | (() => void); -}; -foo.method && foo.method(); +declare const foo: void; +foo && foo(); `, errors: [{ messageId: 'preferOptionalChain' }], output: ` -declare const foo: { - method: undefined | (() => void); -}; -foo.method?.(); +declare const foo: void; +foo?.(); + `, + }, + // check flag with obj + { + code: ` + declare const x: { y: boolean }; + x && x.y; + `, + errors: [{ messageId: 'preferOptionalChain' }], + options: [{ checkBoolean: false }], + output: ` + declare const x: { y: boolean }; + x?.y; `, }, // Exclude for everything else, an error occurs @@ -1899,6 +1908,13 @@ foo.method?.(); `, options: [{ checkBoolean: false }], }, + { + code: ` + declare const x: { y: boolean }; + x?.y && x?.y.length; + `, + options: [{ checkBoolean: false }], + }, { code: ` declare const x: number; @@ -1922,10 +1938,8 @@ foo.method?.(); }, { code: ` -declare const foo: { - method: undefined | (() => void); -}; -foo.method && foo.method(); +declare const foo: void; +foo && foo.method(); `, options: [{ checkVoid: false }], }, From b0adaa81139f03d798218f8ecf97239bd21511a5 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 12 Jul 2025 22:32:12 +0900 Subject: [PATCH 11/20] fix: ignore isValidFalseBooleanCheckType if there is no more op --- .../gatherLogicalOperands.ts | 16 +++++++++------- .../prefer-optional-chain.test.ts | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 10 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 ab6f90b121a9..1d0c6aa0e56d 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 @@ -250,12 +250,13 @@ export function gatherLogicalOperands( case AST_NODE_TYPES.UnaryExpression: if ( operand.operator === '!' && - isValidFalseBooleanCheckType( - operand.argument, - areMoreOperands && node.operator === '||', - parserServices, - options, - ) + (!areMoreOperands || + isValidFalseBooleanCheckType( + operand.argument, + node.operator === '||', + parserServices, + options, + )) ) { result.push({ comparedName: operand.argument, @@ -276,9 +277,10 @@ export function gatherLogicalOperands( default: if ( + !areMoreOperands || isValidFalseBooleanCheckType( operand, - areMoreOperands && node.operator === '&&', + node.operator === '&&', parserServices, options, ) 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 aeef33d21238..6e1f61693467 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 @@ -1698,12 +1698,21 @@ foo?.(); declare const x: { y: boolean }; x && x.y; `, - errors: [{ messageId: 'preferOptionalChain' }], - options: [{ checkBoolean: false }], - output: ` + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` declare const x: { y: boolean }; x?.y; `, + }, + ], + }, + ], + options: [{ checkBoolean: false }], }, // Exclude for everything else, an error occurs { From 2559e548991a4535f0c4a03c0de952cb581c0bdd Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 12 Jul 2025 22:52:35 +0900 Subject: [PATCH 12/20] feat: remove checkVoid option --- .../PreferOptionalChainOptions.ts | 1 - .../prefer-optional-chain-utils/gatherLogicalOperands.ts | 3 --- packages/eslint-plugin/src/rules/prefer-optional-chain.ts | 6 ------ .../prefer-optional-chain/prefer-optional-chain.test.ts | 8 -------- .../tests/schema-snapshots/prefer-optional-chain.shot | 6 ------ 5 files changed, 24 deletions(-) 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 c799268477ee..de1147b54447 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,6 +10,5 @@ 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 1d0c6aa0e56d..12be2cfc57d1 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 @@ -109,9 +109,6 @@ function isValidFalseBooleanCheckType( if (options.checkBigInt !== true) { flagsToExcludeFromCheck |= ts.TypeFlags.BigIntLike; } - if (options.checkVoid !== true) { - flagsToExcludeFromCheck |= ts.TypeFlags.Void; - } return types.every(t => !isTypeFlagSet(t, flagsToExcludeFromCheck)); } diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index ba58a314f75a..241f4240e78d 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -82,11 +82,6 @@ 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: @@ -105,7 +100,6 @@ export default createRule< checkNumber: true, checkString: true, checkUnknown: true, - checkVoid: true, requireNullish: false, }, ], 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 6e1f61693467..3a1b3199d11f 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 @@ -1736,7 +1736,6 @@ foo?.(); checkNumber: false, checkString: false, checkUnknown: false, - checkVoid: false, }, ], }, @@ -1945,13 +1944,6 @@ foo?.(); `, options: [{ checkUnknown: false }], }, - { - code: ` -declare const foo: void; -foo && 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 8ec3cc46b7b5..7d300bdbdfdd 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/prefer-optional-chain.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/prefer-optional-chain.shot @@ -33,10 +33,6 @@ "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" @@ -65,8 +61,6 @@ 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 9fa30d7f137bc2b1d3770db9937abf5cc7537e2e Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 12 Jul 2025 23:07:24 +0900 Subject: [PATCH 13/20] docs: remove checkVoid on docs --- .../docs/rules/prefer-optional-chain.mdx | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx b/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx index 6fa0720ba2c3..777c80068e69 100644 --- a/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx @@ -265,37 +265,6 @@ 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 1ef4c51f2199453484895983e6a3f8dc58ac61c0 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 12 Jul 2025 23:36:23 +0900 Subject: [PATCH 14/20] fix: reupdate snapshot --- .../prefer-optional-chain.shot | 19 ------------------- 1 file changed, 19 deletions(-) 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 032898689ebb..8607ddca5bca 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,25 +149,6 @@ 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 87107cdac33353b3656a3b0c5f0b7a9ffd267e83 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 16 Jul 2025 17:31:16 +0900 Subject: [PATCH 15/20] chore: revert gatherLogicalOperands.ts --- .../gatherLogicalOperands.ts | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 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 12be2cfc57d1..174f8982cad8 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,6 +59,8 @@ 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, @@ -90,27 +92,26 @@ function isValidFalseBooleanCheckType( return false; } - let flagsToExcludeFromCheck = 0; - if (options.checkAny !== true) { - flagsToExcludeFromCheck |= ts.TypeFlags.Any; + let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object; + if (options.checkAny === true) { + allowedFlags |= 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; } - - return types.every(t => !isTypeFlagSet(t, flagsToExcludeFromCheck)); + return types.every(t => isTypeFlagSet(t, allowedFlags)); } export function gatherLogicalOperands( @@ -247,13 +248,12 @@ export function gatherLogicalOperands( case AST_NODE_TYPES.UnaryExpression: if ( operand.operator === '!' && - (!areMoreOperands || - isValidFalseBooleanCheckType( - operand.argument, - node.operator === '||', - parserServices, - options, - )) + isValidFalseBooleanCheckType( + operand.argument, + areMoreOperands && node.operator === '||', + parserServices, + options, + ) ) { result.push({ comparedName: operand.argument, @@ -274,10 +274,9 @@ export function gatherLogicalOperands( default: if ( - !areMoreOperands || isValidFalseBooleanCheckType( operand, - node.operator === '&&', + areMoreOperands && node.operator === '&&', parserServices, options, ) From cba0f378bf15bff89583a2f3d2b62f0f475f70b5 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 16 Jul 2025 17:32:57 +0900 Subject: [PATCH 16/20] chore: revert perfer-optional-chain.test.ts --- .../prefer-optional-chain.test.ts | 66 ------------------- 1 file changed, 66 deletions(-) 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 3a1b3199d11f..c05f6218b24c 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,65 +1680,6 @@ describe('hand-crafted cases', () => { ], output: 'a?.prop;', }, - // check void - { - code: ` -declare const foo: void; -foo && foo(); - `, - errors: [{ messageId: 'preferOptionalChain' }], - output: ` -declare const foo: void; -foo?.(); - `, - }, - // check flag with obj - { - code: ` - declare const x: { y: boolean }; - x && x.y; - `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: [ - { - messageId: 'optionalChainSuggest', - output: ` - declare const x: { y: boolean }; - x?.y; - `, - }, - ], - }, - ], - options: [{ checkBoolean: false }], - }, - // 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, - }, - ], - }, ], valid: [ '!a || !b;', @@ -1916,13 +1857,6 @@ foo?.(); `, options: [{ checkBoolean: false }], }, - { - code: ` - declare const x: { y: boolean }; - x?.y && x?.y.length; - `, - options: [{ checkBoolean: false }], - }, { code: ` declare const x: number; From 433e6d479d53c921c3d047c2313d734f3ca30bdf Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 16 Jul 2025 17:35:41 +0900 Subject: [PATCH 17/20] fix: ignore isValidFalseBooleanCheckType if there is no more op --- .../gatherLogicalOperands.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 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 174f8982cad8..fdb6996c612d 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 @@ -248,12 +248,13 @@ export function gatherLogicalOperands( case AST_NODE_TYPES.UnaryExpression: if ( operand.operator === '!' && - isValidFalseBooleanCheckType( - operand.argument, - areMoreOperands && node.operator === '||', - parserServices, - options, - ) + (!areMoreOperands || + isValidFalseBooleanCheckType( + operand.argument, + node.operator === '||', + parserServices, + options, + )) ) { result.push({ comparedName: operand.argument, @@ -274,9 +275,10 @@ export function gatherLogicalOperands( default: if ( + !areMoreOperands || isValidFalseBooleanCheckType( operand, - areMoreOperands && node.operator === '&&', + node.operator === '&&', parserServices, options, ) From baf82db531984be08af90b56006c475ec02dd189 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 16 Jul 2025 17:52:42 +0900 Subject: [PATCH 18/20] test: add test case --- .../prefer-optional-chain.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 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..a3d395e3cd37 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,46 @@ describe('hand-crafted cases', () => { ], output: 'a?.prop;', }, + { + code: ` +declare const foo: { + bar: undefined | (() => void); +}; + +foo.bar && foo.bar(); + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` +declare const foo: { + bar: undefined | (() => void); +}; + +foo.bar?.(); + `, + }, + { + code: ` +declare const foo: { bar: string }; + +const baz = foo && foo.bar; + `, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` +declare const foo: { bar: string }; + +const baz = foo?.bar; + `, + }, + ], + }, + ], + options: [{ checkString: false }], + }, ], valid: [ '!a || !b;', @@ -1917,6 +1957,10 @@ describe('hand-crafted cases', () => { !x || x.a; `, "typeof globalThis !== 'undefined' && globalThis.Array();", + ` + declare const x: void; + x && x(); + `, ], }); }); From 6095671e6a06c2f76e11d9f94d0991adc8579b8b Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Mon, 21 Jul 2025 01:47:58 -0600 Subject: [PATCH 19/20] Update packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts --- .../rules/prefer-optional-chain/prefer-optional-chain.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a3d395e3cd37..3cd9425ff9bf 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 @@ -1958,7 +1958,8 @@ const baz = foo?.bar; `, "typeof globalThis !== 'undefined' && globalThis.Array();", ` - declare const x: void; + declare const x: void | (() => void); + x && x(); x && x(); `, ], From 6fafc2261cabb79253b05ced1e01cdb98ce17bf5 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Mon, 21 Jul 2025 01:51:02 -0600 Subject: [PATCH 20/20] Update prefer-optional-chain.test.ts --- .../rules/prefer-optional-chain/prefer-optional-chain.test.ts | 1 - 1 file changed, 1 deletion(-) 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 3cd9425ff9bf..12c73b04eea7 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 @@ -1960,7 +1960,6 @@ const baz = foo?.bar; ` declare const x: void | (() => void); x && x(); - x && x(); `, ], });