Skip to content

🤖 Cherry-pick PR #34588 into release-3.7 #34988

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 0 additions & 4 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1556,10 +1556,6 @@ namespace ts {
}
}

function isOutermostOptionalChain(node: OptionalChain) {
return !isOptionalChain(node.parent) || isOptionalChainRoot(node.parent) || node !== node.parent.expression;
}

function bindOptionalExpression(node: Expression, trueTarget: FlowLabel, falseTarget: FlowLabel) {
doWithConditionalBranches(bind, node, trueTarget, falseTarget);
if (!isOptionalChain(node) || isOutermostOptionalChain(node)) {
Expand Down
99 changes: 57 additions & 42 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8670,14 +8670,23 @@ namespace ts {
return result;
}

function getOptionalCallSignature(signature: Signature) {
return signatureIsOptionalCall(signature) ? signature :
(signature.optionalCallSignatureCache || (signature.optionalCallSignatureCache = createOptionalCallSignature(signature)));
function getOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags): Signature {
if ((signature.flags & SignatureFlags.CallChainFlags) === callChainFlags) {
return signature;
}
if (!signature.optionalCallSignatureCache) {
signature.optionalCallSignatureCache = {};
}
const key = callChainFlags === SignatureFlags.IsInnerCallChain ? "inner" : "outer";
return signature.optionalCallSignatureCache[key]
|| (signature.optionalCallSignatureCache[key] = createOptionalCallSignature(signature, callChainFlags));
}

function createOptionalCallSignature(signature: Signature) {
function createOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags) {
Debug.assert(callChainFlags === SignatureFlags.IsInnerCallChain || callChainFlags === SignatureFlags.IsOuterCallChain,
"An optional call signature can either be for an inner call chain or an outer call chain, but not both.");
const result = cloneSignature(signature);
result.flags |= SignatureFlags.IsOptionalCall;
result.flags |= callChainFlags;
return result;
}

Expand Down Expand Up @@ -10292,9 +10301,12 @@ namespace ts {
signature.unionSignatures ? getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype) :
getReturnTypeFromAnnotation(signature.declaration!) ||
(nodeIsMissing((<FunctionLikeDeclaration>signature.declaration).body) ? anyType : getReturnTypeFromBody(<FunctionLikeDeclaration>signature.declaration));
if (signatureIsOptionalCall(signature)) {
if (signature.flags & SignatureFlags.IsInnerCallChain) {
type = addOptionalTypeMarker(type);
}
else if (signature.flags & SignatureFlags.IsOuterCallChain) {
type = getOptionalType(type);
}
if (!popTypeResolution()) {
if (signature.declaration) {
const typeNode = getEffectiveReturnTypeNode(signature.declaration);
Expand Down Expand Up @@ -16743,8 +16755,8 @@ namespace ts {
return strictNullChecks ? filterType(type, isNotOptionalTypeMarker) : type;
}

function propagateOptionalTypeMarker(type: Type, wasOptional: boolean) {
return wasOptional ? addOptionalTypeMarker(type) : type;
function propagateOptionalTypeMarker(type: Type, node: OptionalChain, wasOptional: boolean) {
return wasOptional ? isOutermostOptionalChain(node) ? getOptionalType(type) : addOptionalTypeMarker(type) : type;
}

function getOptionalExpressionType(exprType: Type, expression: Expression) {
Expand Down Expand Up @@ -22811,7 +22823,7 @@ namespace ts {
function checkPropertyAccessChain(node: PropertyAccessChain) {
const leftType = checkExpression(node.expression);
const nonOptionalType = getOptionalExpressionType(leftType, node.expression);
return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), nonOptionalType !== leftType);
return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), node, nonOptionalType !== leftType);
}

function checkQualifiedName(node: QualifiedName) {
Expand Down Expand Up @@ -23243,7 +23255,7 @@ namespace ts {
function checkElementAccessChain(node: ElementAccessChain) {
const exprType = checkExpression(node.expression);
const nonOptionalType = getOptionalExpressionType(exprType, node.expression);
return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), nonOptionalType !== exprType);
return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), node, nonOptionalType !== exprType);
}

function checkElementAccessExpression(node: ElementAccessExpression, exprType: Type): Type {
Expand Down Expand Up @@ -23348,7 +23360,7 @@ namespace ts {
// interface B extends A { (x: 'foo'): string }
// const b: B;
// b('foo') // <- here overloads should be processed as [(x:'foo'): string, (x: string): void]
function reorderCandidates(signatures: readonly Signature[], result: Signature[], isOptionalCall: boolean): void {
function reorderCandidates(signatures: readonly Signature[], result: Signature[], callChainFlags: SignatureFlags): void {
let lastParent: Node | undefined;
let lastSymbol: Symbol | undefined;
let cutoffIndex = 0;
Expand Down Expand Up @@ -23390,7 +23402,7 @@ namespace ts {
spliceIndex = index;
}

result.splice(spliceIndex, 0, isOptionalCall ? getOptionalCallSignature(signature) : signature);
result.splice(spliceIndex, 0, callChainFlags ? getOptionalCallSignature(signature, callChainFlags) : signature);
}
}

Expand Down Expand Up @@ -24056,7 +24068,7 @@ namespace ts {
return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount);
}

function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, isOptionalCall: boolean, fallbackError?: DiagnosticMessage): Signature {
function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, fallbackError?: DiagnosticMessage): Signature {
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
const isDecorator = node.kind === SyntaxKind.Decorator;
const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node);
Expand All @@ -24075,7 +24087,7 @@ namespace ts {

const candidates = candidatesOutArray || [];
// reorderCandidates fills up the candidates array directly
reorderCandidates(signatures, candidates, isOptionalCall);
reorderCandidates(signatures, candidates, callChainFlags);
if (!candidates.length) {
if (reportErrors) {
diagnostics.add(getDiagnosticForCallNode(node, Diagnostics.Call_target_does_not_contain_any_signatures));
Expand Down Expand Up @@ -24462,22 +24474,25 @@ namespace ts {
const baseTypeNode = getEffectiveBaseTypeNode(getContainingClass(node)!);
if (baseTypeNode) {
const baseConstructors = getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.typeArguments, baseTypeNode);
return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, /*isOptional*/ false);
return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, SignatureFlags.None);
}
}
return resolveUntypedCall(node);
}

let isOptional: boolean;
let callChainFlags: SignatureFlags;
let funcType = checkExpression(node.expression);
if (isCallChain(node)) {
const nonOptionalType = getOptionalExpressionType(funcType, node.expression);
isOptional = nonOptionalType !== funcType;
callChainFlags = nonOptionalType === funcType ? SignatureFlags.None :
isOutermostOptionalChain(node) ? SignatureFlags.IsOuterCallChain :
SignatureFlags.IsInnerCallChain;
funcType = nonOptionalType;
}
else {
isOptional = false;
callChainFlags = SignatureFlags.None;
}

funcType = checkNonNullTypeWithReporter(
funcType,
node.expression,
Expand Down Expand Up @@ -24553,7 +24568,7 @@ namespace ts {
return resolveErrorCall(node);
}

return resolveCall(node, callSignatures, candidatesOutArray, checkMode, isOptional);
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags);
}

function isGenericFunctionReturningFunction(signature: Signature) {
Expand Down Expand Up @@ -24624,7 +24639,7 @@ namespace ts {
return resolveErrorCall(node);
}

return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
}

// If expressionType's apparent type is an object type with no construct signatures but
Expand All @@ -24633,7 +24648,7 @@ namespace ts {
// operation is Any. It is an error to have a Void this type.
const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call);
if (callSignatures.length) {
const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
if (!noImplicitAny) {
if (signature.declaration && !isJSConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) {
error(node, Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword);
Expand Down Expand Up @@ -24848,7 +24863,7 @@ namespace ts {
return resolveErrorCall(node);
}

return resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
}

/**
Expand Down Expand Up @@ -24911,7 +24926,7 @@ namespace ts {
return resolveErrorCall(node);
}

return resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false, headMessage);
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None, headMessage);
}

function createSignatureForJSXIntrinsic(node: JsxOpeningLikeElement, result: Type): Signature {
Expand Down Expand Up @@ -24963,7 +24978,7 @@ namespace ts {
return resolveErrorCall(node);
}

return resolveCall(node, signatures, candidatesOutArray, checkMode, /*isOptional*/ false);
return resolveCall(node, signatures, candidatesOutArray, checkMode, SignatureFlags.None);
}

/**
Expand Down Expand Up @@ -27436,6 +27451,20 @@ namespace ts {
}
}

function getReturnTypeOfSingleNonGenericCallSignature(funcType: Type) {
const signature = getSingleCallSignature(funcType);
if (signature && !signature.typeParameters) {
return getReturnTypeOfSignature(signature);
}
}

function getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr: CallChain) {
const funcType = checkExpression(expr.expression);
const nonOptionalType = getOptionalExpressionType(funcType, expr.expression);
const returnType = getReturnTypeOfSingleNonGenericCallSignature(funcType);
return returnType && propagateOptionalTypeMarker(returnType, expr, nonOptionalType !== funcType);
}

/**
* Returns the type of an expression. Unlike checkExpression, this function is simply concerned
* with computing the type and may not fully check all contained sub-expressions for errors.
Expand All @@ -27447,21 +27476,10 @@ namespace ts {
// Optimize for the common case of a call to a function with a single non-generic call
// signature where we can just fetch the return type without checking the arguments.
if (isCallExpression(expr) && expr.expression.kind !== SyntaxKind.SuperKeyword && !isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) {
let isOptional: boolean;
let funcType: Type;
if (isCallChain(expr)) {
funcType = checkExpression(expr.expression);
const nonOptionalType = getOptionalExpressionType(funcType, expr.expression);
isOptional = funcType !== nonOptionalType;
funcType = checkNonNullType(nonOptionalType, expr.expression);
}
else {
isOptional = false;
funcType = checkNonNullExpression(expr.expression);
}
const signature = getSingleCallSignature(funcType);
if (signature && !signature.typeParameters) {
return propagateOptionalTypeMarker(getReturnTypeOfSignature(signature), isOptional);
const type = isCallChain(expr) ? getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr) :
getReturnTypeOfSingleNonGenericCallSignature(checkNonNullExpression(expr.expression));
if (type) {
return type;
}
}
else if (isAssertionExpression(expr) && !isConstTypeReference(expr.type)) {
Expand Down Expand Up @@ -36174,7 +36192,4 @@ namespace ts {
return !!(s.flags & SignatureFlags.HasLiteralTypes);
}

export function signatureIsOptionalCall(s: Signature) {
return !!(s.flags & SignatureFlags.IsOptionalCall);
}
}
13 changes: 8 additions & 5 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4673,14 +4673,17 @@ namespace ts {
/* @internal */
export const enum SignatureFlags {
None = 0,
HasRestParameter = 1 << 0, // Indicates last parameter is rest parameter
HasLiteralTypes = 1 << 1, // Indicates signature is specialized
IsOptionalCall = 1 << 2, // Indicates signature comes from a CallChain
HasRestParameter = 1 << 0, // Indicates last parameter is rest parameter
HasLiteralTypes = 1 << 1, // Indicates signature is specialized
IsInnerCallChain = 1 << 2, // Indicates signature comes from a CallChain nested in an outer OptionalChain
IsOuterCallChain = 1 << 3, // Indicates signature comes from a CallChain that is the outermost chain of an optional expression

// We do not propagate `IsOptionalCall` to instantiated signatures, as that would result in us
// We do not propagate `IsInnerCallChain` to instantiated signatures, as that would result in us
// attempting to add `| undefined` on each recursive call to `getReturnTypeOfSignature` when
// instantiating the return type.
PropagatingFlags = HasRestParameter | HasLiteralTypes,

CallChainFlags = IsInnerCallChain | IsOuterCallChain,
}

export interface Signature {
Expand Down Expand Up @@ -4712,7 +4715,7 @@ namespace ts {
/* @internal */
canonicalSignatureCache?: Signature; // Canonical version of signature (deferred)
/* @internal */
optionalCallSignatureCache?: Signature; // Optional chained call version of signature (deferred)
optionalCallSignatureCache?: { inner?: Signature, outer?: Signature }; // Optional chained call version of signature (deferred)
/* @internal */
isolatedSignatureType?: ObjectType; // A manufactured type that just contains the signature for purposes of signature comparison
/* @internal */
Expand Down
27 changes: 22 additions & 5 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5917,6 +5917,11 @@ namespace ts {
|| kind === SyntaxKind.CallExpression);
}

/* @internal */
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
return isOptionalChain(node) && !!node.questionDotToken;
}

/**
* Determines whether a node is the expression preceding an optional chain (i.e. `a` in `a?.b`).
*/
Expand All @@ -5925,6 +5930,23 @@ namespace ts {
return isOptionalChainRoot(node.parent) && node.parent.expression === node;
}

/**
* Determines whether a node is the outermost `OptionalChain` in an ECMAScript `OptionalExpression`:
*
* 1. For `a?.b.c`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.`)
* 2. For `(a?.b.c).d`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.` since parens end the chain)
* 3. For `a?.b.c?.d`, both `a?.b.c` and `a?.b.c?.d` are outermost (`c` is the end of the chain starting at `a?.`, and `d` is
* the end of the chain starting at `c?.`)
* 4. For `a?.(b?.c).d`, both `b?.c` and `a?.(b?.c)d` are outermost (`c` is the end of the chain starting at `b`, and `d` is
* the end of the chain starting at `a?.`)
*/
/* @internal */
export function isOutermostOptionalChain(node: OptionalChain) {
return !isOptionalChain(node.parent) // cases 1 and 2
|| isOptionalChainRoot(node.parent) // case 3
|| node !== node.parent.expression; // case 4
}

export function isNullishCoalesce(node: Node) {
return node.kind === SyntaxKind.BinaryExpression && (<BinaryExpression>node).operatorToken.kind === SyntaxKind.QuestionQuestionToken;
}
Expand Down Expand Up @@ -7246,11 +7268,6 @@ namespace ts {
return node.kind === SyntaxKind.GetAccessor;
}

/* @internal */
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
return isOptionalChain(node) && !!node.questionDotToken;
}

/** True if has jsdoc nodes attached to it. */
/* @internal */
// TODO: GH#19856 Would like to return `node is Node & { jsDoc: JSDoc[] }` but it causes long compile times
Expand Down
6 changes: 3 additions & 3 deletions tests/baselines/reference/callChain.3.types
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ const n4: number | undefined = a?.m?.({x: absorb()}); // likewise
>a?.m : (<T>(obj: { x: T; }) => T) | undefined
>a : { m?<T>(obj: { x: T; }): T; } | undefined
>m : (<T>(obj: { x: T; }) => T) | undefined
>{x: absorb()} : { x: number | undefined; }
>x : number | undefined
>absorb() : number | undefined
>{x: absorb()} : { x: number; }
>x : number
>absorb() : number
>absorb : <T>() => T

// Also a test showing `!` vs `?` for good measure
Expand Down
Loading