Skip to content

Commit

Permalink
Cherry-pick PR microsoft#34588 into release-3.7
Browse files Browse the repository at this point in the history
Component commits:
99328e9 Propagate 'undefined' instead of the optional type marker at an optional chain boundary

7aa6eee Merge branch 'master' into fix34579
# Conflicts:
#	src/compiler/utilities.ts

61e6765 Update src/compiler/types.ts
Co-Authored-By: Nathan Shively-Sanders <[email protected]>
  • Loading branch information
rbuckton authored and typescript-bot committed Nov 8, 2019
1 parent a22ad16 commit e2e9380
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 59 deletions.
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

0 comments on commit e2e9380

Please sign in to comment.