From cfdd53614ab3237d39aa32615168fe1819bca15c Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 14:44:01 +0900 Subject: [PATCH 01/22] test: add default test --- .../prefer-optional-chain.test.ts | 489 ++++++++++++++++++ 1 file changed, 489 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 b72289d020b2..7d9fba724763 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 @@ -698,6 +698,495 @@ describe('|| {}', () => { }); }); +describe('chain ending with comparison', () => { + ruleTester.run('prefer-optional-chain', rule, { + valid: [ + 'foo && foo.bar == x', + 'foo && foo.bar == null', + 'foo && foo.bar == undefined', + 'foo && foo.bar === x', + 'foo && foo.bar === undefined', + 'foo && foo.bar !== 0', + 'foo && foo.bar !== 1', + 'foo && foo.bar !== "123"', + 'foo && foo.bar !== {}', + 'foo && foo.bar !== false', + 'foo && foo.bar !== true', + 'foo && foo.bar !== null', + 'foo && foo.bar !== x', + 'foo != null && foo.bar == x', + 'foo != null && foo.bar == null', + 'foo != null && foo.bar == undefined', + 'foo != null && foo.bar === x', + 'foo != null && foo.bar === undefined', + 'foo != null && foo.bar !== 0', + 'foo != null && foo.bar !== 1', + 'foo != null && foo.bar !== "123"', + 'foo != null && foo.bar !== {}', + 'foo != null && foo.bar !== false', + 'foo != null && foo.bar !== true', + 'foo != null && foo.bar !== null', + 'foo != null && foo.bar !== x', + '!foo && foo.bar == 0;', + '!foo && foo.bar == 1;', + '!foo && foo.bar == "123";', + '!foo && foo.bar == {};', + '!foo && foo.bar == false;', + '!foo && foo.bar == true;', + '!foo && foo.bar === 0;', + '!foo && foo.bar === 1;', + '!foo && foo.bar === "123";', + '!foo && foo.bar === {};', + '!foo && foo.bar === false;', + '!foo && foo.bar === true;', + '!foo && foo.bar === null;', + '!foo && foo.bar !== undefined;', + 'foo == null && foo.bar == 0;', + 'foo == null && foo.bar == 1;', + 'foo == null && foo.bar == "123";', + 'foo == null && foo.bar == {};', + 'foo == null && foo.bar == false;', + 'foo == null && foo.bar == true;', + 'foo == null && foo.bar === 0;', + 'foo == null && foo.bar === 1;', + 'foo == null && foo.bar === "123";', + 'foo == null && foo.bar === {};', + 'foo == null && foo.bar === false;', + 'foo == null && foo.bar === true;', + 'foo == null && foo.bar === null;', + 'foo == null && foo.bar !== undefined;', + ], + invalid: [ + { + code: `foo && foo.bar == 0;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == 0;`, + }, + { + code: `foo && foo.bar == 1;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == 1;`, + }, + { + code: `foo && foo.bar == "123";`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == "123";`, + }, + { + code: `foo && foo.bar == {};`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == {};`, + }, + { + code: `foo && foo.bar == false;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == false;`, + }, + { + code: `foo && foo.bar == true;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == true;`, + }, + { + code: `foo && foo.bar === 0;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === 0;`, + }, + { + code: `foo && foo.bar === 1;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === 1;`, + }, + { + code: `foo && foo.bar === "123";`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === "123";`, + }, + { + code: `foo && foo.bar === {};`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === {};`, + }, + { + code: `foo && foo.bar === false;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === false;`, + }, + { + code: `foo && foo.bar === true;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === true;`, + }, + { + code: `foo && foo.bar === null;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === null;`, + }, + { + code: `foo && foo.bar !== undefined;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== undefined;`, + }, + { + code: `foo != null && foo.bar == 0;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == 0;`, + }, + { + code: `foo != null && foo.bar == 1;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == 1;`, + }, + { + code: `foo != null && foo.bar == "123";`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == "123";`, + }, + { + code: `foo != null && foo.bar == {};`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == {};`, + }, + { + code: `foo != null && foo.bar == false;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == false;`, + }, + { + code: `foo != null && foo.bar == true;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == true;`, + }, + { + code: `foo != null && foo.bar === 0;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === 0;`, + }, + { + code: `foo != null && foo.bar === 1;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === 1;`, + }, + { + code: `foo != null && foo.bar === "123";`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === "123";`, + }, + { + code: `foo != null && foo.bar === {};`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === {};`, + }, + { + code: `foo != null && foo.bar === false;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === false;`, + }, + { + code: `foo != null && foo.bar === true;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === true;`, + }, + { + code: `foo != null && foo.bar === null;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === null;`, + }, + { + code: `foo != null && foo.bar !== undefined;`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== undefined;`, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar == x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar == null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar == undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar === x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar === undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== 0; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 0; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== 1; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 1; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== "123"; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== "123"; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== {}; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== {}; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== false; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== false; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== true; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== true; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar !== x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar == x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar == null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar == undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar === x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar === undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== 0; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 0; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== 1; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 1; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== "123"; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== "123"; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== {}; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== {}; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== false; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== false; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== true; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== true; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar !== x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== x; + `, + }, + ], + }); +}); + describe('hand-crafted cases', () => { ruleTester.run('prefer-optional-chain', rule, { invalid: [ From 33e6c710bf0f0dcc29384a89eee57927d60ef148 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 15:06:24 +0900 Subject: [PATCH 02/22] feat: comparison operators at the end of optional chains --- .../analyzeChain.ts | 107 ++++++++++++++++-- .../gatherLogicalOperands.ts | 97 ++++++++++++++-- .../src/rules/prefer-optional-chain.ts | 11 ++ 3 files changed, 194 insertions(+), 21 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index c4364765f92c..c3ba545eff77 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -10,10 +10,10 @@ import type { } from '@typescript-eslint/utils/ts-eslint'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { unionConstituents } from 'ts-api-utils'; +import { isFalsyType, unionConstituents } from 'ts-api-utils'; import * as ts from 'typescript'; -import type { ValidOperand } from './gatherLogicalOperands'; +import type { LastChainOperand, ValidOperand } from './gatherLogicalOperands'; import type { PreferOptionalChainMessageIds, PreferOptionalChainOptions, @@ -31,7 +31,11 @@ import { } from '../../util'; import { checkNullishAndReport } from './checkNullishAndReport'; import { compareNodes, NodeComparisonResult } from './compareNodes'; -import { NullishComparisonType } from './gatherLogicalOperands'; +import { + ComparisonType, + NullishComparisonType, + OperandValidity, +} from './gatherLogicalOperands'; function includesType( parserServices: ParserServicesWithTypeInformation, @@ -48,6 +52,66 @@ function includesType( return false; } +function isTruthyOperand( + comparedName: TSESTree.Node, + nullishComparisonType: NullishComparisonType | ComparisonType, + parserServices: ParserServicesWithTypeInformation, +): boolean { + let ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + const comparedNameType = parserServices.getTypeAtLocation(comparedName); + + if (isTypeFlagSet(comparedNameType, ANY_UNKNOWN_FLAGS)) { + return false; + } + switch (nullishComparisonType) { + case NullishComparisonType.Boolean: + const types = unionConstituents(comparedNameType); + return types.every(type => !isFalsyType(type)); + case NullishComparisonType.NotStrictEqualUndefined: + return !isTypeFlagSet(comparedNameType, ts.TypeFlags.Undefined); + case NullishComparisonType.NotStrictEqualNull: + return !isTypeFlagSet(comparedNameType, ts.TypeFlags.Null); + case NullishComparisonType.NotEqualNullOrUndefined: + return !isTypeFlagSet( + comparedNameType, + ts.TypeFlags.Null | ts.TypeFlags.Undefined, + ); + default: + return false; + } +} + +function isValidLastChainOperand( + ComparisonValueType: TSESTree.Node, + operator: TSESTree.BinaryExpression['operator'], + parserServices: ParserServicesWithTypeInformation, +) { + const type = parserServices.getTypeAtLocation(ComparisonValueType); + let ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + + switch (operator) { + case '==': { + const types = unionConstituents(type); + const isNullish = types.some(t => + isTypeFlagSet( + t, + ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined, + ), + ); + return !isNullish; + } + case '===': { + const types = unionConstituents(type); + const isUndefined = types.some(t => + isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), + ); + return !isUndefined; + } + default: + return false; + } +} + // I hate that these functions are identical aside from the enum values used // I can't think of a good way to reuse the code here in a way that will preserve // the type safety and simplicity. @@ -66,14 +130,6 @@ const analyzeAndChainOperand: OperandAnalyzer = ( ) => { switch (operand.comparisonType) { case NullishComparisonType.Boolean: { - const nextOperand = chain.at(index + 1); - if ( - nextOperand?.comparisonType === - NullishComparisonType.NotStrictEqualNull && - operand.comparedName.type === AST_NODE_TYPES.Identifier - ) { - return null; - } return [operand]; } @@ -521,10 +577,11 @@ export function analyzeChain( node: TSESTree.Node, operator: TSESTree.LogicalExpression['operator'], chain: ValidOperand[], + lastChainOperand?: LastChainOperand, ): void { // need at least 2 operands in a chain for it to be a chain if ( - chain.length <= 1 || + chain.length + (lastChainOperand ? 1 : 0) <= 1 || /* istanbul ignore next -- previous checks make this unreachable, but keep it for exhaustiveness check */ operator === '??' ) { @@ -624,7 +681,33 @@ export function analyzeChain( subChain.push(currentOperand); } } + const lastOperand = subChain.flat().at(-1); + + if (lastOperand && lastChainOperand) { + const comparisonResult = compareNodes( + lastOperand.comparedName, + lastChainOperand.comparedName, + ); + if ( + comparisonResult === NodeComparisonResult.Subset && + (isTruthyOperand( + lastOperand.comparedName, + lastOperand.comparisonType, + parserServices, + ) || + isValidLastChainOperand( + lastChainOperand.comparisonValue, + lastChainOperand.node.operator, + parserServices, + )) + ) { + subChain.push({ + ...lastChainOperand, + type: OperandValidity.Valid, + }); + } + } // check the leftovers maybeReportThenReset(); } 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 fdb6996c612d..0fa45a1698f0 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 @@ -25,6 +25,7 @@ const enum ComparisonValueType { } export const enum OperandValidity { Valid = 'Valid', + Last = 'Last', Invalid = 'Invalid', } export const enum NullishComparisonType { @@ -48,17 +49,31 @@ export const enum NullishComparisonType { /** `x` */ Boolean = 'Boolean', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum } +export const enum ComparisonType { + NotEqual = 'NotEqual', + Equal = 'Equal', + NotStrictEqual = 'NotStrictEqual', + StrictEqual = 'StrictEqual', +} export interface ValidOperand { comparedName: TSESTree.Node; - comparisonType: NullishComparisonType; + comparisonType: NullishComparisonType | ComparisonType; isYoda: boolean; node: TSESTree.Expression; type: OperandValidity.Valid; } +export interface LastChainOperand { + comparedName: TSESTree.Node; + comparisonType: ComparisonType; + comparisonValue: TSESTree.Node; + isYoda: boolean; + node: TSESTree.BinaryExpression; + type: OperandValidity.Last; +} export interface InvalidOperand { type: OperandValidity.Invalid; } -type Operand = InvalidOperand | ValidOperand; +type Operand = InvalidOperand | ValidOperand | LastChainOperand; const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( @@ -201,9 +216,6 @@ export function gatherLogicalOperands( }); continue; } - // x == something :( - result.push({ type: OperandValidity.Invalid }); - continue; case '!==': case '===': { @@ -232,11 +244,51 @@ export function gatherLogicalOperands( type: OperandValidity.Valid, }); continue; + } + } + } - default: - // x === something :( - result.push({ type: OperandValidity.Invalid }); - continue; + // x == something :( + // x === something :( + // x != something :( + // x !== something :( + const binaryComparisonChain = getBinaryComparisonChain(operand); + if (binaryComparisonChain) { + const { comparedName, comparedValue, isYoda } = binaryComparisonChain; + + switch (operand.operator) { + case '==': + case '===': { + const comparisonType = + operand.operator === '==' + ? ComparisonType.Equal + : ComparisonType.StrictEqual; + result.push({ + isYoda, + comparedName, + comparisonType: comparisonType, + type: OperandValidity.Last, + node: operand, + comparisonValue: comparedValue, + }); + continue; + } + + case '!=': + case '!==': { + const comparisonType = + operand.operator === '!=' + ? ComparisonType.NotEqual + : ComparisonType.NotStrictEqual; + result.push({ + isYoda, + comparedName, + comparisonType: comparisonType, + type: OperandValidity.Last, + node: operand, + comparisonValue: comparedValue, + }); + continue; } } } @@ -374,4 +426,31 @@ export function gatherLogicalOperands( return null; } + + function getBinaryComparisonChain(node: TSESTree.BinaryExpression) { + const { left, right } = node; + let isYoda = false; + const isLeftMemberExpression = + left.type === AST_NODE_TYPES.MemberExpression; + const isRightMemberExpression = + right.type === AST_NODE_TYPES.MemberExpression; + if (isLeftMemberExpression && !isRightMemberExpression) { + const [comparedName, comparedValue] = [left, right]; + return { + isYoda, + comparedName, + comparedValue, + }; + } else if (!isLeftMemberExpression && isRightMemberExpression) { + const [comparedName, comparedValue] = [right, left]; + + isYoda = true; + return { + isYoda, + comparedName, + comparedValue, + }; + } + return null; + } } diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 241f4240e78d..c3d61c0f0f70 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -138,6 +138,17 @@ export default createRule< currentChain, ); currentChain = []; + } else if (operand.type === OperandValidity.Last) { + analyzeChain( + context, + parserServices, + options, + node, + node.operator, + currentChain, + operand, + ); + currentChain = []; } else { currentChain.push(operand); } From 6bf2f641a306ec1be9a215729eb32a14cf7a95fa Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 15:08:15 +0900 Subject: [PATCH 03/22] fix : comparison operators with logical operators Ensure comparison operator consistency based on logical operator - When parent operator is `&&`, enforce `==` or `===` as the last operand operator. - When parent operator is `||`, enforce `!=` or `!==` as the last operand operator. --- .../gatherLogicalOperands.ts | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 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 0fa45a1698f0..d4dcf97da121 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 @@ -197,53 +197,55 @@ export function gatherLogicalOperands( continue; } - switch (operand.operator) { - case '!=': - case '==': - if ( - comparedValue === ComparisonValueType.Null || - comparedValue === ComparisonValueType.Undefined - ) { - // x == null, x == undefined - result.push({ - comparedName: comparedExpression, - comparisonType: operand.operator.startsWith('!') - ? NullishComparisonType.NotEqualNullOrUndefined - : NullishComparisonType.EqualNullOrUndefined, - isYoda, - node: operand, - type: OperandValidity.Valid, - }); - continue; - } - - case '!==': - case '===': { - const comparedName = comparedExpression; - switch (comparedValue) { - case ComparisonValueType.Null: + if (operand.operator.startsWith('!') !== (node.operator === '||')) { + switch (operand.operator) { + case '!=': + case '==': + if ( + comparedValue === ComparisonValueType.Null || + comparedValue === ComparisonValueType.Undefined + ) { + // x == null, x == undefined result.push({ - comparedName, + comparedName: comparedExpression, comparisonType: operand.operator.startsWith('!') - ? NullishComparisonType.NotStrictEqualNull - : NullishComparisonType.StrictEqualNull, + ? NullishComparisonType.NotEqualNullOrUndefined + : NullishComparisonType.EqualNullOrUndefined, isYoda, node: operand, type: OperandValidity.Valid, }); continue; + } - case ComparisonValueType.Undefined: - result.push({ - comparedName, - comparisonType: operand.operator.startsWith('!') - ? NullishComparisonType.NotStrictEqualUndefined - : NullishComparisonType.StrictEqualUndefined, - isYoda, - node: operand, - type: OperandValidity.Valid, - }); - continue; + case '!==': + case '===': { + const comparedName = comparedExpression; + switch (comparedValue) { + case ComparisonValueType.Null: + result.push({ + comparedName, + comparisonType: operand.operator.startsWith('!') + ? NullishComparisonType.NotStrictEqualNull + : NullishComparisonType.StrictEqualNull, + isYoda, + node: operand, + type: OperandValidity.Valid, + }); + continue; + + case ComparisonValueType.Undefined: + result.push({ + comparedName, + comparisonType: operand.operator.startsWith('!') + ? NullishComparisonType.NotStrictEqualUndefined + : NullishComparisonType.StrictEqualUndefined, + isYoda, + node: operand, + type: OperandValidity.Valid, + }); + continue; + } } } } From 3aa3b04dc16240317e55d8a2bd6a604c424147f1 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 15:16:38 +0900 Subject: [PATCH 04/22] fix : suggestion - Already check isTruthyOperand and isValidLastChainOperand so dowmstream error not occur when ComparisonType - And !==, != check not nullish --- .../analyzeChain.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index c3ba545eff77..588c2eb42de3 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -326,6 +326,10 @@ function getReportDescriptor( lastOperand.comparisonType === NullishComparisonType.StrictEqualUndefined || lastOperand.comparisonType === NullishComparisonType.NotStrictEqualUndefined || + lastOperand.comparisonType === ComparisonType.Equal || + lastOperand.comparisonType === ComparisonType.NotEqual || + lastOperand.comparisonType === ComparisonType.NotStrictEqual || + lastOperand.comparisonType === ComparisonType.StrictEqual || (operator === '||' && lastOperand.comparisonType === NullishComparisonType.NotBoolean) ) { @@ -652,6 +656,31 @@ export function analyzeChain( // ^^^^^^^ invalid OR chain logical, but still part of // the chain for combination purposes + if (lastOperand) { + const comparisonResult = compareNodes( + lastOperand.comparedName, + operand.comparedName, + ); + switch (operand.comparisonType) { + case NullishComparisonType.NotStrictEqualUndefined: { + if (comparisonResult === NodeComparisonResult.Subset) { + subChain.push(operand); + } + } + case NullishComparisonType.NotStrictEqualNull: { + if ( + comparisonResult === NodeComparisonResult.Subset && + isTruthyOperand( + lastOperand.comparedName, + lastOperand.comparisonType, + parserServices, + ) + ) { + subChain.push(operand); + } + } + } + } maybeReportThenReset(); continue; } From 51d956f54d340f476a94b4b7021008aa7aa3811b Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 17:51:58 +0900 Subject: [PATCH 05/22] fix : != null is last chain some occur in chain when != null is last chain https://github.com/typescript-eslint/typescript-eslint/issues/7654 --- .../analyzeChain.ts | 18 +++---- .../prefer-optional-chain.test.ts | 50 +++---------------- 2 files changed, 17 insertions(+), 51 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 588c2eb42de3..f7ceec398743 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -129,10 +129,7 @@ const analyzeAndChainOperand: OperandAnalyzer = ( chain, ) => { switch (operand.comparisonType) { - case NullishComparisonType.Boolean: { - return [operand]; - } - + case NullishComparisonType.Boolean: case NullishComparisonType.NotEqualNullOrUndefined: return [operand]; @@ -148,7 +145,8 @@ const analyzeAndChainOperand: OperandAnalyzer = ( return [operand, nextOperand]; } if ( - includesType( + nextOperand && + !includesType( parserServices, operand.comparedName, ts.TypeFlags.Undefined, @@ -157,10 +155,9 @@ const analyzeAndChainOperand: OperandAnalyzer = ( // we know the next operand is not an `undefined` check and that this // operand includes `undefined` - which means that making this an // optional chain would change the runtime behavior of the expression - return null; + return [operand]; } - - return [operand]; + return null; } case NullishComparisonType.NotStrictEqualUndefined: { @@ -676,7 +673,10 @@ export function analyzeChain( parserServices, ) ) { - subChain.push(operand); + subChain.push({ + ...operand, + comparisonType: ComparisonType.NotStrictEqual, + }); } } } 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 7d9fba724763..ef7eb690c0d8 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 @@ -1445,53 +1445,11 @@ describe('hand-crafted cases', () => { errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: '!foo.bar!.baz?.paz;', }, - { - code: ` - declare const foo: { bar: string } | null; - foo !== null && foo.bar !== null; - `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: [ - { - messageId: 'optionalChainSuggest', - output: ` - declare const foo: { bar: string } | null; - foo?.bar !== null; - `, - }, - ], - }, - ], - output: null, - }, { code: 'foo != null && foo.bar != null;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: 'foo?.bar != null;', }, - { - code: ` - declare const foo: { bar: string | null } | null; - foo != null && foo.bar !== null; - `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: [ - { - messageId: 'optionalChainSuggest', - output: ` - declare const foo: { bar: string | null } | null; - foo?.bar !== null; - `, - }, - ], - }, - ], - output: null, - }, { code: ` declare const foo: { bar: string | null } | null; @@ -2298,6 +2256,14 @@ const baz = foo?.bar; '(x || y) != null && (x || y).foo;', // TODO - should we handle this? '(await foo) && (await foo).bar;', + ` + declare const foo: { bar: string } | null; + foo !== null && foo.bar !== null; + `, + ` + declare const foo: { bar: string | null } | null; + foo != null && foo.bar !== null; + `, { code: ` declare const x: string; From 8c3de3a772e31191e5bca05883025fe51bd95ac0 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 18:02:55 +0900 Subject: [PATCH 06/22] text: change basecases a != null && a.b != null a?.b != null always same when a != null is true --- .../prefer-optional-chain.test.ts | 26 ++++++++++++++++++- 1 file changed, 25 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 ef7eb690c0d8..4601dd92b493 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 @@ -2443,6 +2443,7 @@ describe('base cases', () => { // with the `| null | undefined` type - `!== null` doesn't cover the // `undefined` case - so optional chaining is not a valid conversion valid: BaseCases({ + skipIds: [20, 26], mutateCode: c => c.replaceAll('&&', '!== null &&'), mutateOutput: identity, operator: '&&', @@ -2455,7 +2456,30 @@ describe('base cases', () => { mutateOutput: identity, operator: '&&', useSuggestionFixer: true, - }), + }).concat([ + { + code: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar !== null && foo.bar() !== null && foo.bar().baz !== null && foo.bar().baz.buzz !== null && foo.bar().baz.buzz(); + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar?.() !== null && foo.bar().baz !== null && foo.bar().baz.buzz !== null && foo.bar().baz.buzz(); + `, + }, + { + code: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar !== null && foo.bar?.() !== null && foo.bar?.().baz; + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar?.() !== null && foo.bar?.().baz; + `, + }, + ]), }); }); From 5987dfc9b010a5bd5ff13802b9bd368b3d1a8f16 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 18:11:44 +0900 Subject: [PATCH 07/22] test: change !== undefined base cases a !== undefined && a.b a?.b always same when a is not nullish --- .../prefer-optional-chain.test.ts | 26 ++++++++++++++++++- 1 file changed, 25 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 4601dd92b493..e8a0a1901ece 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 @@ -2500,6 +2500,7 @@ describe('base cases', () => { // with the `| null | undefined` type - `!== undefined` doesn't cover the // `null` case - so optional chaining is not a valid conversion valid: BaseCases({ + skipIds: [20, 26], mutateCode: c => c.replaceAll('&&', '!== undefined &&'), mutateOutput: identity, operator: '&&', @@ -2512,7 +2513,30 @@ describe('base cases', () => { mutateOutput: identity, operator: '&&', useSuggestionFixer: true, - }), + }).concat([ + { + code: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar !== undefined && foo.bar() !== undefined && foo.bar().baz !== undefined && foo.bar().baz.buzz !== undefined && foo.bar().baz.buzz(); + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar?.() !== undefined && foo.bar().baz !== undefined && foo.bar().baz.buzz !== undefined && foo.bar().baz.buzz(); + `, + }, + { + code: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar !== undefined && foo.bar?.() !== undefined && foo.bar?.().baz; + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar?.() !== undefined && foo.bar?.().baz; + `, + }, + ]), }); }); From bd5d289433a80a63cdaec8fa233df59f19ce2182 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 18:18:28 +0900 Subject: [PATCH 08/22] test: when !== undefined is last chain --- .../prefer-optional-chain/prefer-optional-chain.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 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 e8a0a1901ece..13ac97b033ff 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 @@ -1203,8 +1203,7 @@ describe('hand-crafted cases', () => { { code: 'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: - 'foo?.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', + output: 'foo?.bar?.baz !== undefined && foo.bar.baz.buzz;', }, { code: ` @@ -1215,8 +1214,7 @@ describe('hand-crafted cases', () => { `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - foo.bar?.baz != null && - foo.bar.baz.qux !== undefined && + foo.bar?.baz?.qux !== undefined && foo.bar.baz.qux.buzz; `, }, From 15b7fc812206f380b311a0397585b06d46e1ff9c Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 19:16:44 +0900 Subject: [PATCH 09/22] test: add some union case --- .../prefer-optional-chain.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 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 13ac97b033ff..a7b357a33628 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 @@ -755,6 +755,22 @@ describe('chain ending with comparison', () => { 'foo == null && foo.bar === true;', 'foo == null && foo.bar === null;', 'foo == null && foo.bar !== undefined;', + ` + declare const x: false | { a: string }; + x && x.a == x; + `, + ` + declare const x: '' | { a: string }; + x && x.a == x; + `, + ` + declare const x: 0 | { a: string }; + x && x.a == x; + `, + ` + declare const x: 0n | { a: string }; + x && x.a; + `, ], invalid: [ { @@ -1183,6 +1199,28 @@ describe('chain ending with comparison', () => { foo?.bar !== x; `, }, + { + code: ` + declare const foo: { bar: number } | 1; + foo && foo.bar == x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number } | 1; + foo?.bar == x; + `, + }, + { + code: ` + declare const foo: { bar: number } | 0; + foo != null && foo.bar == x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number } | 0; + foo?.bar == x; + `, + }, ], }); }); From 7ffa8031367f1f675a67bced5b1277e537c92271 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Tue, 26 Aug 2025 19:33:20 +0900 Subject: [PATCH 10/22] test: add base case in `||` operator --- .../prefer-optional-chain.test.ts | 480 ++++++++++++++++++ 1 file changed, 480 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 a7b357a33628..60c28c7f11a2 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 @@ -771,6 +771,60 @@ describe('chain ending with comparison', () => { declare const x: 0n | { a: string }; x && x.a; `, + '!foo || foo.bar != x', + '!foo || foo.bar != null', + '!foo || foo.bar != undefined', + '!foo || foo.bar === 0', + '!foo || foo.bar === 1', + '!foo || foo.bar === "123"', + '!foo || foo.bar === {}', + '!foo || foo.bar === false', + '!foo || foo.bar === true', + '!foo || foo.bar === null', + '!foo || foo.bar === x', + '!foo || foo.bar !== x', + '!foo || foo.bar !== undefined', + 'foo == null || foo.bar != x', + 'foo == null || foo.bar != null', + 'foo == null || foo.bar != undefined', + 'foo == null || foo.bar === 0', + 'foo == null || foo.bar === 1', + 'foo == null || foo.bar === "123"', + 'foo == null || foo.bar === {}', + 'foo == null || foo.bar === false', + 'foo == null || foo.bar === true', + 'foo == null || foo.bar === null', + 'foo == null || foo.bar === x', + 'foo == null || foo.bar !== x', + 'foo == null || foo.bar !== undefined', + 'foo || foo.bar != 0;', + 'foo || foo.bar != 1;', + 'foo || foo.bar != "123";', + 'foo || foo.bar != {};', + 'foo || foo.bar != false;', + 'foo || foo.bar != true;', + 'foo || foo.bar === undefined;', + 'foo || foo.bar !== 0;', + 'foo || foo.bar !== 1;', + 'foo || foo.bar !== "123";', + 'foo || foo.bar !== {};', + 'foo || foo.bar !== false;', + 'foo || foo.bar !== true;', + 'foo || foo.bar !== null;', + 'foo != null || foo.bar != 0;', + 'foo != null || foo.bar != 1;', + 'foo != null || foo.bar != "123";', + 'foo != null || foo.bar != {};', + 'foo != null || foo.bar != false;', + 'foo != null || foo.bar != true;', + 'foo != null || foo.bar === undefined;', + 'foo != null || foo.bar !== 0;', + 'foo != null || foo.bar !== 1;', + 'foo != null || foo.bar !== "123";', + 'foo != null || foo.bar !== {};', + 'foo != null || foo.bar !== false;', + 'foo != null || foo.bar !== true;', + 'foo != null || foo.bar !== null;', ], invalid: [ { @@ -1221,6 +1275,432 @@ describe('chain ending with comparison', () => { foo?.bar == x; `, }, + { + code: `!foo || foo.bar != 0`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != 0`, + }, + { + code: `!foo || foo.bar != 1`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != 1`, + }, + { + code: `!foo || foo.bar != "123"`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != "123"`, + }, + { + code: `!foo || foo.bar != {}`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != {}`, + }, + { + code: `!foo || foo.bar != false`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != false`, + }, + { + code: `!foo || foo.bar != true`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != true`, + }, + { + code: `!foo || foo.bar === undefined`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === undefined`, + }, + { + code: `!foo || foo.bar !== 0`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== 0`, + }, + { + code: `!foo || foo.bar !== 1`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== 1`, + }, + { + code: `!foo || foo.bar !== "123"`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== "123"`, + }, + { + code: `!foo || foo.bar !== {}`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== {}`, + }, + { + code: `!foo || foo.bar !== false`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== false`, + }, + { + code: `!foo || foo.bar !== true`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== true`, + }, + { + code: `!foo || foo.bar !== null`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== null`, + }, + { + code: `foo == null || foo.bar != 0`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != 0`, + }, + { + code: `foo == null || foo.bar != 1`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != 1`, + }, + { + code: `foo == null || foo.bar != "123"`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != "123"`, + }, + { + code: `foo == null || foo.bar != {}`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != {}`, + }, + { + code: `foo == null || foo.bar != false`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != false`, + }, + { + code: `foo == null || foo.bar != true`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != true`, + }, + { + code: `foo == null || foo.bar === undefined`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar === undefined`, + }, + { + code: `foo == null || foo.bar !== 0`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== 0`, + }, + { + code: `foo == null || foo.bar !== 1`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== 1`, + }, + { + code: `foo == null || foo.bar !== "123"`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== "123"`, + }, + { + code: `foo == null || foo.bar !== {}`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== {}`, + }, + { + code: `foo == null || foo.bar !== false`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== false`, + }, + { + code: `foo == null || foo.bar !== true`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== true`, + }, + { + code: `foo == null || foo.bar !== null`, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar !== null`, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar == x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar == null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar == undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar === x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar === undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== 0; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 0; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== 1; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 1; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== "123"; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== "123"; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== {}; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== {}; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== false; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== false; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== true; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== true; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar !== x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar == x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar == null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar == undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar == undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar === x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === x; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar === undefined; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar === undefined; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== 0; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 0; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== 1; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== 1; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== "123"; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== "123"; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== {}; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== {}; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== false; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== false; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== true; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== true; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo == null || foo.bar !== x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar !== x; + `, + }, ], }); }); From df6809a6238ebc2452183d61a3958148d9ac474e Mon Sep 17 00:00:00 2001 From: mdm317 Date: Wed, 27 Aug 2025 00:31:20 +0900 Subject: [PATCH 11/22] fix : test case a || b is always b when a is false --- .../prefer-optional-chain.test.ts | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 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 60c28c7f11a2..2a19868f2746 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 @@ -3088,6 +3088,7 @@ describe('base cases', () => { // with the `| null | undefined` type - `=== null` doesn't cover the // `undefined` case - so optional chaining is not a valid conversion valid: BaseCases({ + skipIds: [20, 26], mutateCode: c => c.replaceAll('||', '=== null ||'), mutateOutput: identity, operator: '||', @@ -3105,7 +3106,30 @@ describe('base cases', () => { mutateOutput: c => c.replace(/;$/, ' === null;'), operator: '||', useSuggestionFixer: true, - }), + }).concat([ + { + code: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar === null || foo.bar() === null || foo.bar().baz === null || foo.bar().baz.buzz === null || foo.bar().baz.buzz(); + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar?.() === null || foo.bar().baz === null || foo.bar().baz.buzz === null || foo.bar().baz.buzz(); + `, + }, + { + code: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar === null || foo.bar?.() === null || foo.bar?.().baz; + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar?.() === null || foo.bar?.().baz; + `, + }, + ]), }); }); @@ -3130,6 +3154,7 @@ describe('base cases', () => { // with the `| null | undefined` type - `=== undefined` doesn't cover the // `null` case - so optional chaining is not a valid conversion valid: BaseCases({ + skipIds: [20, 26], mutateCode: c => c.replaceAll('||', '=== undefined ||'), mutateOutput: identity, operator: '||', @@ -3146,7 +3171,30 @@ describe('base cases', () => { mutateDeclaration: c => c.replaceAll('| null', ''), mutateOutput: c => c.replace(/;$/, ' === undefined;'), operator: '||', - }), + }).concat([ + { + code: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar === undefined || foo.bar() === undefined || foo.bar().baz === undefined || foo.bar().baz.buzz === undefined || foo.bar().baz.buzz(); + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; + foo.bar?.() === undefined || foo.bar().baz === undefined || foo.bar().baz.buzz === undefined || foo.bar().baz.buzz(); + `, + }, + { + code: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar === undefined || foo.bar?.() === undefined || foo.bar?.().baz; + `, + errors: [{ messageId: 'preferOptionalChain' }], + output: ` + declare const foo: {bar: () => ({baz: number} | null | undefined)}; + foo.bar?.() === undefined || foo.bar?.().baz; + `, + }, + ]), }); }); From 165e9144801530083eec1c0ebfe3b7fb503222d0 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Wed, 27 Aug 2025 00:31:38 +0900 Subject: [PATCH 12/22] feat : or chain --- .../analyzeChain.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index f7ceec398743..10e50e196eee 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -65,13 +65,17 @@ function isTruthyOperand( } switch (nullishComparisonType) { case NullishComparisonType.Boolean: + case NullishComparisonType.NotBoolean: const types = unionConstituents(comparedNameType); return types.every(type => !isFalsyType(type)); case NullishComparisonType.NotStrictEqualUndefined: + case NullishComparisonType.StrictEqualUndefined: return !isTypeFlagSet(comparedNameType, ts.TypeFlags.Undefined); case NullishComparisonType.NotStrictEqualNull: + case NullishComparisonType.StrictEqualNull: return !isTypeFlagSet(comparedNameType, ts.TypeFlags.Null); case NullishComparisonType.NotEqualNullOrUndefined: + case NullishComparisonType.EqualNullOrUndefined: return !isTypeFlagSet( comparedNameType, ts.TypeFlags.Null | ts.TypeFlags.Undefined, @@ -81,7 +85,7 @@ function isTruthyOperand( } } -function isValidLastChainOperand( +function isValidAndLastChainOperand( ComparisonValueType: TSESTree.Node, operator: TSESTree.BinaryExpression['operator'], parserServices: ParserServicesWithTypeInformation, @@ -107,6 +111,42 @@ function isValidLastChainOperand( ); return !isUndefined; } + case '!=': + case '!==': + return false; + default: + return false; + } +} +function isValidOrLastChainOperand( + ComparisonValueType: TSESTree.Node, + operator: TSESTree.BinaryExpression['operator'], + parserServices: ParserServicesWithTypeInformation, +) { + const type = parserServices.getTypeAtLocation(ComparisonValueType); + let ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + + switch (operator) { + case '!=': { + const types = unionConstituents(type); + const isNullish = types.some(t => + isTypeFlagSet( + t, + ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined, + ), + ); + return !isNullish; + } + case '!==': { + const types = unionConstituents(type); + const isUndefined = types.some(t => + isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), + ); + return !isUndefined; + } + case '==': + case '===': + return false; default: return false; } @@ -209,6 +249,7 @@ const analyzeOrChainOperand: OperandAnalyzer = ( ) { return [operand, nextOperand]; } + if ( includesType( parserServices, @@ -221,7 +262,6 @@ const analyzeOrChainOperand: OperandAnalyzer = ( // optional chain would change the runtime behavior of the expression return null; } - return [operand]; } @@ -659,11 +699,13 @@ export function analyzeChain( operand.comparedName, ); switch (operand.comparisonType) { + case NullishComparisonType.StrictEqualUndefined: case NullishComparisonType.NotStrictEqualUndefined: { if (comparisonResult === NodeComparisonResult.Subset) { subChain.push(operand); } } + case NullishComparisonType.StrictEqualNull: case NullishComparisonType.NotStrictEqualNull: { if ( comparisonResult === NodeComparisonResult.Subset && @@ -717,7 +759,10 @@ export function analyzeChain( lastOperand.comparedName, lastChainOperand.comparedName, ); - + const isValidLastChainOperand = + operator === '&&' + ? isValidAndLastChainOperand + : isValidOrLastChainOperand; if ( comparisonResult === NodeComparisonResult.Subset && (isTruthyOperand( From b7d2e02d3d90ceee2c7332a7d180c36779adabe5 Mon Sep 17 00:00:00 2001 From: mdm317 <62943813+mdm317@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:56:00 +0900 Subject: [PATCH 13/22] refactoring: change funtion name --- .../src/rules/prefer-optional-chain-utils/analyzeChain.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 10e50e196eee..8e1b4ed23206 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -52,7 +52,7 @@ function includesType( return false; } -function isTruthyOperand( +function isAlwaysTruthyOperand( comparedName: TSESTree.Node, nullishComparisonType: NullishComparisonType | ComparisonType, parserServices: ParserServicesWithTypeInformation, @@ -709,7 +709,7 @@ export function analyzeChain( case NullishComparisonType.NotStrictEqualNull: { if ( comparisonResult === NodeComparisonResult.Subset && - isTruthyOperand( + isAlwaysTruthyOperand( lastOperand.comparedName, lastOperand.comparisonType, parserServices, @@ -765,7 +765,7 @@ export function analyzeChain( : isValidOrLastChainOperand; if ( comparisonResult === NodeComparisonResult.Subset && - (isTruthyOperand( + (isAlwaysTruthyOperand( lastOperand.comparedName, lastOperand.comparisonType, parserServices, From 7fe1f3325ec7faccfe443af4d8bda3f7ba70249f Mon Sep 17 00:00:00 2001 From: mdm317 <62943813+mdm317@users.noreply.github.com> Date: Thu, 4 Sep 2025 17:21:42 +0900 Subject: [PATCH 14/22] lint --- .../src/rules/prefer-includes.ts | 2 +- .../analyzeChain.ts | 12 +- .../gatherLogicalOperands.ts | 28 +- .../rules/prefer-string-starts-ends-with.ts | 2 +- .../prefer-optional-chain.test.ts | 1286 +++++++++-------- 5 files changed, 703 insertions(+), 627 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-includes.ts b/packages/eslint-plugin/src/rules/prefer-includes.ts index 6825fc042a6d..4da95f8f9866 100644 --- a/packages/eslint-plugin/src/rules/prefer-includes.ts +++ b/packages/eslint-plugin/src/rules/prefer-includes.ts @@ -39,7 +39,7 @@ export default createRule({ function isNumber(node: TSESTree.Node, value: number): boolean { const evaluated = getStaticValue(node, globalScope); - return evaluated != null && evaluated.value === value; + return evaluated?.value === value; } function isPositiveCheck(node: TSESTree.BinaryExpression): boolean { diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 8e1b4ed23206..c716b819e728 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -54,10 +54,10 @@ function includesType( function isAlwaysTruthyOperand( comparedName: TSESTree.Node, - nullishComparisonType: NullishComparisonType | ComparisonType, + nullishComparisonType: ComparisonType | NullishComparisonType, parserServices: ParserServicesWithTypeInformation, ): boolean { - let ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; const comparedNameType = parserServices.getTypeAtLocation(comparedName); if (isTypeFlagSet(comparedNameType, ANY_UNKNOWN_FLAGS)) { @@ -65,9 +65,10 @@ function isAlwaysTruthyOperand( } switch (nullishComparisonType) { case NullishComparisonType.Boolean: - case NullishComparisonType.NotBoolean: + case NullishComparisonType.NotBoolean: { const types = unionConstituents(comparedNameType); return types.every(type => !isFalsyType(type)); + } case NullishComparisonType.NotStrictEqualUndefined: case NullishComparisonType.StrictEqualUndefined: return !isTypeFlagSet(comparedNameType, ts.TypeFlags.Undefined); @@ -91,7 +92,7 @@ function isValidAndLastChainOperand( parserServices: ParserServicesWithTypeInformation, ) { const type = parserServices.getTypeAtLocation(ComparisonValueType); - let ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; switch (operator) { case '==': { @@ -124,7 +125,7 @@ function isValidOrLastChainOperand( parserServices: ParserServicesWithTypeInformation, ) { const type = parserServices.getTypeAtLocation(ComparisonValueType); - let ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; switch (operator) { case '!=': { @@ -704,6 +705,7 @@ export function analyzeChain( if (comparisonResult === NodeComparisonResult.Subset) { subChain.push(operand); } + break; } case NullishComparisonType.StrictEqualNull: case NullishComparisonType.NotStrictEqualNull: { 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 d4dcf97da121..35a76d12ae49 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 @@ -57,7 +57,7 @@ export const enum ComparisonType { } export interface ValidOperand { comparedName: TSESTree.Node; - comparisonType: NullishComparisonType | ComparisonType; + comparisonType: ComparisonType | NullishComparisonType; isYoda: boolean; node: TSESTree.Expression; type: OperandValidity.Valid; @@ -73,7 +73,7 @@ export interface LastChainOperand { export interface InvalidOperand { type: OperandValidity.Invalid; } -type Operand = InvalidOperand | ValidOperand | LastChainOperand; +type Operand = InvalidOperand | LastChainOperand | ValidOperand; const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( @@ -217,6 +217,7 @@ export function gatherLogicalOperands( }); continue; } + break; case '!==': case '===': { @@ -266,12 +267,12 @@ export function gatherLogicalOperands( ? ComparisonType.Equal : ComparisonType.StrictEqual; result.push({ - isYoda, comparedName, - comparisonType: comparisonType, - type: OperandValidity.Last, - node: operand, + comparisonType, comparisonValue: comparedValue, + isYoda, + node: operand, + type: OperandValidity.Last, }); continue; } @@ -283,12 +284,12 @@ export function gatherLogicalOperands( ? ComparisonType.NotEqual : ComparisonType.NotStrictEqual; result.push({ - isYoda, comparedName, - comparisonType: comparisonType, - type: OperandValidity.Last, - node: operand, + comparisonType, comparisonValue: comparedValue, + isYoda, + node: operand, + type: OperandValidity.Last, }); continue; } @@ -439,18 +440,19 @@ export function gatherLogicalOperands( if (isLeftMemberExpression && !isRightMemberExpression) { const [comparedName, comparedValue] = [left, right]; return { - isYoda, comparedName, comparedValue, + isYoda, }; - } else if (!isLeftMemberExpression && isRightMemberExpression) { + } + if (!isLeftMemberExpression && isRightMemberExpression) { const [comparedName, comparedValue] = [right, left]; isYoda = true; return { - isYoda, comparedName, comparedValue, + isYoda, }; } return null; diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index ee946a4f85a0..414e1d54b0a8 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -96,7 +96,7 @@ export default createRule({ value: number, ): node is TSESTree.Literal { const evaluated = getStaticValue(node, globalScope); - return evaluated != null && evaluated.value === value; + return evaluated?.value === value; } /** 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 2a19868f2746..4d38aaaa78b8 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 @@ -700,1008 +700,1008 @@ describe('|| {}', () => { describe('chain ending with comparison', () => { ruleTester.run('prefer-optional-chain', rule, { - valid: [ - 'foo && foo.bar == x', - 'foo && foo.bar == null', - 'foo && foo.bar == undefined', - 'foo && foo.bar === x', - 'foo && foo.bar === undefined', - 'foo && foo.bar !== 0', - 'foo && foo.bar !== 1', - 'foo && foo.bar !== "123"', - 'foo && foo.bar !== {}', - 'foo && foo.bar !== false', - 'foo && foo.bar !== true', - 'foo && foo.bar !== null', - 'foo && foo.bar !== x', - 'foo != null && foo.bar == x', - 'foo != null && foo.bar == null', - 'foo != null && foo.bar == undefined', - 'foo != null && foo.bar === x', - 'foo != null && foo.bar === undefined', - 'foo != null && foo.bar !== 0', - 'foo != null && foo.bar !== 1', - 'foo != null && foo.bar !== "123"', - 'foo != null && foo.bar !== {}', - 'foo != null && foo.bar !== false', - 'foo != null && foo.bar !== true', - 'foo != null && foo.bar !== null', - 'foo != null && foo.bar !== x', - '!foo && foo.bar == 0;', - '!foo && foo.bar == 1;', - '!foo && foo.bar == "123";', - '!foo && foo.bar == {};', - '!foo && foo.bar == false;', - '!foo && foo.bar == true;', - '!foo && foo.bar === 0;', - '!foo && foo.bar === 1;', - '!foo && foo.bar === "123";', - '!foo && foo.bar === {};', - '!foo && foo.bar === false;', - '!foo && foo.bar === true;', - '!foo && foo.bar === null;', - '!foo && foo.bar !== undefined;', - 'foo == null && foo.bar == 0;', - 'foo == null && foo.bar == 1;', - 'foo == null && foo.bar == "123";', - 'foo == null && foo.bar == {};', - 'foo == null && foo.bar == false;', - 'foo == null && foo.bar == true;', - 'foo == null && foo.bar === 0;', - 'foo == null && foo.bar === 1;', - 'foo == null && foo.bar === "123";', - 'foo == null && foo.bar === {};', - 'foo == null && foo.bar === false;', - 'foo == null && foo.bar === true;', - 'foo == null && foo.bar === null;', - 'foo == null && foo.bar !== undefined;', - ` - declare const x: false | { a: string }; - x && x.a == x; - `, - ` - declare const x: '' | { a: string }; - x && x.a == x; - `, - ` - declare const x: 0 | { a: string }; - x && x.a == x; - `, - ` - declare const x: 0n | { a: string }; - x && x.a; - `, - '!foo || foo.bar != x', - '!foo || foo.bar != null', - '!foo || foo.bar != undefined', - '!foo || foo.bar === 0', - '!foo || foo.bar === 1', - '!foo || foo.bar === "123"', - '!foo || foo.bar === {}', - '!foo || foo.bar === false', - '!foo || foo.bar === true', - '!foo || foo.bar === null', - '!foo || foo.bar === x', - '!foo || foo.bar !== x', - '!foo || foo.bar !== undefined', - 'foo == null || foo.bar != x', - 'foo == null || foo.bar != null', - 'foo == null || foo.bar != undefined', - 'foo == null || foo.bar === 0', - 'foo == null || foo.bar === 1', - 'foo == null || foo.bar === "123"', - 'foo == null || foo.bar === {}', - 'foo == null || foo.bar === false', - 'foo == null || foo.bar === true', - 'foo == null || foo.bar === null', - 'foo == null || foo.bar === x', - 'foo == null || foo.bar !== x', - 'foo == null || foo.bar !== undefined', - 'foo || foo.bar != 0;', - 'foo || foo.bar != 1;', - 'foo || foo.bar != "123";', - 'foo || foo.bar != {};', - 'foo || foo.bar != false;', - 'foo || foo.bar != true;', - 'foo || foo.bar === undefined;', - 'foo || foo.bar !== 0;', - 'foo || foo.bar !== 1;', - 'foo || foo.bar !== "123";', - 'foo || foo.bar !== {};', - 'foo || foo.bar !== false;', - 'foo || foo.bar !== true;', - 'foo || foo.bar !== null;', - 'foo != null || foo.bar != 0;', - 'foo != null || foo.bar != 1;', - 'foo != null || foo.bar != "123";', - 'foo != null || foo.bar != {};', - 'foo != null || foo.bar != false;', - 'foo != null || foo.bar != true;', - 'foo != null || foo.bar === undefined;', - 'foo != null || foo.bar !== 0;', - 'foo != null || foo.bar !== 1;', - 'foo != null || foo.bar !== "123";', - 'foo != null || foo.bar !== {};', - 'foo != null || foo.bar !== false;', - 'foo != null || foo.bar !== true;', - 'foo != null || foo.bar !== null;', - ], invalid: [ { - code: `foo && foo.bar == 0;`, + code: 'foo && foo.bar == 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == 0;`, }, { - code: `foo && foo.bar == 1;`, + code: 'foo && foo.bar == 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == 1;`, }, { - code: `foo && foo.bar == "123";`, + code: "foo && foo.bar == '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar == "123";`, + output: `foo?.bar == '123';`, }, { - code: `foo && foo.bar == {};`, + code: 'foo && foo.bar == {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == {};`, }, { - code: `foo && foo.bar == false;`, + code: 'foo && foo.bar == false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == false;`, }, { - code: `foo && foo.bar == true;`, + code: 'foo && foo.bar == true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == true;`, }, { - code: `foo && foo.bar === 0;`, + code: 'foo && foo.bar === 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === 0;`, }, { - code: `foo && foo.bar === 1;`, + code: 'foo && foo.bar === 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === 1;`, }, { - code: `foo && foo.bar === "123";`, + code: "foo && foo.bar === '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar === "123";`, + output: `foo?.bar === '123';`, }, { - code: `foo && foo.bar === {};`, + code: 'foo && foo.bar === {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === {};`, }, { - code: `foo && foo.bar === false;`, + code: 'foo && foo.bar === false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === false;`, }, { - code: `foo && foo.bar === true;`, + code: 'foo && foo.bar === true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === true;`, }, { - code: `foo && foo.bar === null;`, + code: 'foo && foo.bar === null;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === null;`, }, { - code: `foo && foo.bar !== undefined;`, + code: 'foo && foo.bar !== undefined;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar !== undefined;`, }, { - code: `foo != null && foo.bar == 0;`, + code: 'foo != null && foo.bar == 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == 0;`, }, { - code: `foo != null && foo.bar == 1;`, + code: 'foo != null && foo.bar == 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == 1;`, }, { - code: `foo != null && foo.bar == "123";`, + code: "foo != null && foo.bar == '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar == "123";`, + output: `foo?.bar == '123';`, }, { - code: `foo != null && foo.bar == {};`, + code: 'foo != null && foo.bar == {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == {};`, }, { - code: `foo != null && foo.bar == false;`, + code: 'foo != null && foo.bar == false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == false;`, }, { - code: `foo != null && foo.bar == true;`, + code: 'foo != null && foo.bar == true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar == true;`, }, { - code: `foo != null && foo.bar === 0;`, + code: 'foo != null && foo.bar === 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === 0;`, }, { - code: `foo != null && foo.bar === 1;`, + code: 'foo != null && foo.bar === 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === 1;`, }, { - code: `foo != null && foo.bar === "123";`, + code: "foo != null && foo.bar === '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar === "123";`, + output: `foo?.bar === '123';`, }, { - code: `foo != null && foo.bar === {};`, + code: 'foo != null && foo.bar === {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === {};`, }, { - code: `foo != null && foo.bar === false;`, + code: 'foo != null && foo.bar === false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === false;`, }, { - code: `foo != null && foo.bar === true;`, + code: 'foo != null && foo.bar === true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === true;`, }, { - code: `foo != null && foo.bar === null;`, + code: 'foo != null && foo.bar === null;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === null;`, }, { - code: `foo != null && foo.bar !== undefined;`, + code: 'foo != null && foo.bar !== undefined;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar !== undefined;`, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar == x; - `, + declare const foo: { bar: number }; + foo && foo.bar == x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == x; - `, + declare const foo: { bar: number }; + foo?.bar == x; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar == null; - `, + declare const foo: { bar: number }; + foo && foo.bar == null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == null; - `, + declare const foo: { bar: number }; + foo?.bar == null; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar == undefined; - `, + declare const foo: { bar: number }; + foo && foo.bar == undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == undefined; - `, + declare const foo: { bar: number }; + foo?.bar == undefined; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar === x; - `, + declare const foo: { bar: number }; + foo && foo.bar === x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === x; - `, + declare const foo: { bar: number }; + foo?.bar === x; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar === undefined; - `, + declare const foo: { bar: number }; + foo && foo.bar === undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === undefined; - `, + declare const foo: { bar: number }; + foo?.bar === undefined; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== 0; - `, + declare const foo: { bar: number }; + foo && foo.bar !== 0; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 0; - `, + declare const foo: { bar: number }; + foo?.bar !== 0; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== 1; - `, + declare const foo: { bar: number }; + foo && foo.bar !== 1; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 1; - `, + declare const foo: { bar: number }; + foo?.bar !== 1; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== "123"; - `, + declare const foo: { bar: number }; + foo && foo.bar !== '123'; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== "123"; - `, + declare const foo: { bar: number }; + foo?.bar !== '123'; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== {}; - `, + declare const foo: { bar: number }; + foo && foo.bar !== {}; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== {}; - `, + declare const foo: { bar: number }; + foo?.bar !== {}; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== false; - `, + declare const foo: { bar: number }; + foo && foo.bar !== false; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== false; - `, + declare const foo: { bar: number }; + foo?.bar !== false; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== true; - `, + declare const foo: { bar: number }; + foo && foo.bar !== true; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== true; - `, + declare const foo: { bar: number }; + foo?.bar !== true; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== null; - `, + declare const foo: { bar: number }; + foo && foo.bar !== null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== null; - `, + declare const foo: { bar: number }; + foo?.bar !== null; + `, }, { code: ` - declare const foo: { bar: number }; - foo && foo.bar !== x; - `, + declare const foo: { bar: number }; + foo && foo.bar !== x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== x; - `, + declare const foo: { bar: number }; + foo?.bar !== x; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar == x; - `, + declare const foo: { bar: number }; + foo != null && foo.bar == x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == x; - `, + declare const foo: { bar: number }; + foo?.bar == x; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar == null; - `, + declare const foo: { bar: number }; + foo != null && foo.bar == null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == null; - `, + declare const foo: { bar: number }; + foo?.bar == null; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar == undefined; - `, + declare const foo: { bar: number }; + foo != null && foo.bar == undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == undefined; - `, + declare const foo: { bar: number }; + foo?.bar == undefined; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar === x; - `, + declare const foo: { bar: number }; + foo != null && foo.bar === x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === x; - `, + declare const foo: { bar: number }; + foo?.bar === x; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar === undefined; - `, + declare const foo: { bar: number }; + foo != null && foo.bar === undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === undefined; - `, + declare const foo: { bar: number }; + foo?.bar === undefined; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== 0; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== 0; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 0; - `, + declare const foo: { bar: number }; + foo?.bar !== 0; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== 1; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== 1; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 1; - `, + declare const foo: { bar: number }; + foo?.bar !== 1; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== "123"; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== '123'; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== "123"; - `, + declare const foo: { bar: number }; + foo?.bar !== '123'; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== {}; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== {}; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== {}; - `, + declare const foo: { bar: number }; + foo?.bar !== {}; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== false; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== false; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== false; - `, + declare const foo: { bar: number }; + foo?.bar !== false; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== true; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== true; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== true; - `, + declare const foo: { bar: number }; + foo?.bar !== true; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== null; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== null; - `, + declare const foo: { bar: number }; + foo?.bar !== null; + `, }, { code: ` - declare const foo: { bar: number }; - foo != null && foo.bar !== x; - `, + declare const foo: { bar: number }; + foo != null && foo.bar !== x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== x; - `, + declare const foo: { bar: number }; + foo?.bar !== x; + `, }, { code: ` - declare const foo: { bar: number } | 1; - foo && foo.bar == x; - `, + declare const foo: { bar: number } | 1; + foo && foo.bar == x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number } | 1; - foo?.bar == x; - `, + declare const foo: { bar: number } | 1; + foo?.bar == x; + `, }, { code: ` - declare const foo: { bar: number } | 0; - foo != null && foo.bar == x; - `, + declare const foo: { bar: number } | 0; + foo != null && foo.bar == x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number } | 0; - foo?.bar == x; - `, + declare const foo: { bar: number } | 0; + foo?.bar == x; + `, }, { - code: `!foo || foo.bar != 0`, + code: '!foo || foo.bar != 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != 0`, + output: `foo?.bar != 0;`, }, { - code: `!foo || foo.bar != 1`, + code: '!foo || foo.bar != 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != 1`, + output: `foo?.bar != 1;`, }, { - code: `!foo || foo.bar != "123"`, + code: "!foo || foo.bar != '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != "123"`, + output: `foo?.bar != '123';`, }, { - code: `!foo || foo.bar != {}`, + code: '!foo || foo.bar != {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != {}`, + output: `foo?.bar != {};`, }, { - code: `!foo || foo.bar != false`, + code: '!foo || foo.bar != false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != false`, + output: `foo?.bar != false;`, }, { - code: `!foo || foo.bar != true`, + code: '!foo || foo.bar != true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != true`, + output: `foo?.bar != true;`, }, { - code: `!foo || foo.bar === undefined`, + code: '!foo || foo.bar === undefined;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar === undefined`, + output: `foo?.bar === undefined;`, }, { - code: `!foo || foo.bar !== 0`, + code: '!foo || foo.bar !== 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== 0`, + output: `foo?.bar !== 0;`, }, { - code: `!foo || foo.bar !== 1`, + code: '!foo || foo.bar !== 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== 1`, + output: `foo?.bar !== 1;`, }, { - code: `!foo || foo.bar !== "123"`, + code: "!foo || foo.bar !== '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== "123"`, + output: `foo?.bar !== '123';`, }, { - code: `!foo || foo.bar !== {}`, + code: '!foo || foo.bar !== {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== {}`, + output: `foo?.bar !== {};`, }, { - code: `!foo || foo.bar !== false`, + code: '!foo || foo.bar !== false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== false`, + output: `foo?.bar !== false;`, }, { - code: `!foo || foo.bar !== true`, + code: '!foo || foo.bar !== true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== true`, + output: `foo?.bar !== true;`, }, { - code: `!foo || foo.bar !== null`, + code: '!foo || foo.bar !== null;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== null`, + output: `foo?.bar !== null;`, }, { - code: `foo == null || foo.bar != 0`, + code: 'foo == null || foo.bar != 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != 0`, + output: `foo?.bar != 0;`, }, { - code: `foo == null || foo.bar != 1`, + code: 'foo == null || foo.bar != 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != 1`, + output: `foo?.bar != 1;`, }, { - code: `foo == null || foo.bar != "123"`, + code: "foo == null || foo.bar != '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != "123"`, + output: `foo?.bar != '123';`, }, { - code: `foo == null || foo.bar != {}`, + code: 'foo == null || foo.bar != {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != {}`, + output: `foo?.bar != {};`, }, { - code: `foo == null || foo.bar != false`, + code: 'foo == null || foo.bar != false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != false`, + output: `foo?.bar != false;`, }, { - code: `foo == null || foo.bar != true`, + code: 'foo == null || foo.bar != true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar != true`, + output: `foo?.bar != true;`, }, { - code: `foo == null || foo.bar === undefined`, + code: 'foo == null || foo.bar === undefined;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar === undefined`, + output: `foo?.bar === undefined;`, }, { - code: `foo == null || foo.bar !== 0`, + code: 'foo == null || foo.bar !== 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== 0`, + output: `foo?.bar !== 0;`, }, { - code: `foo == null || foo.bar !== 1`, + code: 'foo == null || foo.bar !== 1;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== 1`, + output: `foo?.bar !== 1;`, }, { - code: `foo == null || foo.bar !== "123"`, + code: "foo == null || foo.bar !== '123';", errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== "123"`, + output: `foo?.bar !== '123';`, }, { - code: `foo == null || foo.bar !== {}`, + code: 'foo == null || foo.bar !== {};', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== {}`, + output: `foo?.bar !== {};`, }, { - code: `foo == null || foo.bar !== false`, + code: 'foo == null || foo.bar !== false;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== false`, + output: `foo?.bar !== false;`, }, { - code: `foo == null || foo.bar !== true`, + code: 'foo == null || foo.bar !== true;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== true`, + output: `foo?.bar !== true;`, }, { - code: `foo == null || foo.bar !== null`, + code: 'foo == null || foo.bar !== null;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], - output: `foo?.bar !== null`, + output: `foo?.bar !== null;`, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar == x; - `, + declare const foo: { bar: number }; + !foo || foo.bar == x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == x; - `, + declare const foo: { bar: number }; + foo?.bar == x; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar == null; - `, + declare const foo: { bar: number }; + !foo || foo.bar == null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == null; - `, + declare const foo: { bar: number }; + foo?.bar == null; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar == undefined; - `, + declare const foo: { bar: number }; + !foo || foo.bar == undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == undefined; - `, + declare const foo: { bar: number }; + foo?.bar == undefined; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar === x; - `, + declare const foo: { bar: number }; + !foo || foo.bar === x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === x; - `, + declare const foo: { bar: number }; + foo?.bar === x; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar === undefined; - `, + declare const foo: { bar: number }; + !foo || foo.bar === undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === undefined; - `, + declare const foo: { bar: number }; + foo?.bar === undefined; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== 0; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== 0; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 0; - `, + declare const foo: { bar: number }; + foo?.bar !== 0; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== 1; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== 1; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 1; - `, + declare const foo: { bar: number }; + foo?.bar !== 1; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== "123"; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== '123'; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== "123"; - `, + declare const foo: { bar: number }; + foo?.bar !== '123'; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== {}; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== {}; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== {}; - `, + declare const foo: { bar: number }; + foo?.bar !== {}; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== false; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== false; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== false; - `, + declare const foo: { bar: number }; + foo?.bar !== false; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== true; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== true; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== true; - `, + declare const foo: { bar: number }; + foo?.bar !== true; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== null; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== null; - `, + declare const foo: { bar: number }; + foo?.bar !== null; + `, }, { code: ` - declare const foo: { bar: number }; - !foo || foo.bar !== x; - `, + declare const foo: { bar: number }; + !foo || foo.bar !== x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== x; - `, + declare const foo: { bar: number }; + foo?.bar !== x; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar == x; - `, + declare const foo: { bar: number }; + foo == null || foo.bar == x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == x; - `, + declare const foo: { bar: number }; + foo?.bar == x; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar == null; - `, + declare const foo: { bar: number }; + foo == null || foo.bar == null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == null; - `, + declare const foo: { bar: number }; + foo?.bar == null; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar == undefined; - `, + declare const foo: { bar: number }; + foo == null || foo.bar == undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar == undefined; - `, + declare const foo: { bar: number }; + foo?.bar == undefined; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar === x; - `, + declare const foo: { bar: number }; + foo == null || foo.bar === x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === x; - `, + declare const foo: { bar: number }; + foo?.bar === x; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar === undefined; - `, + declare const foo: { bar: number }; + foo == null || foo.bar === undefined; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar === undefined; - `, + declare const foo: { bar: number }; + foo?.bar === undefined; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== 0; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== 0; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 0; - `, + declare const foo: { bar: number }; + foo?.bar !== 0; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== 1; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== 1; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== 1; - `, + declare const foo: { bar: number }; + foo?.bar !== 1; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== "123"; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== '123'; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== "123"; - `, + declare const foo: { bar: number }; + foo?.bar !== '123'; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== {}; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== {}; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== {}; - `, + declare const foo: { bar: number }; + foo?.bar !== {}; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== false; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== false; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== false; - `, + declare const foo: { bar: number }; + foo?.bar !== false; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== true; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== true; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== true; - `, + declare const foo: { bar: number }; + foo?.bar !== true; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== null; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== null; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== null; - `, + declare const foo: { bar: number }; + foo?.bar !== null; + `, }, { code: ` - declare const foo: { bar: number }; - foo == null || foo.bar !== x; - `, + declare const foo: { bar: number }; + foo == null || foo.bar !== x; + `, errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: ` - declare const foo: { bar: number }; - foo?.bar !== x; - `, + declare const foo: { bar: number }; + foo?.bar !== x; + `, }, ], + valid: [ + 'foo && foo.bar == x;', + 'foo && foo.bar == null;', + 'foo && foo.bar == undefined;', + 'foo && foo.bar === x;', + 'foo && foo.bar === undefined;', + 'foo && foo.bar !== 0;', + 'foo && foo.bar !== 1;', + "foo && foo.bar !== '123';", + 'foo && foo.bar !== {};', + 'foo && foo.bar !== false;', + 'foo && foo.bar !== true;', + 'foo && foo.bar !== null;', + 'foo && foo.bar !== x;', + 'foo != null && foo.bar == x;', + 'foo != null && foo.bar == null;', + 'foo != null && foo.bar == undefined;', + 'foo != null && foo.bar === x;', + 'foo != null && foo.bar === undefined;', + 'foo != null && foo.bar !== 0;', + 'foo != null && foo.bar !== 1;', + "foo != null && foo.bar !== '123';", + 'foo != null && foo.bar !== {};', + 'foo != null && foo.bar !== false;', + 'foo != null && foo.bar !== true;', + 'foo != null && foo.bar !== null;', + 'foo != null && foo.bar !== x;', + '!foo && foo.bar == 0;', + '!foo && foo.bar == 1;', + "!foo && foo.bar == '123';", + '!foo && foo.bar == {};', + '!foo && foo.bar == false;', + '!foo && foo.bar == true;', + '!foo && foo.bar === 0;', + '!foo && foo.bar === 1;', + "!foo && foo.bar === '123';", + '!foo && foo.bar === {};', + '!foo && foo.bar === false;', + '!foo && foo.bar === true;', + '!foo && foo.bar === null;', + '!foo && foo.bar !== undefined;', + 'foo == null && foo.bar == 0;', + 'foo == null && foo.bar == 1;', + "foo == null && foo.bar == '123';", + 'foo == null && foo.bar == {};', + 'foo == null && foo.bar == false;', + 'foo == null && foo.bar == true;', + 'foo == null && foo.bar === 0;', + 'foo == null && foo.bar === 1;', + "foo == null && foo.bar === '123';", + 'foo == null && foo.bar === {};', + 'foo == null && foo.bar === false;', + 'foo == null && foo.bar === true;', + 'foo == null && foo.bar === null;', + 'foo == null && foo.bar !== undefined;', + ` + declare const x: false | { a: string }; + x && x.a == x; + `, + ` + declare const x: '' | { a: string }; + x && x.a == x; + `, + ` + declare const x: 0 | { a: string }; + x && x.a == x; + `, + ` + declare const x: 0n | { a: string }; + x && x.a; + `, + '!foo || foo.bar != x;', + '!foo || foo.bar != null;', + '!foo || foo.bar != undefined;', + '!foo || foo.bar === 0;', + '!foo || foo.bar === 1;', + "!foo || foo.bar === '123';", + '!foo || foo.bar === {};', + '!foo || foo.bar === false;', + '!foo || foo.bar === true;', + '!foo || foo.bar === null;', + '!foo || foo.bar === x;', + '!foo || foo.bar !== x;', + '!foo || foo.bar !== undefined;', + 'foo == null || foo.bar != x;', + 'foo == null || foo.bar != null;', + 'foo == null || foo.bar != undefined;', + 'foo == null || foo.bar === 0;', + 'foo == null || foo.bar === 1;', + "foo == null || foo.bar === '123';", + 'foo == null || foo.bar === {};', + 'foo == null || foo.bar === false;', + 'foo == null || foo.bar === true;', + 'foo == null || foo.bar === null;', + 'foo == null || foo.bar === x;', + 'foo == null || foo.bar !== x;', + 'foo == null || foo.bar !== undefined;', + 'foo || foo.bar != 0;', + 'foo || foo.bar != 1;', + "foo || foo.bar != '123';", + 'foo || foo.bar != {};', + 'foo || foo.bar != false;', + 'foo || foo.bar != true;', + 'foo || foo.bar === undefined;', + 'foo || foo.bar !== 0;', + 'foo || foo.bar !== 1;', + "foo || foo.bar !== '123';", + 'foo || foo.bar !== {};', + 'foo || foo.bar !== false;', + 'foo || foo.bar !== true;', + 'foo || foo.bar !== null;', + 'foo != null || foo.bar != 0;', + 'foo != null || foo.bar != 1;', + "foo != null || foo.bar != '123';", + 'foo != null || foo.bar != {};', + 'foo != null || foo.bar != false;', + 'foo != null || foo.bar != true;', + 'foo != null || foo.bar === undefined;', + 'foo != null || foo.bar !== 0;', + 'foo != null || foo.bar !== 1;', + "foo != null || foo.bar !== '123';", + 'foo != null || foo.bar !== {};', + 'foo != null || foo.bar !== false;', + 'foo != null || foo.bar !== true;', + 'foo != null || foo.bar !== null;', + ], }); }); @@ -2959,43 +2959,61 @@ describe('base cases', () => { // with the `| null | undefined` type - `!== null` doesn't cover the // `undefined` case - so optional chaining is not a valid conversion valid: BaseCases({ - skipIds: [20, 26], mutateCode: c => c.replaceAll('&&', '!== null &&'), mutateOutput: identity, operator: '&&', + skipIds: [20, 26], }), // but if the type is just `| null` - then it covers the cases and is // a valid conversion - invalid: BaseCases({ - mutateCode: c => c.replaceAll('&&', '!== null &&'), - mutateDeclaration: c => c.replaceAll('| undefined', ''), - mutateOutput: identity, - operator: '&&', - useSuggestionFixer: true, - }).concat([ + invalid: [ + ...BaseCases({ + mutateCode: c => c.replaceAll('&&', '!== null &&'), + mutateDeclaration: c => c.replaceAll('| undefined', ''), + mutateOutput: identity, + operator: '&&', + useSuggestionFixer: true, + }), { code: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar !== null && foo.bar() !== null && foo.bar().baz !== null && foo.bar().baz.buzz !== null && foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar !== null && + foo.bar() !== null && + foo.bar().baz !== null && + foo.bar().baz.buzz !== null && + foo.bar().baz.buzz(); `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar?.() !== null && foo.bar().baz !== null && foo.bar().baz.buzz !== null && foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar?.() !== null && + foo.bar().baz !== null && + foo.bar().baz.buzz !== null && + foo.bar().baz.buzz(); `, }, { code: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar !== null && foo.bar?.() !== null && foo.bar?.().baz; `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar?.() !== null && foo.bar?.().baz; `, }, - ]), + ], }); }); @@ -3016,43 +3034,61 @@ describe('base cases', () => { // with the `| null | undefined` type - `!== undefined` doesn't cover the // `null` case - so optional chaining is not a valid conversion valid: BaseCases({ - skipIds: [20, 26], mutateCode: c => c.replaceAll('&&', '!== undefined &&'), mutateOutput: identity, operator: '&&', + skipIds: [20, 26], }), // but if the type is just `| undefined` - then it covers the cases and is // a valid conversion - invalid: BaseCases({ - mutateCode: c => c.replaceAll('&&', '!== undefined &&'), - mutateDeclaration: c => c.replaceAll('| null', ''), - mutateOutput: identity, - operator: '&&', - useSuggestionFixer: true, - }).concat([ + invalid: [ + ...BaseCases({ + mutateCode: c => c.replaceAll('&&', '!== undefined &&'), + mutateDeclaration: c => c.replaceAll('| null', ''), + mutateOutput: identity, + operator: '&&', + useSuggestionFixer: true, + }), { code: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar !== undefined && foo.bar() !== undefined && foo.bar().baz !== undefined && foo.bar().baz.buzz !== undefined && foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar !== undefined && + foo.bar() !== undefined && + foo.bar().baz !== undefined && + foo.bar().baz.buzz !== undefined && + foo.bar().baz.buzz(); `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar?.() !== undefined && foo.bar().baz !== undefined && foo.bar().baz.buzz !== undefined && foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar?.() !== undefined && + foo.bar().baz !== undefined && + foo.bar().baz.buzz !== undefined && + foo.bar().baz.buzz(); `, }, { code: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar !== undefined && foo.bar?.() !== undefined && foo.bar?.().baz; `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar?.() !== undefined && foo.bar?.().baz; `, }, - ]), + ], }); }); @@ -3088,48 +3124,66 @@ describe('base cases', () => { // with the `| null | undefined` type - `=== null` doesn't cover the // `undefined` case - so optional chaining is not a valid conversion valid: BaseCases({ - skipIds: [20, 26], mutateCode: c => c.replaceAll('||', '=== null ||'), mutateOutput: identity, operator: '||', + skipIds: [20, 26], }), // but if the type is just `| null` - then it covers the cases and is // a valid conversion - invalid: BaseCases({ - mutateCode: c => - c - .replaceAll('||', '=== null ||') - // SEE TODO AT THE BOTTOM OF THE RULE - // We need to ensure the final operand is also a "valid" `||` check - .replace(/;$/, ' === null;'), - mutateDeclaration: c => c.replaceAll('| undefined', ''), - mutateOutput: c => c.replace(/;$/, ' === null;'), - operator: '||', - useSuggestionFixer: true, - }).concat([ + invalid: [ + ...BaseCases({ + mutateCode: c => + c + .replaceAll('||', '=== null ||') + // SEE TODO AT THE BOTTOM OF THE RULE + // We need to ensure the final operand is also a "valid" `||` check + .replace(/;$/, ' === null;'), + mutateDeclaration: c => c.replaceAll('| undefined', ''), + mutateOutput: c => c.replace(/;$/, ' === null;'), + operator: '||', + useSuggestionFixer: true, + }), { code: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar === null || foo.bar() === null || foo.bar().baz === null || foo.bar().baz.buzz === null || foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar === null || + foo.bar() === null || + foo.bar().baz === null || + foo.bar().baz.buzz === null || + foo.bar().baz.buzz(); `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar?.() === null || foo.bar().baz === null || foo.bar().baz.buzz === null || foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar?.() === null || + foo.bar().baz === null || + foo.bar().baz.buzz === null || + foo.bar().baz.buzz(); `, }, { code: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar === null || foo.bar?.() === null || foo.bar?.().baz; `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar?.() === null || foo.bar?.().baz; `, }, - ]), + ], }); }); @@ -3154,47 +3208,65 @@ describe('base cases', () => { // with the `| null | undefined` type - `=== undefined` doesn't cover the // `null` case - so optional chaining is not a valid conversion valid: BaseCases({ - skipIds: [20, 26], mutateCode: c => c.replaceAll('||', '=== undefined ||'), mutateOutput: identity, operator: '||', + skipIds: [20, 26], }), // but if the type is just `| undefined` - then it covers the cases and is // a valid conversion - invalid: BaseCases({ - mutateCode: c => - c - .replaceAll('||', '=== undefined ||') - // SEE TODO AT THE BOTTOM OF THE RULE - // We need to ensure the final operand is also a "valid" `||` check - .replace(/;$/, ' === undefined;'), - mutateDeclaration: c => c.replaceAll('| null', ''), - mutateOutput: c => c.replace(/;$/, ' === undefined;'), - operator: '||', - }).concat([ + invalid: [ + ...BaseCases({ + mutateCode: c => + c + .replaceAll('||', '=== undefined ||') + // SEE TODO AT THE BOTTOM OF THE RULE + // We need to ensure the final operand is also a "valid" `||` check + .replace(/;$/, ' === undefined;'), + mutateDeclaration: c => c.replaceAll('| null', ''), + mutateOutput: c => c.replace(/;$/, ' === undefined;'), + operator: '||', + }), { code: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar === undefined || foo.bar() === undefined || foo.bar().baz === undefined || foo.bar().baz.buzz === undefined || foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar === undefined || + foo.bar() === undefined || + foo.bar().baz === undefined || + foo.bar().baz.buzz === undefined || + foo.bar().baz.buzz(); `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: {buzz: (() => number) | null | undefined} | null | undefined}) | null | undefined}; - foo.bar?.() === undefined || foo.bar().baz === undefined || foo.bar().baz.buzz === undefined || foo.bar().baz.buzz(); + declare const foo: { + bar: () => + | { baz: { buzz: (() => number) | null | undefined } | null | undefined } + | null + | undefined; + }; + foo.bar?.() === undefined || + foo.bar().baz === undefined || + foo.bar().baz.buzz === undefined || + foo.bar().baz.buzz(); `, }, { code: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar === undefined || foo.bar?.() === undefined || foo.bar?.().baz; `, errors: [{ messageId: 'preferOptionalChain' }], output: ` - declare const foo: {bar: () => ({baz: number} | null | undefined)}; + declare const foo: { bar: () => { baz: number } | null | undefined }; foo.bar?.() === undefined || foo.bar?.().baz; `, }, - ]), + ], }); }); From 90c13faa2b9cbfb011d114bcf5f7dbfdcf345341 Mon Sep 17 00:00:00 2001 From: mdm317 <62943813+mdm317@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:01:38 +0900 Subject: [PATCH 15/22] lint --- packages/eslint-plugin/src/rules/no-base-to-string.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index 26eadce99839..f26ad529f220 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -270,6 +270,7 @@ export default createRule({ const declarations = toString.getDeclarations(); + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (declarations == null || declarations.length !== 1) { // If there are multiple declarations, at least one of them must not be // the default object toString. From 609d1aa5082961ccce43bf293ebcb461e24980d4 Mon Sep 17 00:00:00 2001 From: mdm317 <62943813+mdm317@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:45:48 +0900 Subject: [PATCH 16/22] chore: new readme shot --- .../docs-eslint-output-snapshots/prefer-optional-chain.shot | 2 ++ 1 file changed, 2 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..9a8438008091 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 @@ -32,7 +32,9 @@ foo && ~~~~~~~~~~~~~~~ foo.a.b !== null && foo.a.b.c != undefined && + ~~~~~~~~~~~~~~~~~~~~~~~~~ Prefer using an optional chain expression instead, as it's more concise and easier to read. foo.a.b.c.d !== undefined && +~~~~~~~~~~~~~~~~~~~~~~~~~~~ foo.a.b.c.d.e; Correct From 9aa435a0736396cd0d04705b54321309e302aae6 Mon Sep 17 00:00:00 2001 From: mdm317 <62943813+mdm317@users.noreply.github.com> Date: Fri, 5 Sep 2025 02:36:42 +0900 Subject: [PATCH 17/22] Refactor --- .../analyzeChain.ts | 42 +++++++++---------- .../gatherLogicalOperands.ts | 2 +- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index c716b819e728..5481f0b8e134 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -31,11 +31,7 @@ import { } from '../../util'; import { checkNullishAndReport } from './checkNullishAndReport'; import { compareNodes, NodeComparisonResult } from './compareNodes'; -import { - ComparisonType, - NullishComparisonType, - OperandValidity, -} from './gatherLogicalOperands'; +import { NullishComparisonType } from './gatherLogicalOperands'; function includesType( parserServices: ParserServicesWithTypeInformation, @@ -54,7 +50,7 @@ function includesType( function isAlwaysTruthyOperand( comparedName: TSESTree.Node, - nullishComparisonType: ComparisonType | NullishComparisonType, + nullishComparisonType: NullishComparisonType, parserServices: ParserServicesWithTypeInformation, ): boolean { const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; @@ -301,7 +297,7 @@ const analyzeOrChainOperand: OperandAnalyzer = ( * @returns The range to report. */ function getReportRange( - chain: ValidOperand[], + chain: { node: TSESTree.Expression }[], boundary: TSESTree.Range, sourceCode: SourceCode, ): TSESTree.Range { @@ -341,8 +337,10 @@ function getReportDescriptor( node: TSESTree.Node, operator: '&&' | '||', options: PreferOptionalChainOptions, - chain: ValidOperand[], + subChain: ValidOperand[], + lastChain: (LastChainOperand | ValidOperand) | undefined, ): ReportDescriptor { + const chain = lastChain ? [...subChain, lastChain] : subChain; const lastOperand = chain[chain.length - 1]; let useSuggestionFixer: boolean; @@ -358,16 +356,13 @@ function getReportDescriptor( // `undefined`, or else we're going to change the final type - which is // unsafe and might cause downstream type errors. else if ( + lastChain || lastOperand.comparisonType === NullishComparisonType.EqualNullOrUndefined || lastOperand.comparisonType === NullishComparisonType.NotEqualNullOrUndefined || lastOperand.comparisonType === NullishComparisonType.StrictEqualUndefined || lastOperand.comparisonType === NullishComparisonType.NotStrictEqualUndefined || - lastOperand.comparisonType === ComparisonType.Equal || - lastOperand.comparisonType === ComparisonType.NotEqual || - lastOperand.comparisonType === ComparisonType.NotStrictEqual || - lastOperand.comparisonType === ComparisonType.StrictEqual || (operator === '||' && lastOperand.comparisonType === NullishComparisonType.NotBoolean) ) { @@ -643,16 +638,20 @@ export function analyzeChain( // Things like x !== null && x !== undefined have two nodes, but they are // one logical unit here, so we'll allow them to be grouped. let subChain: (readonly ValidOperand[] | ValidOperand)[] = []; + let lastChain: LastChainOperand | ValidOperand | undefined = undefined; const maybeReportThenReset = ( newChainSeed?: readonly [ValidOperand, ...ValidOperand[]], ): void => { - if (subChain.length > 1) { + if (subChain.length + (lastChain ? 1 : 0) > 1) { const subChainFlat = subChain.flat(); + const maybeNullishNodes = lastChain + ? subChainFlat.map(({ node }) => node) + : subChainFlat.slice(0, -1).map(({ node }) => node); checkNullishAndReport( context, parserServices, options, - subChainFlat.slice(0, -1).map(({ node }) => node), + maybeNullishNodes, getReportDescriptor( context.sourceCode, parserServices, @@ -660,6 +659,7 @@ export function analyzeChain( operator, options, subChainFlat, + lastChain, ), ); } @@ -677,6 +677,7 @@ export function analyzeChain( // ^^^^^^^^^^^ newChainSeed // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain subChain = newChainSeed ? [newChainSeed] : []; + lastChain = undefined; }; for (let i = 0; i < chain.length; i += 1) { @@ -703,7 +704,7 @@ export function analyzeChain( case NullishComparisonType.StrictEqualUndefined: case NullishComparisonType.NotStrictEqualUndefined: { if (comparisonResult === NodeComparisonResult.Subset) { - subChain.push(operand); + lastChain = operand; } break; } @@ -717,11 +718,9 @@ export function analyzeChain( parserServices, ) ) { - subChain.push({ - ...operand, - comparisonType: ComparisonType.NotStrictEqual, - }); + lastChain = operand; } + break; } } } @@ -778,10 +777,7 @@ export function analyzeChain( parserServices, )) ) { - subChain.push({ - ...lastChainOperand, - type: OperandValidity.Valid, - }); + lastChain = lastChainOperand; } } // check the leftovers 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 35a76d12ae49..9092e1e67a75 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 @@ -57,7 +57,7 @@ export const enum ComparisonType { } export interface ValidOperand { comparedName: TSESTree.Node; - comparisonType: ComparisonType | NullishComparisonType; + comparisonType: NullishComparisonType; isYoda: boolean; node: TSESTree.Expression; type: OperandValidity.Valid; From befd7a08541fd08105c3a0a9d6508371cc286223 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Fri, 5 Sep 2025 12:28:31 +0900 Subject: [PATCH 18/22] test: add missing case, and chain when the last chain is != operand --- .../prefer-optional-chain.test.ts | 214 ++++++++++++++++++ 1 file changed, 214 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 4d38aaaa78b8..5e74e4292c72 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 @@ -771,6 +771,16 @@ describe('chain ending with comparison', () => { errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar !== undefined;`, }, + { + code: 'foo && foo.bar != undefined;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != undefined;`, + }, + { + code: 'foo && foo.bar != null;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != null;`, + }, { code: 'foo != null && foo.bar == 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], @@ -841,6 +851,16 @@ describe('chain ending with comparison', () => { errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar !== undefined;`, }, + { + code: 'foo != null && foo.bar != undefined;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != undefined;`, + }, + { + code: 'foo != null && foo.bar != null;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar != null;`, + }, { code: ` declare const foo: { bar: number }; @@ -984,6 +1004,94 @@ describe('chain ending with comparison', () => { foo?.bar !== x; `, }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != 0; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != 0; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != 1; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != 1; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != '123'; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != '123'; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != {}; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != {}; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != false; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != false; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != true; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != true; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo && foo.bar != x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != x; + `, + }, { code: ` declare const foo: { bar: number }; @@ -1127,6 +1235,94 @@ describe('chain ending with comparison', () => { foo?.bar !== x; `, }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != 0; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != 0; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != 1; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != 1; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != '123'; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != '123'; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != {}; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != {}; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != false; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != false; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != true; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != true; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + foo != null && foo.bar != x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != x; + `, + }, { code: ` declare const foo: { bar: number } | 1; @@ -1590,6 +1786,13 @@ describe('chain ending with comparison', () => { 'foo && foo.bar !== true;', 'foo && foo.bar !== null;', 'foo && foo.bar !== x;', + 'foo && foo.bar != 0;', + 'foo && foo.bar != 1;', + "foo && foo.bar != '123';", + 'foo && foo.bar != {};', + 'foo && foo.bar != false;', + 'foo && foo.bar != true;', + 'foo && foo.bar != x;', 'foo != null && foo.bar == x;', 'foo != null && foo.bar == null;', 'foo != null && foo.bar == undefined;', @@ -1603,6 +1806,13 @@ describe('chain ending with comparison', () => { 'foo != null && foo.bar !== true;', 'foo != null && foo.bar !== null;', 'foo != null && foo.bar !== x;', + 'foo != null && foo.bar != 0;', + 'foo != null && foo.bar != 1;', + "foo != null && foo.bar != '123';", + 'foo != null && foo.bar != {};', + 'foo != null && foo.bar != false;', + 'foo != null && foo.bar != true;', + 'foo != null && foo.bar != x;', '!foo && foo.bar == 0;', '!foo && foo.bar == 1;', "!foo && foo.bar == '123';", @@ -1617,6 +1827,8 @@ describe('chain ending with comparison', () => { '!foo && foo.bar === true;', '!foo && foo.bar === null;', '!foo && foo.bar !== undefined;', + '!foo && foo.bar != undefined;', + '!foo && foo.bar != null;', 'foo == null && foo.bar == 0;', 'foo == null && foo.bar == 1;', "foo == null && foo.bar == '123';", @@ -1631,6 +1843,8 @@ describe('chain ending with comparison', () => { 'foo == null && foo.bar === true;', 'foo == null && foo.bar === null;', 'foo == null && foo.bar !== undefined;', + 'foo == null && foo.bar != null;', + 'foo == null && foo.bar != undefined;', ` declare const x: false | { a: string }; x && x.a == x; From c59f77f79e4f03ba368ac1002c129994787054fb Mon Sep 17 00:00:00 2001 From: mdm317 Date: Fri, 5 Sep 2025 13:22:21 +0900 Subject: [PATCH 19/22] test: yoda case when `& chain` --- .../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 5e74e4292c72..4a192622f79d 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 @@ -1771,6 +1771,22 @@ describe('chain ending with comparison', () => { foo?.bar !== x; `, }, + // yoda case + { + code: "foo != null && null != foo.bar && '123' == foo.bar.baz;", + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `'123' == foo?.bar?.baz;`, + }, + { + code: "foo != null && null != foo.bar && '123' === foo.bar.baz;", + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `'123' === foo?.bar?.baz;`, + }, + { + code: 'foo != null && null != foo.bar && undefined !== foo.bar.baz;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `undefined !== foo?.bar?.baz;`, + }, ], valid: [ 'foo && foo.bar == x;', From aa793f25d317b4f6669b12717c150aac91ce9af1 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Fri, 5 Sep 2025 13:22:32 +0900 Subject: [PATCH 20/22] add missing logic --- .../analyzeChain.ts | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 5481f0b8e134..84c1ca6e9593 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -31,7 +31,7 @@ import { } from '../../util'; import { checkNullishAndReport } from './checkNullishAndReport'; import { compareNodes, NodeComparisonResult } from './compareNodes'; -import { NullishComparisonType } from './gatherLogicalOperands'; +import { ComparisonType, NullishComparisonType } from './gatherLogicalOperands'; function includesType( parserServices: ParserServicesWithTypeInformation, @@ -66,33 +66,33 @@ function isAlwaysTruthyOperand( return types.every(type => !isFalsyType(type)); } case NullishComparisonType.NotStrictEqualUndefined: - case NullishComparisonType.StrictEqualUndefined: - return !isTypeFlagSet(comparedNameType, ts.TypeFlags.Undefined); case NullishComparisonType.NotStrictEqualNull: case NullishComparisonType.StrictEqualNull: - return !isTypeFlagSet(comparedNameType, ts.TypeFlags.Null); + case NullishComparisonType.StrictEqualUndefined: + return !isTypeFlagSet( + comparedNameType, + ts.TypeFlags.Null | ts.TypeFlags.Undefined, + ); case NullishComparisonType.NotEqualNullOrUndefined: case NullishComparisonType.EqualNullOrUndefined: return !isTypeFlagSet( comparedNameType, ts.TypeFlags.Null | ts.TypeFlags.Undefined, ); - default: - return false; } } function isValidAndLastChainOperand( ComparisonValueType: TSESTree.Node, - operator: TSESTree.BinaryExpression['operator'], + comparisonType: ComparisonType, parserServices: ParserServicesWithTypeInformation, ) { const type = parserServices.getTypeAtLocation(ComparisonValueType); const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; - switch (operator) { - case '==': { - const types = unionConstituents(type); + const types = unionConstituents(type); + switch (comparisonType) { + case ComparisonType.Equal: { const isNullish = types.some(t => isTypeFlagSet( t, @@ -101,31 +101,33 @@ function isValidAndLastChainOperand( ); return !isNullish; } - case '===': { - const types = unionConstituents(type); + case ComparisonType.StrictEqual: { const isUndefined = types.some(t => isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), ); return !isUndefined; } - case '!=': - case '!==': - return false; - default: - return false; + case ComparisonType.NotStrictEqual: { + return types.every(t => isTypeFlagSet(t, ts.TypeFlags.Undefined)); + } + case ComparisonType.NotEqual: { + return types.every(t => + isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null), + ); + } } } function isValidOrLastChainOperand( ComparisonValueType: TSESTree.Node, - operator: TSESTree.BinaryExpression['operator'], + comparisonType: ComparisonType, parserServices: ParserServicesWithTypeInformation, ) { const type = parserServices.getTypeAtLocation(ComparisonValueType); const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; - switch (operator) { - case '!=': { - const types = unionConstituents(type); + const types = unionConstituents(type); + switch (comparisonType) { + case ComparisonType.NotEqual: { const isNullish = types.some(t => isTypeFlagSet( t, @@ -134,18 +136,18 @@ function isValidOrLastChainOperand( ); return !isNullish; } - case '!==': { - const types = unionConstituents(type); + case ComparisonType.NotStrictEqual: { const isUndefined = types.some(t => isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), ); return !isUndefined; } - case '==': - case '===': - return false; - default: - return false; + case ComparisonType.Equal: + return types.every(t => + isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null), + ); + case ComparisonType.StrictEqual: + return types.every(t => isTypeFlagSet(t, ts.TypeFlags.Undefined)); } } @@ -773,7 +775,7 @@ export function analyzeChain( ) || isValidLastChainOperand( lastChainOperand.comparisonValue, - lastChainOperand.node.operator, + lastChainOperand.comparisonType, parserServices, )) ) { From 2a5798bc86e145150172b85ea114638d72bd82d5 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Fri, 5 Sep 2025 13:50:54 +0900 Subject: [PATCH 21/22] test : add missing case check lastOperand always evaluates to true cases: - case `!= null` - case `!== null && !== undefined` --- .../prefer-optional-chain.test.ts | 170 ++++++++++++++++++ 1 file changed, 170 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 4a192622f79d..6e24e422b1f0 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 @@ -1829,6 +1829,176 @@ describe('chain ending with comparison', () => { 'foo != null && foo.bar != false;', 'foo != null && foo.bar != true;', 'foo != null && foo.bar != x;', + ` + declare const foo: { bar: number } | null; + foo && foo.bar == x; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar == null; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar == undefined; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar === x; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar === undefined; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== 0; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== 1; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== '123'; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== {}; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== false; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== true; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== null; + `, + ` + declare const foo: { bar: number } | null; + foo && foo.bar !== x; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar == x; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar == null; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar == undefined; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar === x; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar === undefined; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== 0; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== 1; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== '123'; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== {}; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== false; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== true; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== null; + `, + ` + declare const foo: { bar: number } | null; + foo != null && foo.bar !== x; + `, + ` + declare const foo: { bar: number } | null; + foo !== null && foo !== undefined && foo.bar == null; + `, + ` + declare const foo: { bar: number } | null; + foo !== null && foo !== undefined && foo.bar === undefined; + `, + ` + declare const foo: { bar: number } | null; + foo !== null && foo !== undefined && foo.bar !== 1; + `, + ` + declare const foo: { bar: number } | null; + foo !== null && foo !== undefined && foo.bar != 1; + `, + + ` + declare const foo: { bar: number } | undefined; + foo !== null && foo !== undefined && foo.bar == null; + `, + ` + declare const foo: { bar: number } | undefined; + foo !== null && foo !== undefined && foo.bar === undefined; + `, + ` + declare const foo: { bar: number } | undefined; + foo !== null && foo !== undefined && foo.bar !== 1; + `, + ` + declare const foo: { bar: number } | undefined; + foo !== null && foo !== undefined && foo.bar != 1; + `, + ` + declare const foo: { bar: number } | null; + foo !== undefined && foo !== undefined && foo.bar == null; + `, + ` + declare const foo: { bar: number } | null; + foo !== undefined && foo !== undefined && foo.bar === undefined; + `, + ` + declare const foo: { bar: number } | null; + foo !== undefined && foo !== undefined && foo.bar !== 1; + `, + ` + declare const foo: { bar: number } | null; + foo !== undefined && foo !== undefined && foo.bar != 1; + `, + + ` + declare const foo: { bar: number } | undefined; + foo !== undefined && foo !== undefined && foo.bar == null; + `, + ` + declare const foo: { bar: number } | undefined; + foo !== undefined && foo !== undefined && foo.bar === undefined; + `, + ` + declare const foo: { bar: number } | undefined; + foo !== undefined && foo !== undefined && foo.bar !== 1; + `, + ` + declare const foo: { bar: number } | undefined; + foo !== undefined && foo !== undefined && foo.bar != 1; + `, '!foo && foo.bar == 0;', '!foo && foo.bar == 1;', "!foo && foo.bar == '123';", From f71611f910e1759411b5a34f56cf0e5ba2006e68 Mon Sep 17 00:00:00 2001 From: mdm317 Date: Fri, 5 Sep 2025 14:27:41 +0900 Subject: [PATCH 22/22] add missing case --- .../prefer-optional-chain.test.ts | 127 ++++++++++++++++++ 1 file changed, 127 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 6e24e422b1f0..a990d70b60bb 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 @@ -1380,6 +1380,16 @@ describe('chain ending with comparison', () => { errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === undefined;`, }, + { + code: '!foo || foo.bar == undefined;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == undefined;`, + }, + { + code: '!foo || foo.bar == null;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == null;`, + }, { code: '!foo || foo.bar !== 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], @@ -1450,6 +1460,16 @@ describe('chain ending with comparison', () => { errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `foo?.bar === undefined;`, }, + { + code: 'foo == null || foo.bar == undefined;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == undefined;`, + }, + { + code: 'foo == null || foo.bar == null;', + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: `foo?.bar == null;`, + }, { code: 'foo == null || foo.bar !== 0;', errors: [{ messageId: 'preferOptionalChain', suggestions: null }], @@ -1628,6 +1648,95 @@ describe('chain ending with comparison', () => { foo?.bar !== x; `, }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != 0; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != 0; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != 1; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != 1; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != '123'; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != '123'; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != {}; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != {}; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != false; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != false; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != true; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != true; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != null; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != null; + `, + }, + { + code: ` + declare const foo: { bar: number }; + !foo || foo.bar != x; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const foo: { bar: number }; + foo?.bar != x; + `, + }, + { code: ` declare const foo: { bar: number }; @@ -2058,6 +2167,13 @@ describe('chain ending with comparison', () => { '!foo || foo.bar === true;', '!foo || foo.bar === null;', '!foo || foo.bar === x;', + '!foo || foo.bar == 0;', + '!foo || foo.bar == 1;', + "!foo || foo.bar == '123';", + '!foo || foo.bar == {};', + '!foo || foo.bar == false;', + '!foo || foo.bar == true;', + '!foo || foo.bar == x;', '!foo || foo.bar !== x;', '!foo || foo.bar !== undefined;', 'foo == null || foo.bar != x;', @@ -2071,6 +2187,13 @@ describe('chain ending with comparison', () => { 'foo == null || foo.bar === true;', 'foo == null || foo.bar === null;', 'foo == null || foo.bar === x;', + 'foo == null || foo.bar == 0;', + 'foo == null || foo.bar == 1;', + "foo == null || foo.bar == '123';", + 'foo == null || foo.bar == {};', + 'foo == null || foo.bar == false;', + 'foo == null || foo.bar == true;', + 'foo == null || foo.bar == x;', 'foo == null || foo.bar !== x;', 'foo == null || foo.bar !== undefined;', 'foo || foo.bar != 0;', @@ -2080,6 +2203,8 @@ describe('chain ending with comparison', () => { 'foo || foo.bar != false;', 'foo || foo.bar != true;', 'foo || foo.bar === undefined;', + 'foo || foo.bar == undefined;', + 'foo || foo.bar == null;', 'foo || foo.bar !== 0;', 'foo || foo.bar !== 1;', "foo || foo.bar !== '123';", @@ -2094,6 +2219,8 @@ describe('chain ending with comparison', () => { 'foo != null || foo.bar != false;', 'foo != null || foo.bar != true;', 'foo != null || foo.bar === undefined;', + 'foo != null || foo.bar == undefined;', + 'foo != null || foo.bar == null;', 'foo != null || foo.bar !== 0;', 'foo != null || foo.bar !== 1;', "foo != null || foo.bar !== '123';",