Skip to content

Commit

Permalink
Propagate 'undefined' instead of the optional type marker at an optio…
Browse files Browse the repository at this point in the history
…nal chain boundary (microsoft#34588)

* Propagate 'undefined' instead of the optional type marker at an optional chain boundary

* Update src/compiler/types.ts

Co-Authored-By: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
  • Loading branch information
rbuckton and sandersn committed Nov 1, 2019
1 parent ec367fe commit ba5e86f
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
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
Expand Up @@ -8691,14 +8691,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 @@ -10313,9 +10322,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 @@ -16767,8 +16779,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 @@ -22835,7 +22847,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 @@ -23267,7 +23279,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 @@ -23372,7 +23384,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 @@ -23414,7 +23426,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 @@ -24080,7 +24092,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 @@ -24099,7 +24111,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 @@ -24486,22 +24498,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 @@ -24577,7 +24592,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 @@ -24648,7 +24663,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 @@ -24657,7 +24672,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 @@ -24872,7 +24887,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 @@ -24935,7 +24950,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 @@ -24987,7 +25002,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 @@ -27460,6 +27475,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 @@ -27471,21 +27500,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 @@ -36198,7 +36216,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
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
Expand Up @@ -5947,6 +5947,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 @@ -5955,6 +5960,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 @@ -7276,11 +7298,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
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

0 comments on commit ba5e86f

Please sign in to comment.