diff --git a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts index 8ba1be086c19..9862ee6efc43 100644 --- a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts +++ b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts @@ -96,8 +96,7 @@ export default createRule({ const lhs = lhsName.typeAnnotation?.typeAnnotation; if ( - !rhs || - rhs.type !== AST_NODE_TYPES.NewExpression || + rhs?.type !== AST_NODE_TYPES.NewExpression || rhs.callee.type !== AST_NODE_TYPES.Identifier ) { return; diff --git a/packages/eslint-plugin/src/rules/no-dupe-class-members.ts b/packages/eslint-plugin/src/rules/no-dupe-class-members.ts index 4bdfd016eb3c..76f52a5ee7e1 100644 --- a/packages/eslint-plugin/src/rules/no-dupe-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-dupe-class-members.ts @@ -40,10 +40,7 @@ export default createRule({ return; } - if ( - node.value && - node.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression - ) { + if (node.value?.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression) { return; } diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index d491da6227d4..2c0294f9661c 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -657,10 +657,7 @@ export default createRule({ } function checkJSXAttribute(node: TSESTree.JSXAttribute): void { - if ( - node.value == null || - node.value.type !== AST_NODE_TYPES.JSXExpressionContainer - ) { + if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) { return; } const expressionContainer = services.esTreeNodeToTSNodeMap.get( diff --git a/packages/eslint-plugin/src/rules/no-redeclare.ts b/packages/eslint-plugin/src/rules/no-redeclare.ts index 841f36323e03..77310981887c 100644 --- a/packages/eslint-plugin/src/rules/no-redeclare.ts +++ b/packages/eslint-plugin/src/rules/no-redeclare.ts @@ -271,9 +271,8 @@ export default createRule({ // Node.js or ES modules has a special scope. if ( scope.type === ScopeType.global && - scope.childScopes[0] && // The special scope's block is the Program node. - scope.block === scope.childScopes[0].block + scope.block === scope.childScopes[0]?.block ) { findVariablesInScope(scope.childScopes[0]); } diff --git a/packages/eslint-plugin/src/rules/no-type-alias.ts b/packages/eslint-plugin/src/rules/no-type-alias.ts index 0dae356d03f8..5a40d7f690e2 100644 --- a/packages/eslint-plugin/src/rules/no-type-alias.ts +++ b/packages/eslint-plugin/src/rules/no-type-alias.ts @@ -229,8 +229,7 @@ export default createRule({ if ( type.node.type === AST_NODE_TYPES.TSTypeOperator && ['keyof', 'readonly'].includes(type.node.operator) && - type.node.typeAnnotation && - type.node.typeAnnotation.type === AST_NODE_TYPES.TSTupleType + type.node.typeAnnotation?.type === AST_NODE_TYPES.TSTupleType ) { return true; } diff --git a/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts b/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts index 774c65e53771..4192bec216df 100644 --- a/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts +++ b/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts @@ -30,8 +30,7 @@ export default createRule({ const moduleType = context.sourceCode.getTokenBefore(node.id); if ( - moduleType && - moduleType.type === AST_TOKEN_TYPES.Identifier && + moduleType?.type === AST_TOKEN_TYPES.Identifier && moduleType.value === 'module' ) { context.report({ diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 0956b558b10a..83fa8e5a6fc3 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -568,8 +568,7 @@ export default createRule({ } if ( - !assignmentExpression || - assignmentExpression.type !== AST_NODE_TYPES.AssignmentExpression || + assignmentExpression?.type !== AST_NODE_TYPES.AssignmentExpression || !isMemberAccessLike(assignmentExpression.left) ) { return; 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 a2b11134e540..f402fefce7d1 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,15 @@ import { } from '../../util'; import { checkNullishAndReport } from './checkNullishAndReport'; import { compareNodes, NodeComparisonResult } from './compareNodes'; -import { ComparisonType, NullishComparisonType } from './gatherLogicalOperands'; +import { + ComparisonType, + NullishComparisonType, + Yoda, +} from './gatherLogicalOperands'; + +type LastChainOperandForReport = Omit & { + isYoda: boolean; +}; function includesType( parserServices: ParserServicesWithTypeInformation, @@ -257,6 +265,57 @@ const analyzeOrChainOperand: OperandAnalyzer = ( } }; +const resolveOperandSubset = ( + previousOperand: ValidOperand, + lastChainOperand: LastChainOperand, +) => { + const isNameSubset = + compareNodes( + previousOperand.comparedName, + lastChainOperand.comparedName, + ) === NodeComparisonResult.Subset; + + if (lastChainOperand.yoda !== Yoda.Unknown) { + return { + comparedName: lastChainOperand.comparedName, + comparisonValue: lastChainOperand.comparisonValue, + isSubset: isNameSubset, + isYoda: lastChainOperand.yoda === Yoda.Yes, + }; + } + + const isValueSubset = + compareNodes( + previousOperand.comparedName, + lastChainOperand.comparisonValue, + ) === NodeComparisonResult.Subset; + + if (isNameSubset && !isValueSubset) { + return { + comparedName: lastChainOperand.comparedName, + comparisonValue: lastChainOperand.comparisonValue, + isSubset: true, + isYoda: false, + }; + } + + if (!isNameSubset && isValueSubset) { + return { + comparedName: lastChainOperand.comparisonValue, + comparisonValue: lastChainOperand.comparedName, + isSubset: true, + isYoda: true, + }; + } + + return { + comparedName: lastChainOperand.comparisonValue, + comparisonValue: lastChainOperand.comparisonValue, + isSubset: false, + isYoda: true, + }; +}; + /** * Returns the range that needs to be reported from the chain. * @param chain The chain of logical expressions. @@ -306,7 +365,7 @@ function getReportDescriptor( operator: '&&' | '||', options: PreferOptionalChainOptions, subChain: ValidOperand[], - lastChain: (LastChainOperand | ValidOperand) | undefined, + lastChain: (LastChainOperandForReport | ValidOperand) | undefined, ): ReportDescriptor { const chain = lastChain ? [...subChain, lastChain] : subChain; const lastOperand = chain[chain.length - 1]; @@ -606,7 +665,9 @@ 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; + let lastChain: LastChainOperandForReport | ValidOperand | undefined = + undefined; + const maybeReportThenReset = ( newChainSeed?: readonly [ValidOperand, ...ValidOperand[]], ): void => { @@ -710,23 +771,27 @@ export function analyzeChain( const lastOperand = subChain.flat().at(-1); if (lastOperand && lastChainOperand) { - const comparisonResult = compareNodes( - lastOperand.comparedName, - lastChainOperand.comparedName, - ); const isValidLastChainOperand = operator === '&&' ? isValidAndLastChainOperand : isValidOrLastChainOperand; + + const { comparedName, comparisonValue, isSubset, isYoda } = + resolveOperandSubset(lastOperand, lastChainOperand); if ( - comparisonResult === NodeComparisonResult.Subset && + isSubset && isValidLastChainOperand( - lastChainOperand.comparisonValue, + comparisonValue, lastChainOperand.comparisonType, parserServices, ) ) { - lastChain = lastChainOperand; + lastChain = { + ...lastChainOperand, + comparedName, + comparisonValue, + isYoda, + }; } } // 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 9092e1e67a75..22e1b6fa048c 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 @@ -18,6 +18,11 @@ import type { PreferOptionalChainOptions } from './PreferOptionalChainOptions'; import { isReferenceToGlobalFunction, isTypeFlagSet } from '../../util'; +export const enum Yoda { + Yes, + No, + Unknown, +} const enum ComparisonValueType { Null = 'Null', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum Undefined = 'Undefined', @@ -66,7 +71,7 @@ export interface LastChainOperand { comparedName: TSESTree.Node; comparisonType: ComparisonType; comparisonValue: TSESTree.Node; - isYoda: boolean; + yoda: Yoda; node: TSESTree.BinaryExpression; type: OperandValidity.Last; } @@ -257,7 +262,7 @@ export function gatherLogicalOperands( // x !== something :( const binaryComparisonChain = getBinaryComparisonChain(operand); if (binaryComparisonChain) { - const { comparedName, comparedValue, isYoda } = binaryComparisonChain; + const { comparedName, comparedValue, yoda } = binaryComparisonChain; switch (operand.operator) { case '==': @@ -270,9 +275,9 @@ export function gatherLogicalOperands( comparedName, comparisonType, comparisonValue: comparedValue, - isYoda, node: operand, type: OperandValidity.Last, + yoda, }); continue; } @@ -287,9 +292,9 @@ export function gatherLogicalOperands( comparedName, comparisonType, comparisonValue: comparedValue, - isYoda, node: operand, type: OperandValidity.Last, + yoda, }); continue; } @@ -430,29 +435,46 @@ export function gatherLogicalOperands( return null; } + function isMemberBasedExpression( + node: TSESTree.Expression | TSESTree.PrivateIdentifier, + ): node is TSESTree.CallExpression | TSESTree.MemberExpression { + if (node.type === AST_NODE_TYPES.MemberExpression) { + return true; + } + if ( + node.type === AST_NODE_TYPES.CallExpression && + node.callee.type === AST_NODE_TYPES.MemberExpression + ) { + return true; + } + return false; + } + 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; + const isLeftMemberExpression = isMemberBasedExpression(left); + const isRightMemberExpression = isMemberBasedExpression(right); if (isLeftMemberExpression && !isRightMemberExpression) { const [comparedName, comparedValue] = [left, right]; return { comparedName, comparedValue, - isYoda, + yoda: Yoda.No, }; } if (!isLeftMemberExpression && isRightMemberExpression) { const [comparedName, comparedValue] = [right, left]; - - isYoda = true; return { comparedName, comparedValue, - isYoda, + yoda: Yoda.Yes, + }; + } + if (isLeftMemberExpression && isRightMemberExpression) { + return { + comparedName: left, + comparedValue: right, + yoda: Yoda.Unknown, }; } return null; diff --git a/packages/eslint-plugin/src/rules/unified-signatures.ts b/packages/eslint-plugin/src/rules/unified-signatures.ts index 47e148197f1b..bf3740b6683d 100644 --- a/packages/eslint-plugin/src/rules/unified-signatures.ts +++ b/packages/eslint-plugin/src/rules/unified-signatures.ts @@ -302,11 +302,7 @@ export default createRule({ } function isThisParam(param: TSESTree.Parameter | undefined): boolean { - return ( - param != null && - param.type === AST_NODE_TYPES.Identifier && - param.name === 'this' - ); + return param?.type === AST_NODE_TYPES.Identifier && param.name === 'this'; } function isThisVoidParam(param: TSESTree.Parameter | undefined) { @@ -510,7 +506,7 @@ export default createRule({ a: TSESTree.TypeNode | undefined, b: TSESTree.TypeNode | undefined, ): boolean { - return a === b || (a != null && b != null && a.type === b.type); + return a === b || (a != null && a.type === b?.type); } /* Returns the first index where `a` and `b` differ. */ @@ -595,10 +591,7 @@ export default createRule({ containingNode?: ContainingNode, ): void { key ??= getOverloadKey(signature); - if ( - currentScope && - (containingNode ?? signature).parent === currentScope.parent - ) { + if ((containingNode ?? signature).parent === currentScope?.parent) { const overloads = currentScope.overloads.get(key); if (overloads != null) { overloads.push(signature); diff --git a/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts b/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts index b72ece7f9a25..3580aeeb0081 100644 --- a/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts +++ b/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts @@ -315,7 +315,7 @@ abstract class ThisScope extends Visitor { // ``` case AST_NODE_TYPES.VariableDeclarator: { const value = firstDef.node.init; - if (value == null || value.type !== AST_NODE_TYPES.ThisExpression) { + if (value?.type !== AST_NODE_TYPES.ThisExpression) { return null; } diff --git a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts index b9aeb65dbede..09aa1e1a013b 100644 --- a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts +++ b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts @@ -125,7 +125,7 @@ function isConstructorArgument( function isPropertyOfObjectWithType( property: TSESTree.Node | undefined, ): boolean { - if (!property || property.type !== AST_NODE_TYPES.Property) { + if (property?.type !== AST_NODE_TYPES.Property) { return false; } const objectExpr = property.parent; diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 4355e8d83f60..a39235e17aad 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -66,10 +66,7 @@ export function arraysAreEqual( ): boolean { return ( a === b || - (a != null && - b != null && - a.length === b.length && - a.every((x, idx) => eq(x, b[idx]))) + (a != null && a.length === b?.length && a.every((x, idx) => eq(x, b[idx]))) ); } 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 6639951b66e4..47227a63dc93 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 @@ -1338,6 +1338,266 @@ describe('chain ending with comparison', () => { errors: [{ messageId: 'preferOptionalChain', suggestions: null }], output: `undefined !== foo?.bar?.baz;`, }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a != null && a.b === foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a?.b === foo.three; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a != null && a.b() === foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a?.b() === foo.three; + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a != null && a.b == foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a?.b == foo.three; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a != null && a.b() == foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a?.b() == foo.three; + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + a != null && a.b !== foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + a?.b !== foo.three; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + a != null && a.b() != foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + a?.b() != foo.three; + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a === null || a.b !== foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a?.b !== foo.three; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a === null || a.b() != foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a?.b() != foo.three; + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + a === null || a.b == foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + a?.b == foo.three; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + a === null || a.b() === foo.three; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + a?.b() === foo.three; + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a != null && foo.three === a.b; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + foo.three === a?.b; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a != null && foo.three === a.b(); + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + foo.three === a?.b(); + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a != null && foo.three == a.b; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + foo.three == a?.b; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a != null && foo.three == a.b(); + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + foo.three == a?.b(); + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + a != null && foo.three !== a.b; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + foo.three !== a?.b; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + a != null && foo.three != a.b(); + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + foo.three != a?.b(); + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + a == null || foo.three !== a.b; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: 3 }; + foo.three !== a?.b; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + a == null || foo.three != a.b(); + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: 3 }; + foo.three != a?.b(); + `, + }, + { + code: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + a == null || foo.three == a.b; + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: number } | null; + declare const foo: { three: undefined }; + foo.three == a?.b; + `, + }, + { + code: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + a == null || foo.three === a.b(); + `, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + output: ` + declare const a: { b: () => number } | null; + declare const foo: { three: undefined }; + foo.three === a?.b(); + `, + }, ], valid: [ 'foo && foo.bar == undeclaredVar;', @@ -1345,6 +1605,8 @@ describe('chain ending with comparison', () => { 'foo && foo.bar == undefined;', 'foo && foo.bar === undeclaredVar;', 'foo && foo.bar === undefined;', + 'foo && foo.bar === too.bar;', + 'foo && foo.bar === foo.baz;', 'foo && foo.bar !== 0;', 'foo && foo.bar !== 1;', "foo && foo.bar !== '123';", @@ -1353,6 +1615,8 @@ describe('chain ending with comparison', () => { 'foo && foo.bar !== true;', 'foo && foo.bar !== null;', 'foo && foo.bar !== undeclaredVar;', + 'foo && foo.bar !== too.bar;', + 'foo && foo.bar !== foo.baz;', 'foo && foo.bar != 0;', 'foo && foo.bar != 1;', "foo && foo.bar != '123';", @@ -1360,6 +1624,8 @@ describe('chain ending with comparison', () => { 'foo && foo.bar != false;', 'foo && foo.bar != true;', 'foo && foo.bar != undeclaredVar;', + 'foo && foo.bar != too.bar;', + 'foo && foo.bar != foo.baz;', 'foo != null && foo.bar == undeclaredVar;', 'foo != null && foo.bar == null;', 'foo != null && foo.bar == undefined;', @@ -1380,6 +1646,38 @@ describe('chain ending with comparison', () => { 'foo != null && foo.bar != false;', 'foo != null && foo.bar != true;', 'foo != null && foo.bar != undeclaredVar;', + ` + declare const foo: { bar: number; baz: number } | null; + foo != null && foo.bar == foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => number } | null; + foo != null && foo.bar == foo.baz(); + `, + ` + declare const foo: { bar: number; baz: number } | null; + foo != null && foo.bar === foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => number } | null; + foo != null && foo.bar === foo.baz(); + `, + ` + declare const foo: { bar: number; baz: undefined } | null; + foo != null && foo.bar != foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => undefined } | null; + foo != null && foo.bar != foo.baz(); + `, + ` + declare const foo: { bar: number; baz: undefined } | null; + foo != null && foo.bar !== foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => undefined } | null; + foo != null && foo.bar !== foo.baz(); + `, ` declare const foo: { bar: number }; foo && foo.bar == undeclaredVar; @@ -1838,6 +2136,38 @@ describe('chain ending with comparison', () => { 'foo == null || foo.bar == undeclaredVar;', 'foo == null || foo.bar !== undeclaredVar;', 'foo == null || foo.bar !== undefined;', + ` + declare const foo: { bar: number; baz: number } | null; + foo == null || foo.bar != foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => number } | null; + foo == null || foo.bar != foo.baz(); + `, + ` + declare const foo: { bar: number; baz: undefined } | null; + foo == null || foo.bar === foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => undefined } | null; + foo == null || foo.bar === foo.baz(); + `, + ` + declare const foo: { bar: number; baz: undefined } | null; + foo == null || foo.bar == foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => undefined } | null; + foo == null || foo.bar == foo.baz(); + `, + ` + declare const foo: { bar: number; baz: number } | null; + foo == null || foo.bar !== foo.baz; + `, + ` + declare const foo: { bar: number; baz: () => number } | null; + foo == null || foo.bar !== foo.baz(); + `, 'foo || foo.bar != 0;', 'foo || foo.bar != 1;', "foo || foo.bar != '123';", diff --git a/packages/scope-manager/src/referencer/Referencer.ts b/packages/scope-manager/src/referencer/Referencer.ts index 1777491588ae..93bf570bd320 100644 --- a/packages/scope-manager/src/referencer/Referencer.ts +++ b/packages/scope-manager/src/referencer/Referencer.ts @@ -489,8 +489,7 @@ export class Referencer extends Visitor { // NOTE: In ES6, ForStatement dynamically generates per iteration environment. However, this is // a static analyzer, we only generate one scope for ForStatement. if ( - node.init && - node.init.type === AST_NODE_TYPES.VariableDeclaration && + node.init?.type === AST_NODE_TYPES.VariableDeclaration && node.init.kind !== 'var' ) { this.scopeManager.nestForScope(node); diff --git a/packages/type-utils/src/predicates.ts b/packages/type-utils/src/predicates.ts index cc40098b6002..015b092b4a06 100644 --- a/packages/type-utils/src/predicates.ts +++ b/packages/type-utils/src/predicates.ts @@ -129,7 +129,7 @@ export function typeIsOrHasBaseType( for (const baseType of typeAndBaseTypes) { const baseSymbol = baseType.getSymbol(); - if (baseSymbol && baseSymbol.name === parentSymbol.name) { + if (baseSymbol?.name === parentSymbol.name) { return true; } } diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 1f5778b94e2e..cf452705a825 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -908,10 +908,7 @@ export class Converter { this.#checkForStatementDeclaration(node.initializer, node.kind); return this.createNode(node, { type: AST_NODE_TYPES.ForOfStatement, - await: Boolean( - node.awaitModifier && - node.awaitModifier.kind === SyntaxKind.AwaitKeyword, - ), + await: node.awaitModifier?.kind === SyntaxKind.AwaitKeyword, body: this.convertChild(node.statement), left: this.convertPattern(node.initializer), right: this.convertChild(node.expression), @@ -1597,10 +1594,8 @@ export class Converter { } else { result = this.createNode(node, { type: AST_NODE_TYPES.Property, - computed: Boolean( - node.propertyName && - node.propertyName.kind === SyntaxKind.ComputedPropertyName, - ), + computed: + node.propertyName?.kind === SyntaxKind.ComputedPropertyName, key: this.convertChild(node.propertyName ?? node.name), kind: 'init', method: false,