Include source node inferences in string literal completions (#54121)

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
This commit is contained in:
Ron Buckton 2023-09-25 18:29:29 -04:00 committed by GitHub
parent 55d8bed85c
commit d7b4cee7df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 35 deletions

View File

@ -469,6 +469,7 @@ import {
isCallChain,
isCallExpression,
isCallLikeExpression,
isCallLikeOrFunctionLikeExpression,
isCallOrNewExpression,
isCallSignatureDeclaration,
isCatchClause,
@ -1653,12 +1654,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
getTypeOfPropertyOfContextualType,
getFullyQualifiedName,
getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal),
getResolvedSignatureForStringLiteralCompletions: (call, editingArgument, candidatesOutArray, checkMode = CheckMode.IsForStringLiteralArgumentCompletions) => {
if (checkMode & CheckMode.IsForStringLiteralArgumentCompletions) {
return runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, checkMode & ~CheckMode.IsForStringLiteralArgumentCompletions));
}
return runWithoutResolvedSignatureCaching(editingArgument, () => getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, checkMode & ~CheckMode.IsForStringLiteralArgumentCompletions));
},
getCandidateSignaturesForStringLiteralCompletions,
getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => runWithoutResolvedSignatureCaching(node, () => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp)),
getExpandedParameters,
hasEffectiveRestParameter,
@ -1838,32 +1834,55 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
typeHasCallOrConstructSignatures,
};
function getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node) {
const candidatesSet = new Set<Signature>();
const candidates: Signature[] = [];
// first, get candidates when inference is blocked from the source node.
runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignatureWorker(call, candidates, /*argumentCount*/ undefined, CheckMode.IsForStringLiteralArgumentCompletions));
for (const candidate of candidates) {
candidatesSet.add(candidate);
}
// reset candidates for second pass
candidates.length = 0;
// next, get candidates where the source node is considered for inference.
runWithoutResolvedSignatureCaching(editingArgument, () => getResolvedSignatureWorker(call, candidates, /*argumentCount*/ undefined, CheckMode.Normal));
for (const candidate of candidates) {
candidatesSet.add(candidate);
}
return arrayFrom(candidatesSet);
}
function runWithoutResolvedSignatureCaching<T>(node: Node | undefined, fn: () => T): T {
const cachedResolvedSignatures = [];
const cachedTypes = [];
while (node) {
if (isCallLikeExpression(node) || isFunctionLike(node)) {
node = findAncestor(node, isCallLikeOrFunctionLikeExpression);
if (node) {
const cachedResolvedSignatures = [];
const cachedTypes = [];
while (node) {
const nodeLinks = getNodeLinks(node);
const resolvedSignature = nodeLinks.resolvedSignature;
cachedResolvedSignatures.push([nodeLinks, resolvedSignature] as const);
cachedResolvedSignatures.push([nodeLinks, nodeLinks.resolvedSignature] as const);
nodeLinks.resolvedSignature = undefined;
if (isFunctionLike(node)) {
const symbolLinks = getSymbolLinks(getSymbolOfDeclaration(node));
const type = symbolLinks.type;
cachedTypes.push([symbolLinks, type] as const);
symbolLinks.type = undefined;
}
node = findAncestor(node.parent, isCallLikeOrFunctionLikeExpression);
}
if (isFunctionLike(node)) {
const symbolLinks = getSymbolLinks(getSymbolOfDeclaration(node));
const type = symbolLinks.type;
cachedTypes.push([symbolLinks, type] as const);
symbolLinks.type = undefined;
const result = fn();
for (const [nodeLinks, resolvedSignature] of cachedResolvedSignatures) {
nodeLinks.resolvedSignature = resolvedSignature;
}
node = node.parent;
for (const [symbolLinks, type] of cachedTypes) {
symbolLinks.type = type;
}
return result;
}
const result = fn();
for (const [nodeLinks, resolvedSignature] of cachedResolvedSignatures) {
nodeLinks.resolvedSignature = resolvedSignature;
}
for (const [symbolLinks, type] of cachedTypes) {
symbolLinks.type = type;
}
return result;
return fn();
}
function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T {
@ -33207,7 +33226,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
for (let i = 0; i < argCount; i++) {
const arg = args[i];
if (arg.kind !== SyntaxKind.OmittedExpression) {
if (arg.kind !== SyntaxKind.OmittedExpression && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions && hasSkipDirectInferenceFlag(arg))) {
const paramType = getTypeAtPosition(signature, i);
if (couldContainTypeVariables(paramType)) {
const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode);
@ -33849,6 +33868,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// decorators are applied to a declaration by the emitter, and not to an expression.
const isSingleNonGenericCandidate = candidates.length === 1 && !candidates[0].typeParameters;
let argCheckMode = !isDecorator && !isSingleNonGenericCandidate && some(args, isContextSensitive) ? CheckMode.SkipContextSensitive : CheckMode.Normal;
argCheckMode |= checkMode & CheckMode.IsForStringLiteralArgumentCompletions;
// The following variables are captured and modified by calls to chooseOverload.
// If overload resolution or type argument inference fails, we want to report the

View File

@ -1,6 +1,5 @@
import {
BaseNodeFactory,
CheckMode,
CreateSourceFileOptions,
EmitHelperFactory,
GetCanonicalFileName,
@ -5010,7 +5009,7 @@ export interface TypeChecker {
*/
getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
/** @internal */ getResolvedSignatureForSignatureHelp(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
/** @internal */ getResolvedSignatureForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node, candidatesOutArray: Signature[], checkMode?: CheckMode): Signature | undefined;
/** @internal */ getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node): Signature[];
/** @internal */ getExpandedParameters(sig: Signature): readonly (readonly Symbol[])[];
/** @internal */ hasEffectiveRestParameter(sig: Signature): boolean;
/** @internal */ containsArgumentsReference(declaration: SignatureDeclaration): boolean;

View File

@ -1920,6 +1920,11 @@ export function isPropertyAccessOrQualifiedName(node: Node): node is PropertyAcc
|| kind === SyntaxKind.QualifiedName;
}
/** @internal */
export function isCallLikeOrFunctionLikeExpression(node: Node): node is CallLikeExpression | SignatureDeclaration {
return isCallLikeExpression(node) || isFunctionLike(node);
}
export function isCallLikeExpression(node: Node): node is CallLikeExpression {
switch (node.kind) {
case SyntaxKind.JsxOpeningElement:

View File

@ -7,7 +7,6 @@ import {
CaseClause,
changeExtension,
CharacterCodes,
CheckMode,
combinePaths,
comparePaths,
comparePatternKeys,
@ -119,7 +118,6 @@ import {
ScriptElementKind,
ScriptElementKindModifier,
ScriptTarget,
Signature,
signatureHasRestParameter,
SignatureHelp,
singleElementArray,
@ -414,7 +412,7 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
// Get string literal completions from specialized signatures of the target
// i.e. declare function f(a: 'A');
// f("/*completion position*/")
return argumentInfo && (getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) || getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker, CheckMode.Normal)) || fromContextualType(ContextFlags.None);
return argumentInfo && getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) || fromContextualType(ContextFlags.None);
}
// falls through (is `require("")` or `require(""` or `import("")`)
@ -504,12 +502,11 @@ function getAlreadyUsedTypesInStringLiteralUnion(union: UnionTypeNode, current:
return mapDefined(union.types, type => type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined);
}
function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker, checkMode = CheckMode.IsForStringLiteralArgumentCompletions): StringLiteralCompletionsFromTypes | undefined {
function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes | undefined {
let isNewIdentifier = false;
const uniques = new Map<string, true>();
const candidates: Signature[] = [];
const editingArgument = isJsxOpeningLikeElement(call) ? Debug.checkDefined(findAncestor(arg.parent, isJsxAttribute)) : arg;
checker.getResolvedSignatureForStringLiteralCompletions(call, editingArgument, candidates, checkMode);
const candidates = checker.getCandidateSignaturesForStringLiteralCompletions(call, editingArgument);
const types = flatMap(candidates, candidate => {
if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return;
let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex);

View File

@ -0,0 +1,14 @@
/// <reference path="fourslash.ts" />
// repro from https://github.com/microsoft/TypeScript/issues/49680
//// type PathOf<T, K extends string, P extends string = ""> =
//// K extends `${infer U}.${infer V}`
//// ? U extends keyof T ? PathOf<T[U], V, `${P}${U}.`> : `${P}${keyof T & (string | number)}`
//// : K extends keyof T ? `${P}${K}` : `${P}${keyof T & (string | number)}`;
////
//// declare function consumer<K extends string>(path: PathOf<{a: string, b: {c: string}}, K>) : number;
////
//// consumer('b./*ts*/')
verify.completions({ marker: ["ts"], exact: ["a", "b", "b.c"] });

View File

@ -0,0 +1,21 @@
/// <reference path="fourslash.ts" />
// repro from https://github.com/microsoft/TypeScript/issues/49680#issuecomment-1249191842
//// declare function pick<T extends object, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;
//// declare function pick2<T extends object, K extends (keyof T)[]>(obj: T, ...keys: K): Pick<T, K[number]>;
////
//// const obj = { aaa: 1, bbb: '2', ccc: true };
////
//// pick(obj, 'aaa', '/*ts1*/');
//// pick2(obj, 'aaa', '/*ts2*/');
// repro from https://github.com/microsoft/TypeScript/issues/49680#issuecomment-1273677941
//// class Q<T> {
//// public select<Keys extends keyof T>(...args: Keys[]) {}
//// }
//// new Q<{ id: string; name: string }>().select("name", "/*ts3*/");
verify.completions({ marker: ["ts1", "ts2"], exact: ["aaa", "bbb", "ccc"] });
verify.completions({ marker: ["ts3"], exact: ["name", "id"] });