Skip to content

Commit d7b4cee

Browse files
rbucktonAndarist
andauthored
Include source node inferences in string literal completions (#54121)
Co-authored-by: Mateusz Burzyński <[email protected]>
1 parent 55d8bed commit d7b4cee

6 files changed

+91
-35
lines changed

src/compiler/checker.ts

+47-27
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ import {
469469
isCallChain,
470470
isCallExpression,
471471
isCallLikeExpression,
472+
isCallLikeOrFunctionLikeExpression,
472473
isCallOrNewExpression,
473474
isCallSignatureDeclaration,
474475
isCatchClause,
@@ -1653,12 +1654,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
16531654
getTypeOfPropertyOfContextualType,
16541655
getFullyQualifiedName,
16551656
getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal),
1656-
getResolvedSignatureForStringLiteralCompletions: (call, editingArgument, candidatesOutArray, checkMode = CheckMode.IsForStringLiteralArgumentCompletions) => {
1657-
if (checkMode & CheckMode.IsForStringLiteralArgumentCompletions) {
1658-
return runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, checkMode & ~CheckMode.IsForStringLiteralArgumentCompletions));
1659-
}
1660-
return runWithoutResolvedSignatureCaching(editingArgument, () => getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, checkMode & ~CheckMode.IsForStringLiteralArgumentCompletions));
1661-
},
1657+
getCandidateSignaturesForStringLiteralCompletions,
16621658
getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => runWithoutResolvedSignatureCaching(node, () => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp)),
16631659
getExpandedParameters,
16641660
hasEffectiveRestParameter,
@@ -1838,32 +1834,55 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
18381834
typeHasCallOrConstructSignatures,
18391835
};
18401836

1837+
function getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node) {
1838+
const candidatesSet = new Set<Signature>();
1839+
const candidates: Signature[] = [];
1840+
1841+
// first, get candidates when inference is blocked from the source node.
1842+
runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignatureWorker(call, candidates, /*argumentCount*/ undefined, CheckMode.IsForStringLiteralArgumentCompletions));
1843+
for (const candidate of candidates) {
1844+
candidatesSet.add(candidate);
1845+
}
1846+
1847+
// reset candidates for second pass
1848+
candidates.length = 0;
1849+
1850+
// next, get candidates where the source node is considered for inference.
1851+
runWithoutResolvedSignatureCaching(editingArgument, () => getResolvedSignatureWorker(call, candidates, /*argumentCount*/ undefined, CheckMode.Normal));
1852+
for (const candidate of candidates) {
1853+
candidatesSet.add(candidate);
1854+
}
1855+
1856+
return arrayFrom(candidatesSet);
1857+
}
1858+
18411859
function runWithoutResolvedSignatureCaching<T>(node: Node | undefined, fn: () => T): T {
1842-
const cachedResolvedSignatures = [];
1843-
const cachedTypes = [];
1844-
while (node) {
1845-
if (isCallLikeExpression(node) || isFunctionLike(node)) {
1860+
node = findAncestor(node, isCallLikeOrFunctionLikeExpression);
1861+
if (node) {
1862+
const cachedResolvedSignatures = [];
1863+
const cachedTypes = [];
1864+
while (node) {
18461865
const nodeLinks = getNodeLinks(node);
1847-
const resolvedSignature = nodeLinks.resolvedSignature;
1848-
cachedResolvedSignatures.push([nodeLinks, resolvedSignature] as const);
1866+
cachedResolvedSignatures.push([nodeLinks, nodeLinks.resolvedSignature] as const);
18491867
nodeLinks.resolvedSignature = undefined;
1868+
if (isFunctionLike(node)) {
1869+
const symbolLinks = getSymbolLinks(getSymbolOfDeclaration(node));
1870+
const type = symbolLinks.type;
1871+
cachedTypes.push([symbolLinks, type] as const);
1872+
symbolLinks.type = undefined;
1873+
}
1874+
node = findAncestor(node.parent, isCallLikeOrFunctionLikeExpression);
18501875
}
1851-
if (isFunctionLike(node)) {
1852-
const symbolLinks = getSymbolLinks(getSymbolOfDeclaration(node));
1853-
const type = symbolLinks.type;
1854-
cachedTypes.push([symbolLinks, type] as const);
1855-
symbolLinks.type = undefined;
1876+
const result = fn();
1877+
for (const [nodeLinks, resolvedSignature] of cachedResolvedSignatures) {
1878+
nodeLinks.resolvedSignature = resolvedSignature;
18561879
}
1857-
node = node.parent;
1858-
}
1859-
const result = fn();
1860-
for (const [nodeLinks, resolvedSignature] of cachedResolvedSignatures) {
1861-
nodeLinks.resolvedSignature = resolvedSignature;
1862-
}
1863-
for (const [symbolLinks, type] of cachedTypes) {
1864-
symbolLinks.type = type;
1880+
for (const [symbolLinks, type] of cachedTypes) {
1881+
symbolLinks.type = type;
1882+
}
1883+
return result;
18651884
}
1866-
return result;
1885+
return fn();
18671886
}
18681887

18691888
function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T {
@@ -33207,7 +33226,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3320733226

3320833227
for (let i = 0; i < argCount; i++) {
3320933228
const arg = args[i];
33210-
if (arg.kind !== SyntaxKind.OmittedExpression) {
33229+
if (arg.kind !== SyntaxKind.OmittedExpression && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions && hasSkipDirectInferenceFlag(arg))) {
3321133230
const paramType = getTypeAtPosition(signature, i);
3321233231
if (couldContainTypeVariables(paramType)) {
3321333232
const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode);
@@ -33849,6 +33868,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3384933868
// decorators are applied to a declaration by the emitter, and not to an expression.
3385033869
const isSingleNonGenericCandidate = candidates.length === 1 && !candidates[0].typeParameters;
3385133870
let argCheckMode = !isDecorator && !isSingleNonGenericCandidate && some(args, isContextSensitive) ? CheckMode.SkipContextSensitive : CheckMode.Normal;
33871+
argCheckMode |= checkMode & CheckMode.IsForStringLiteralArgumentCompletions;
3385233872

3385333873
// The following variables are captured and modified by calls to chooseOverload.
3385433874
// If overload resolution or type argument inference fails, we want to report the

src/compiler/types.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
BaseNodeFactory,
3-
CheckMode,
43
CreateSourceFileOptions,
54
EmitHelperFactory,
65
GetCanonicalFileName,
@@ -5010,7 +5009,7 @@ export interface TypeChecker {
50105009
*/
50115010
getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
50125011
/** @internal */ getResolvedSignatureForSignatureHelp(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
5013-
/** @internal */ getResolvedSignatureForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node, candidatesOutArray: Signature[], checkMode?: CheckMode): Signature | undefined;
5012+
/** @internal */ getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node): Signature[];
50145013
/** @internal */ getExpandedParameters(sig: Signature): readonly (readonly Symbol[])[];
50155014
/** @internal */ hasEffectiveRestParameter(sig: Signature): boolean;
50165015
/** @internal */ containsArgumentsReference(declaration: SignatureDeclaration): boolean;

src/compiler/utilitiesPublic.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1920,6 +1920,11 @@ export function isPropertyAccessOrQualifiedName(node: Node): node is PropertyAcc
19201920
|| kind === SyntaxKind.QualifiedName;
19211921
}
19221922

1923+
/** @internal */
1924+
export function isCallLikeOrFunctionLikeExpression(node: Node): node is CallLikeExpression | SignatureDeclaration {
1925+
return isCallLikeExpression(node) || isFunctionLike(node);
1926+
}
1927+
19231928
export function isCallLikeExpression(node: Node): node is CallLikeExpression {
19241929
switch (node.kind) {
19251930
case SyntaxKind.JsxOpeningElement:

src/services/stringCompletions.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
CaseClause,
88
changeExtension,
99
CharacterCodes,
10-
CheckMode,
1110
combinePaths,
1211
comparePaths,
1312
comparePatternKeys,
@@ -119,7 +118,6 @@ import {
119118
ScriptElementKind,
120119
ScriptElementKindModifier,
121120
ScriptTarget,
122-
Signature,
123121
signatureHasRestParameter,
124122
SignatureHelp,
125123
singleElementArray,
@@ -414,7 +412,7 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
414412
// Get string literal completions from specialized signatures of the target
415413
// i.e. declare function f(a: 'A');
416414
// f("/*completion position*/")
417-
return argumentInfo && (getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) || getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker, CheckMode.Normal)) || fromContextualType(ContextFlags.None);
415+
return argumentInfo && getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) || fromContextualType(ContextFlags.None);
418416
}
419417
// falls through (is `require("")` or `require(""` or `import("")`)
420418

@@ -504,12 +502,11 @@ function getAlreadyUsedTypesInStringLiteralUnion(union: UnionTypeNode, current:
504502
return mapDefined(union.types, type => type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined);
505503
}
506504

507-
function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker, checkMode = CheckMode.IsForStringLiteralArgumentCompletions): StringLiteralCompletionsFromTypes | undefined {
505+
function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes | undefined {
508506
let isNewIdentifier = false;
509507
const uniques = new Map<string, true>();
510-
const candidates: Signature[] = [];
511508
const editingArgument = isJsxOpeningLikeElement(call) ? Debug.checkDefined(findAncestor(arg.parent, isJsxAttribute)) : arg;
512-
checker.getResolvedSignatureForStringLiteralCompletions(call, editingArgument, candidates, checkMode);
509+
const candidates = checker.getCandidateSignaturesForStringLiteralCompletions(call, editingArgument);
513510
const types = flatMap(candidates, candidate => {
514511
if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return;
515512
let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// repro from https://github.com/microsoft/TypeScript/issues/49680
4+
5+
//// type PathOf<T, K extends string, P extends string = ""> =
6+
//// K extends `${infer U}.${infer V}`
7+
//// ? U extends keyof T ? PathOf<T[U], V, `${P}${U}.`> : `${P}${keyof T & (string | number)}`
8+
//// : K extends keyof T ? `${P}${K}` : `${P}${keyof T & (string | number)}`;
9+
////
10+
//// declare function consumer<K extends string>(path: PathOf<{a: string, b: {c: string}}, K>) : number;
11+
////
12+
//// consumer('b./*ts*/')
13+
14+
verify.completions({ marker: ["ts"], exact: ["a", "b", "b.c"] });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// repro from https://github.com/microsoft/TypeScript/issues/49680#issuecomment-1249191842
4+
5+
//// declare function pick<T extends object, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;
6+
//// declare function pick2<T extends object, K extends (keyof T)[]>(obj: T, ...keys: K): Pick<T, K[number]>;
7+
////
8+
//// const obj = { aaa: 1, bbb: '2', ccc: true };
9+
////
10+
//// pick(obj, 'aaa', '/*ts1*/');
11+
//// pick2(obj, 'aaa', '/*ts2*/');
12+
13+
// repro from https://github.com/microsoft/TypeScript/issues/49680#issuecomment-1273677941
14+
15+
//// class Q<T> {
16+
//// public select<Keys extends keyof T>(...args: Keys[]) {}
17+
//// }
18+
//// new Q<{ id: string; name: string }>().select("name", "/*ts3*/");
19+
20+
verify.completions({ marker: ["ts1", "ts2"], exact: ["aaa", "bbb", "ccc"] });
21+
verify.completions({ marker: ["ts3"], exact: ["name", "id"] });

0 commit comments

Comments
 (0)