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 0dbcfd2825ef..48fca65676ed 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -317,6 +317,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. 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 c4364765f92c..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 @@ -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,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, @@ -48,6 +48,109 @@ function includesType( return false; } +function isAlwaysTruthyOperand( + comparedName: TSESTree.Node, + nullishComparisonType: NullishComparisonType, + parserServices: ParserServicesWithTypeInformation, +): boolean { + const 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: + case NullishComparisonType.NotBoolean: { + const types = unionConstituents(comparedNameType); + return types.every(type => !isFalsyType(type)); + } + case NullishComparisonType.NotStrictEqualUndefined: + case NullishComparisonType.NotStrictEqualNull: + case NullishComparisonType.StrictEqualNull: + 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, + ); + } +} + +function isValidAndLastChainOperand( + ComparisonValueType: TSESTree.Node, + comparisonType: ComparisonType, + parserServices: ParserServicesWithTypeInformation, +) { + const type = parserServices.getTypeAtLocation(ComparisonValueType); + const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + + const types = unionConstituents(type); + switch (comparisonType) { + case ComparisonType.Equal: { + const isNullish = types.some(t => + isTypeFlagSet( + t, + ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined, + ), + ); + return !isNullish; + } + case ComparisonType.StrictEqual: { + const isUndefined = types.some(t => + isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), + ); + return !isUndefined; + } + 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, + comparisonType: ComparisonType, + parserServices: ParserServicesWithTypeInformation, +) { + const type = parserServices.getTypeAtLocation(ComparisonValueType); + const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + + const types = unionConstituents(type); + switch (comparisonType) { + case ComparisonType.NotEqual: { + const isNullish = types.some(t => + isTypeFlagSet( + t, + ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined, + ), + ); + return !isNullish; + } + case ComparisonType.NotStrictEqual: { + const isUndefined = types.some(t => + isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), + ); + return !isUndefined; + } + 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)); + } +} + // 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. @@ -65,18 +168,7 @@ const analyzeAndChainOperand: OperandAnalyzer = ( chain, ) => { 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]; - } - + case NullishComparisonType.Boolean: case NullishComparisonType.NotEqualNullOrUndefined: return [operand]; @@ -92,7 +184,8 @@ const analyzeAndChainOperand: OperandAnalyzer = ( return [operand, nextOperand]; } if ( - includesType( + nextOperand && + !includesType( parserServices, operand.comparedName, ts.TypeFlags.Undefined, @@ -101,10 +194,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: { @@ -156,6 +248,7 @@ const analyzeOrChainOperand: OperandAnalyzer = ( ) { return [operand, nextOperand]; } + if ( includesType( parserServices, @@ -168,7 +261,6 @@ const analyzeOrChainOperand: OperandAnalyzer = ( // optional chain would change the runtime behavior of the expression return null; } - return [operand]; } @@ -207,7 +299,7 @@ const analyzeOrChainOperand: OperandAnalyzer = ( * @returns The range to report. */ function getReportRange( - chain: ValidOperand[], + chain: { node: TSESTree.Expression }[], boundary: TSESTree.Range, sourceCode: SourceCode, ): TSESTree.Range { @@ -247,8 +339,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; @@ -264,6 +358,7 @@ 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 || @@ -521,10 +616,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 === '??' ) { @@ -544,16 +640,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, @@ -561,6 +661,7 @@ export function analyzeChain( operator, options, subChainFlat, + lastChain, ), ); } @@ -578,6 +679,7 @@ export function analyzeChain( // ^^^^^^^^^^^ newChainSeed // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain subChain = newChainSeed ? [newChainSeed] : []; + lastChain = undefined; }; for (let i = 0; i < chain.length; i += 1) { @@ -595,6 +697,35 @@ 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.StrictEqualUndefined: + case NullishComparisonType.NotStrictEqualUndefined: { + if (comparisonResult === NodeComparisonResult.Subset) { + lastChain = operand; + } + break; + } + case NullishComparisonType.StrictEqualNull: + case NullishComparisonType.NotStrictEqualNull: { + if ( + comparisonResult === NodeComparisonResult.Subset && + isAlwaysTruthyOperand( + lastOperand.comparedName, + lastOperand.comparisonType, + parserServices, + ) + ) { + lastChain = operand; + } + break; + } + } + } maybeReportThenReset(); continue; } @@ -624,7 +755,33 @@ export function analyzeChain( subChain.push(currentOperand); } } + const lastOperand = subChain.flat().at(-1); + if (lastOperand && lastChainOperand) { + const comparisonResult = compareNodes( + lastOperand.comparedName, + lastChainOperand.comparedName, + ); + const isValidLastChainOperand = + operator === '&&' + ? isValidAndLastChainOperand + : isValidOrLastChainOperand; + if ( + comparisonResult === NodeComparisonResult.Subset && + (isAlwaysTruthyOperand( + lastOperand.comparedName, + lastOperand.comparisonType, + parserServices, + ) || + isValidLastChainOperand( + lastChainOperand.comparisonValue, + lastChainOperand.comparisonType, + parserServices, + )) + ) { + lastChain = lastChainOperand; + } + } // 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..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 @@ -25,6 +25,7 @@ const enum ComparisonValueType { } export const enum OperandValidity { Valid = 'Valid', + Last = 'Last', Invalid = 'Invalid', } export const enum NullishComparisonType { @@ -48,6 +49,12 @@ 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; @@ -55,10 +62,18 @@ export interface ValidOperand { 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 | LastChainOperand | ValidOperand; const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( @@ -182,61 +197,101 @@ 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; - } - // x == something :( - result.push({ type: OperandValidity.Invalid }); - 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; + } + break; + + 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; + } + } + } + } - case ComparisonValueType.Undefined: - result.push({ - comparedName, - comparisonType: operand.operator.startsWith('!') - ? NullishComparisonType.NotStrictEqualUndefined - : NullishComparisonType.StrictEqualUndefined, - isYoda, - node: operand, - type: OperandValidity.Valid, - }); - 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({ + comparedName, + comparisonType, + comparisonValue: comparedValue, + isYoda, + node: operand, + type: OperandValidity.Last, + }); + continue; + } - default: - // x === something :( - result.push({ type: OperandValidity.Invalid }); - continue; + case '!=': + case '!==': { + const comparisonType = + operand.operator === '!=' + ? ComparisonType.NotEqual + : ComparisonType.NotStrictEqual; + result.push({ + comparedName, + comparisonType, + comparisonValue: comparedValue, + isYoda, + node: operand, + type: OperandValidity.Last, + }); + continue; } } } @@ -374,4 +429,32 @@ 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 { + comparedName, + comparedValue, + isYoda, + }; + } + if (!isLeftMemberExpression && isRightMemberExpression) { + const [comparedName, comparedValue] = [right, left]; + + isYoda = true; + return { + comparedName, + comparedValue, + isYoda, + }; + } + 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); } 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 39125b9dc891..8d15ad78e574 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 @@ -100,7 +100,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 b72289d020b2..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 @@ -698,6 +698,1540 @@ describe('|| {}', () => { }); }); +describe('chain ending with comparison', () => { + ruleTester.run('prefer-optional-chain', rule, { + 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 && 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 }], + 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: '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 }; + 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 && 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; + `, + }, + { + 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; + 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; + `, + }, + { + 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 == 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 }], + 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 == 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 }], + 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 || 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; + `, + }, + // 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;', + '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 && 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;', + '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 != 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;', + ` + 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';", + '!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 && foo.bar != undefined;', + '!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 === 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;', + '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 == 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;', + '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 == 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;', + '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 == undefined;', + 'foo || foo.bar == null;', + '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 == undefined;', + 'foo != null || 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 !== null;', + ], + }); +}); + describe('hand-crafted cases', () => { ruleTester.run('prefer-optional-chain', rule, { invalid: [ @@ -714,8 +2248,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: ` @@ -726,8 +2259,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; `, }, @@ -956,53 +2488,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; @@ -1809,6 +3299,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; @@ -1991,16 +3489,58 @@ describe('base cases', () => { 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, - }), + 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(); + `, + 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; + `, + }, + ], }); }); @@ -2024,16 +3564,58 @@ describe('base cases', () => { 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, - }), + 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(); + `, + 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; + `, + }, + ], }); }); @@ -2072,21 +3654,63 @@ describe('base cases', () => { 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, - }), + 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(); + `, + 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; + `, + }, + ], }); }); @@ -2114,20 +3738,62 @@ describe('base cases', () => { 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: '||', - }), + 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(); + `, + 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; + `, + }, + ], }); });