Skip to content

In services, when overload resolution fails, create a union signature #19620

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

Closed
wants to merge 8 commits into from
Closed
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
149 changes: 113 additions & 36 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3200,7 +3200,7 @@ namespace ts {
if ((symbol as TransientSymbol).syntheticLiteralTypeOrigin) {
const stringValue = (symbol as TransientSymbol).syntheticLiteralTypeOrigin.value;
if (!isIdentifierText(stringValue, compilerOptions.target)) {
return `"${escapeString(stringValue, CharacterCodes.doubleQuote)}"`;
return '"' + escapeString(stringValue, CharacterCodes.doubleQuote) + '"';
}
}
return symbolName(symbol);
Expand Down Expand Up @@ -5568,9 +5568,9 @@ namespace ts {
/**
* Gets the symbolic name for a late-bound member from its type.
*/
function getLateBoundNameFromType(type: LiteralType | UniqueESSymbolType) {
function getLateBoundNameFromType(type: LiteralType | UniqueESSymbolType): __String | undefined {
if (type.flags & TypeFlags.UniqueESSymbol) {
return `__@${type.symbol.escapedName}@${getSymbolId(type.symbol)}` as __String;
return "__@" + type.symbol.escapedName + "@" + getSymbolId(type.symbol) as __String;
}
if (type.flags & TypeFlags.StringOrNumberLiteral) {
return escapeLeadingUnderscores("" + (<LiteralType>type).value);
Expand Down Expand Up @@ -7037,13 +7037,17 @@ namespace ts {
}

function getRestTypeOfSignature(signature: Signature): Type {
return tryGetRestTypeOfSignature(signature) || anyType;
}

function tryGetRestTypeOfSignature(signature: Signature): Type | undefined {
if (signature.hasRestParameter) {
const type = getTypeOfSymbol(lastOrUndefined(signature.parameters));
if (getObjectFlags(type) & ObjectFlags.Reference && (<TypeReference>type).target === globalArrayType) {
return (<TypeReference>type).typeArguments[0];
}
}
return anyType;
return undefined;
}

function getSignatureInstantiation(signature: Signature, typeArguments: Type[], isJavascript: boolean): Signature {
Expand Down Expand Up @@ -7772,7 +7776,7 @@ namespace ts {

// Add the given types to the given type set. Order is preserved, duplicates are removed,
// and nested types of the given kind are flattened into the set.
function addTypesToUnion(typeSet: TypeSet, types: Type[]) {
function addTypesToUnion(typeSet: TypeSet, types: ReadonlyArray<Type>) {
for (const type of types) {
addTypeToUnion(typeSet, type);
}
Expand Down Expand Up @@ -7851,7 +7855,7 @@ namespace ts {
// expression constructs such as array literals and the || and ?: operators). Named types can
// circularly reference themselves and therefore cannot be subtype reduced during their declaration.
// For example, "type Item = string | (() => Item" is a named type that circularly references itself.
function getUnionType(types: Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type {
function getUnionType(types: ReadonlyArray<Type>, unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type {
if (types.length === 0) {
return neverType;
}
Expand Down Expand Up @@ -17060,35 +17064,8 @@ namespace ts {
diagnostics.add(createDiagnosticForNode(node, fallbackError));
}

// No signature was applicable. We have already reported the errors for the invalid signature.
// If this is a type resolution session, e.g. Language Service, try to get better information than anySignature.
// Pick the longest signature. This way we can get a contextual type for cases like:
// declare function f(a: { xa: number; xb: number; }, b: number);
// f({ |
// Also, use explicitly-supplied type arguments if they are provided, so we can get a contextual signature in cases like:
// declare function f<T>(k: keyof T);
// f<Foo>("
if (!produceDiagnostics) {
Debug.assert(candidates.length > 0); // Else would have exited above.
const bestIndex = getLongestCandidateIndex(candidates, apparentArgumentCount === undefined ? args.length : apparentArgumentCount);
const candidate = candidates[bestIndex];

const { typeParameters } = candidate;
if (typeParameters && callLikeExpressionMayHaveTypeArguments(node) && node.typeArguments) {
const typeArguments = node.typeArguments.map(getTypeOfNode);
while (typeArguments.length > typeParameters.length) {
typeArguments.pop();
}
while (typeArguments.length < typeParameters.length) {
typeArguments.push(getDefaultTypeArgumentType(isInJavaScriptFile(node)));
}

const instantiated = createSignatureInstantiation(candidate, typeArguments);
candidates[bestIndex] = instantiated;
return instantiated;
}

return candidate;
return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray);
}

return resolveErrorCall(node);
Expand Down Expand Up @@ -17162,7 +17139,103 @@ namespace ts {
}
}

function getLongestCandidateIndex(candidates: Signature[], argsCount: number): number {
// No signature was applicable. We have already reported the errors for the invalid signature.
// If this is a type resolution session, e.g. Language Service, try to get better information than anySignature.
function getCandidateForOverloadFailure(
node: CallLikeExpression,
candidates: Signature[],
args: ReadonlyArray<Expression>,
hasCandidatesOutArray: boolean,
): Signature {
Debug.assert(candidates.length > 0); // Else should not have called this.
// Normally we will combine overloads. Skip this if they have type parameters since that's hard to combine.
// Don't do this if there is a `candidatesOutArray`,
// because then we want the chosen best candidate to be one of the overloads, not a combination.
return hasCandidatesOutArray || candidates.length === 1 || candidates.some(c => !!c.typeParameters)
? pickLongestCandidateSignature(node, candidates, args)
: createUnionOfSignaturesForOverloadFailure(candidates);
}

function createUnionOfSignaturesForOverloadFailure(candidates: ReadonlyArray<Signature>): Signature {
const thisParameters = mapDefined(candidates, c => c.thisParameter);
let thisParameter: Symbol | undefined;
if (thisParameters.length) {
thisParameter = createCombinedSymbolFromTypes(thisParameters, thisParameters.map(getTypeOfParameter));
}

const { min: minArgumentCount, max: maxNonRestParam } = minAndMax(candidates, getNumNonRestParameters);
const parameters: ts.Symbol[] = [];
for (let i = 0; i < maxNonRestParam; i++) {
const symbols = mapDefined(candidates, ({ parameters, hasRestParameter }) => hasRestParameter ?
i < parameters.length - 1 ? parameters[i] : last(parameters) :
i < parameters.length ? parameters[i] : undefined);
Debug.assert(symbols.length !== 0);
parameters.push(createCombinedSymbolFromTypes(symbols, mapDefined(candidates, candidate => tryGetTypeAtPosition(candidate, i))));
}

const restParameterSymbols = mapDefined(candidates, c => c.hasRestParameter ? last(c.parameters) : undefined);
const hasRestParameter = restParameterSymbols.length !== 0;
if (hasRestParameter) {
const type = createArrayType(getUnionType(mapDefined(candidates, tryGetRestTypeOfSignature), UnionReduction.Subtype));
parameters.push(createCombinedSymbolForOverloadFailure(restParameterSymbols, type));
}

return createSignature(
candidates[0].declaration,
/*typeParameters*/ undefined, // Before calling this we tested for `!candidates.some(c => !!c.typeParameters)`.
thisParameter,
parameters,
/*resolvedReturnType*/ getIntersectionType(candidates.map(getReturnTypeOfSignature)),
/*typePredicate*/ undefined,
minArgumentCount,
hasRestParameter,
/*hasLiteralTypes*/ candidates.some(c => c.hasLiteralTypes));
}

function createCombinedSymbolFromTypes(sources: ReadonlyArray<Symbol>, types: ReadonlyArray<Type>): Symbol {
return createCombinedSymbolForOverloadFailure(sources, getUnionType(types, UnionReduction.Subtype));
}

function createCombinedSymbolForOverloadFailure(sources: ReadonlyArray<Symbol>, type: Type): Symbol {
// This function is currently only used for erroneous overloads, so it's good enough to just use the first source.
return createSymbolWithType(first(sources), type);
}

function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: ReadonlyArray<Expression>): Signature {
// Pick the longest signature. This way we can get a contextual type for cases like:
// declare function f(a: { xa: number; xb: number; }, b: number);
// f({ |
// Also, use explicitly-supplied type arguments if they are provided, so we can get a contextual signature in cases like:
// declare function f<T>(k: keyof T);
// f<Foo>("
const bestIndex = getLongestCandidateIndex(candidates, apparentArgumentCount === undefined ? args.length : apparentArgumentCount);
const candidate = candidates[bestIndex];

const { typeParameters } = candidate;
if (!typeParameters) {
return candidate;
}

if (!callLikeExpressionMayHaveTypeArguments(node) || !node.typeArguments) {
// TODO: This leaks a type parameter! See GH#19854
// Could use `callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments || emptyArray : emptyArray;` instead of `node.typeArguments`.
return candidate;
}

const typeArguments = node.typeArguments.map(getTypeOfNode);
while (typeArguments.length > typeParameters.length) {
typeArguments.pop();
}
while (typeArguments.length < typeParameters.length) {
typeArguments.push(getDefaultTypeArgumentType(isInJavaScriptFile(node)));
}

const instantiated = createSignatureInstantiation(candidate, typeArguments);
candidates[bestIndex] = instantiated;
return instantiated;
}

function getLongestCandidateIndex(candidates: ReadonlyArray<Signature>, argsCount: number): number {
let maxParamsIndex = -1;
let maxParams = -1;

Expand Down Expand Up @@ -17815,9 +17888,13 @@ namespace ts {
}

function getTypeAtPosition(signature: Signature, pos: number): Type {
return tryGetTypeAtPosition(signature, pos) || anyType;
}

function tryGetTypeAtPosition(signature: Signature, pos: number): Type | undefined {
return signature.hasRestParameter ?
pos < signature.parameters.length - 1 ? getTypeOfParameter(signature.parameters[pos]) : getRestTypeOfSignature(signature) :
pos < signature.parameters.length ? getTypeOfParameter(signature.parameters[pos]) : anyType;
pos < signature.parameters.length ? getTypeOfParameter(signature.parameters[pos]) : undefined;
}

function getTypeOfFirstParameterOfSignature(signature: Signature) {
Expand Down
16 changes: 16 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,22 @@ namespace ts {
return to;
}

export function minAndMax<T>(arr: ReadonlyArray<T>, getValue: (value: T) => number): { min: number, max: number } {
Debug.assert(arr.length !== 0);
let min = getValue(arr[0]);
let max = min;
for (let i = 1; i < arr.length; i++) {
const b = getValue(arr[i]);
if (b < min) {
min = b;
}
else if (b > max) {
max = b;
}
}
return { min, max };
}

/**
* Gets the actual offset into an array for a relative offset. Negative offsets indicate a
* position offset from the end of the array.
Expand Down
9 changes: 9 additions & 0 deletions tests/cases/fourslash/completionsCombineOverloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// <reference path="fourslash.ts" />

////interface A { a: number }
////interface B { b: number }
////declare function f(a: A): void;
////declare function f(b: B): void;
////f({ /**/ });

verify.completionsAt("", ["a", "b"]);
13 changes: 13 additions & 0 deletions tests/cases/fourslash/completionsCombineOverloads_restParameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path="fourslash.ts" />

////interface A { a: number }
////interface B { b: number }
////interface C { c: number }
////declare function f(a: A): void;
////declare function f(...bs: B[]): void;
////declare function f(...cs: C[]): void;
////f({ /*1*/ });
////f({ a: 1 }, { /*2*/ });

verify.completionsAt("1", ["a", "b", "c"]);
verify.completionsAt("2", ["b", "c"]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// <reference path="fourslash.ts" />

////interface A { a: number }
////interface B { b: number }
////declare function f(n: number): A;
////declare function f(s: string): B;
////f()./**/

verify.completionsAt("", ["a", "b"]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference path='fourslash.ts'/>

////declare function f<T>(x: number): T;
////const x/**/ = f();

verify.quickInfoAt("", "const x: T"); // TODO: GH#19854