Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed detection of optional chains containing a reference #59144

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27143,7 +27143,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function optionalChainContainsReference(source: Node, target: Node) {
while (isOptionalChain(source)) {
source = source.expression;
if (isMatchingReference(source, target)) {
if (isMatchingReference(target, source)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an extra refactor should be made here.

isMatchingReference accepts reference as the first parameter but optionalChainContainsReference accepts it as the second one (!). That caused the issue here as both of them refer to both of the parameters as source and target - so it wasn't apparent that the reference (called a target here) is passed to isMatchingReference at the position that is used for candidate expressions (also called a target by that function).

I think it would make sense to:

  1. rename those parameters to something like expression and reference or maybe candidate and reference. isMatchingReference sometimes calls recursively itself with adjusted source (what I propose to call a reference here) but I think that doesn't impact the proposed naming
  2. use consistent order of parameters. I think what optionalChainContainsReference uses is better (reference passed as the second argument to it) but isMatchingReference is likely an older function so maybe the team would prefer that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is nearly identical to containsMatchingReference aside from only supporting optional chaining. I'm not sure argument order is the issue here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isMatchingReference walks both source and target to different extents. It may just be that some check on one side or the other is not exhaustive enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is nearly identical to containsMatchingReference aside from only supporting optional chaining.

Yeah, it is almost identical - and notice that source and target are passed down to isMatchingReference by them at different parameter positions. That only makes me believe more that the argument order is the issue here.

isMatchingReference walks both source and target to different extents. It may just be that some check on one side or the other is not exhaustive enough.

It may be that. But I'm not sure to what extent that function should be walking both so I'm hesitant to make changes there as previous authors haven't embed that logic related to optional chains there.


I re-read the code now and analyzed this again and I'm only more convinced that the argument order is incorrect here. Fixing this in isMatchingReference (without changing the adjusted argument order) would require fixing this at its end (in the switch/case that checks source):

            case SyntaxKind.BinaryExpression:
                return (isBinaryExpression(source) && source.operatorToken.kind === SyntaxKind.CommaToken && isMatchingReference(source.right, target));

The case here doesn't fit what is handled here at all, it does fit what is handled on the target side though:

            case SyntaxKind.BinaryExpression:
                return (isAssignmentExpression(target) && isMatchingReference(source, target.left)) ||
                    (isBinaryExpression(target) && target.operatorToken.kind === SyntaxKind.CommaToken && isMatchingReference(source, target.right));

So given that and the fact that arguments really seem to be semantically flipped when judging by other calls to isMatchingReference, I'm inclined to argue the current fix is the correct one.

return true;
}
}
Expand Down
106 changes: 106 additions & 0 deletions tests/baselines/reference/controlFlowOptionalChain4.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//// [tests/cases/conformance/controlFlow/controlFlowOptionalChain4.ts] ////

=== controlFlowOptionalChain4.ts ===
// https://github.com/microsoft/TypeScript/issues/56998

type Type = {
>Type : Symbol(Type, Decl(controlFlowOptionalChain4.ts, 0, 0))

id: number;
>id : Symbol(id, Decl(controlFlowOptionalChain4.ts, 2, 13))

};

type InferenceInfo = {
>InferenceInfo : Symbol(InferenceInfo, Decl(controlFlowOptionalChain4.ts, 4, 2))

typeParameter: Type;
>typeParameter : Symbol(typeParameter, Decl(controlFlowOptionalChain4.ts, 6, 22))
>Type : Symbol(Type, Decl(controlFlowOptionalChain4.ts, 0, 0))

impliedArity?: number;
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))

};

declare function getInferenceInfoForType(type: Type): InferenceInfo | undefined;
>getInferenceInfoForType : Symbol(getInferenceInfoForType, Decl(controlFlowOptionalChain4.ts, 9, 2))
>type : Symbol(type, Decl(controlFlowOptionalChain4.ts, 11, 41))
>Type : Symbol(Type, Decl(controlFlowOptionalChain4.ts, 0, 0))
>InferenceInfo : Symbol(InferenceInfo, Decl(controlFlowOptionalChain4.ts, 4, 2))

function fn1(t1: Type, t2: Type) {
>fn1 : Symbol(fn1, Decl(controlFlowOptionalChain4.ts, 11, 80))
>t1 : Symbol(t1, Decl(controlFlowOptionalChain4.ts, 13, 13))
>Type : Symbol(Type, Decl(controlFlowOptionalChain4.ts, 0, 0))
>t2 : Symbol(t2, Decl(controlFlowOptionalChain4.ts, 13, 22))
>Type : Symbol(Type, Decl(controlFlowOptionalChain4.ts, 0, 0))

let info = getInferenceInfoForType(t1);
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 14, 5))
>getInferenceInfoForType : Symbol(getInferenceInfoForType, Decl(controlFlowOptionalChain4.ts, 9, 2))
>t1 : Symbol(t1, Decl(controlFlowOptionalChain4.ts, 13, 13))

if (info?.impliedArity !== undefined) {
>info?.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 14, 5))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>undefined : Symbol(undefined)

info.impliedArity;
>info.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 14, 5))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))

} else if ((info = getInferenceInfoForType(t2))?.impliedArity !== undefined) {
>(info = getInferenceInfoForType(t2))?.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 14, 5))
>getInferenceInfoForType : Symbol(getInferenceInfoForType, Decl(controlFlowOptionalChain4.ts, 9, 2))
>t2 : Symbol(t2, Decl(controlFlowOptionalChain4.ts, 13, 22))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>undefined : Symbol(undefined)

info.impliedArity;
>info.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 14, 5))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
}
}

function fn2(t1: Type, t2: Type) {
>fn2 : Symbol(fn2, Decl(controlFlowOptionalChain4.ts, 20, 1))
>t1 : Symbol(t1, Decl(controlFlowOptionalChain4.ts, 22, 13))
>Type : Symbol(Type, Decl(controlFlowOptionalChain4.ts, 0, 0))
>t2 : Symbol(t2, Decl(controlFlowOptionalChain4.ts, 22, 22))
>Type : Symbol(Type, Decl(controlFlowOptionalChain4.ts, 0, 0))

let info = getInferenceInfoForType(t1);
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 23, 5))
>getInferenceInfoForType : Symbol(getInferenceInfoForType, Decl(controlFlowOptionalChain4.ts, 9, 2))
>t1 : Symbol(t1, Decl(controlFlowOptionalChain4.ts, 22, 13))

if (info?.impliedArity !== undefined) {
>info?.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 23, 5))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>undefined : Symbol(undefined)

info.impliedArity;
>info.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 23, 5))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))

} else if ((info = getInferenceInfoForType(t2))?.impliedArity) {
>(info = getInferenceInfoForType(t2))?.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 23, 5))
>getInferenceInfoForType : Symbol(getInferenceInfoForType, Decl(controlFlowOptionalChain4.ts, 9, 2))
>t2 : Symbol(t2, Decl(controlFlowOptionalChain4.ts, 22, 22))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))

info.impliedArity;
>info.impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
>info : Symbol(info, Decl(controlFlowOptionalChain4.ts, 23, 5))
>impliedArity : Symbol(impliedArity, Decl(controlFlowOptionalChain4.ts, 7, 22))
}
}

171 changes: 171 additions & 0 deletions tests/baselines/reference/controlFlowOptionalChain4.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//// [tests/cases/conformance/controlFlow/controlFlowOptionalChain4.ts] ////

=== controlFlowOptionalChain4.ts ===
// https://github.com/microsoft/TypeScript/issues/56998

type Type = {
>Type : Type
> : ^^^^

id: number;
>id : number
> : ^^^^^^

};

type InferenceInfo = {
>InferenceInfo : InferenceInfo
> : ^^^^^^^^^^^^^

typeParameter: Type;
>typeParameter : Type
> : ^^^^

impliedArity?: number;
>impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^

};

declare function getInferenceInfoForType(type: Type): InferenceInfo | undefined;
>getInferenceInfoForType : (type: Type) => InferenceInfo | undefined
> : ^ ^^ ^^^^^
>type : Type
> : ^^^^

function fn1(t1: Type, t2: Type) {
>fn1 : (t1: Type, t2: Type) => void
> : ^ ^^ ^^ ^^ ^^^^^^^^^
>t1 : Type
> : ^^^^
>t2 : Type
> : ^^^^

let info = getInferenceInfoForType(t1);
>info : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType(t1) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType : (type: Type) => InferenceInfo | undefined
> : ^ ^^ ^^^^^
>t1 : Type
> : ^^^^

if (info?.impliedArity !== undefined) {
>info?.impliedArity !== undefined : boolean
> : ^^^^^^^
>info?.impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>info : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>undefined : undefined
> : ^^^^^^^^^

info.impliedArity;
>info.impliedArity : number
> : ^^^^^^
>info : InferenceInfo
> : ^^^^^^^^^^^^^
>impliedArity : number
> : ^^^^^^

} else if ((info = getInferenceInfoForType(t2))?.impliedArity !== undefined) {
>(info = getInferenceInfoForType(t2))?.impliedArity !== undefined : boolean
> : ^^^^^^^
>(info = getInferenceInfoForType(t2))?.impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>(info = getInferenceInfoForType(t2)) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>info = getInferenceInfoForType(t2) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>info : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType(t2) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType : (type: Type) => InferenceInfo | undefined
> : ^ ^^ ^^^^^
>t2 : Type
> : ^^^^
>impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>undefined : undefined
> : ^^^^^^^^^

info.impliedArity;
>info.impliedArity : number
> : ^^^^^^
>info : InferenceInfo
> : ^^^^^^^^^^^^^
>impliedArity : number
> : ^^^^^^
}
}

function fn2(t1: Type, t2: Type) {
>fn2 : (t1: Type, t2: Type) => void
> : ^ ^^ ^^ ^^ ^^^^^^^^^
>t1 : Type
> : ^^^^
>t2 : Type
> : ^^^^

let info = getInferenceInfoForType(t1);
>info : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType(t1) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType : (type: Type) => InferenceInfo | undefined
> : ^ ^^ ^^^^^
>t1 : Type
> : ^^^^

if (info?.impliedArity !== undefined) {
>info?.impliedArity !== undefined : boolean
> : ^^^^^^^
>info?.impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>info : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>undefined : undefined
> : ^^^^^^^^^

info.impliedArity;
>info.impliedArity : number
> : ^^^^^^
>info : InferenceInfo
> : ^^^^^^^^^^^^^
>impliedArity : number
> : ^^^^^^

} else if ((info = getInferenceInfoForType(t2))?.impliedArity) {
>(info = getInferenceInfoForType(t2))?.impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>(info = getInferenceInfoForType(t2)) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>info = getInferenceInfoForType(t2) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>info : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType(t2) : InferenceInfo | undefined
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
>getInferenceInfoForType : (type: Type) => InferenceInfo | undefined
> : ^ ^^ ^^^^^
>t2 : Type
> : ^^^^
>impliedArity : number | undefined
> : ^^^^^^^^^^^^^^^^^^

info.impliedArity;
>info.impliedArity : number
> : ^^^^^^
>info : InferenceInfo
> : ^^^^^^^^^^^^^
>impliedArity : number
> : ^^^^^^
}
}

33 changes: 33 additions & 0 deletions tests/cases/conformance/controlFlow/controlFlowOptionalChain4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @strict: true
// @noEmit: true

// https://github.com/microsoft/TypeScript/issues/56998

type Type = {
id: number;
};

type InferenceInfo = {
typeParameter: Type;
impliedArity?: number;
};

declare function getInferenceInfoForType(type: Type): InferenceInfo | undefined;

function fn1(t1: Type, t2: Type) {
let info = getInferenceInfoForType(t1);
if (info?.impliedArity !== undefined) {
info.impliedArity;
} else if ((info = getInferenceInfoForType(t2))?.impliedArity !== undefined) {
info.impliedArity;
}
}

function fn2(t1: Type, t2: Type) {
let info = getInferenceInfoForType(t1);
if (info?.impliedArity !== undefined) {
info.impliedArity;
} else if ((info = getInferenceInfoForType(t2))?.impliedArity) {
info.impliedArity;
}
}