Skip to content

Commit 7ec7d6d

Browse files
authored
Fix string literal completions when a partially-typed string fixes inference to a type parameter (#48410)
* Add failing test * Fix string literal completions when a partially-typed string fixes inference to a type parameter
1 parent bf7bfa1 commit 7ec7d6d

File tree

5 files changed

+94
-49
lines changed

5 files changed

+94
-49
lines changed

src/compiler/checker.ts

+52-38
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,16 @@ namespace ts {
176176
}
177177

178178
const enum CheckMode {
179-
Normal = 0, // Normal type checking
180-
Contextual = 1 << 0, // Explicitly assigned contextual type, therefore not cacheable
181-
Inferential = 1 << 1, // Inferential typing
182-
SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions
183-
SkipGenericFunctions = 1 << 3, // Skip single signature generic functions
184-
IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help
185-
RestBindingElement = 1 << 5, // Checking a type that is going to be used to determine the type of a rest binding element
186-
// e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`,
187-
// we need to preserve generic types instead of substituting them for constraints
179+
Normal = 0, // Normal type checking
180+
Contextual = 1 << 0, // Explicitly assigned contextual type, therefore not cacheable
181+
Inferential = 1 << 1, // Inferential typing
182+
SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions
183+
SkipGenericFunctions = 1 << 3, // Skip single signature generic functions
184+
IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help
185+
IsForStringLiteralArgumentCompletions = 1 << 5, // Do not infer from the argument currently being typed
186+
RestBindingElement = 1 << 6, // Checking a type that is going to be used to determine the type of a rest binding element
187+
// e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`,
188+
// we need to preserve generic types instead of substituting them for constraints
188189
}
189190

190191
const enum SignatureCheckMode {
@@ -540,26 +541,10 @@ namespace ts {
540541
if (!node) {
541542
return undefined;
542543
}
543-
const containingCall = findAncestor(node, isCallLikeExpression);
544-
const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature;
545-
if (contextFlags! & ContextFlags.Completions && containingCall) {
546-
let toMarkSkip = node as Node;
547-
do {
548-
getNodeLinks(toMarkSkip).skipDirectInference = true;
549-
toMarkSkip = toMarkSkip.parent;
550-
} while (toMarkSkip && toMarkSkip !== containingCall);
551-
getNodeLinks(containingCall).resolvedSignature = undefined;
552-
}
553-
const result = getContextualType(node, contextFlags);
554-
if (contextFlags! & ContextFlags.Completions && containingCall) {
555-
let toMarkSkip = node as Node;
556-
do {
557-
getNodeLinks(toMarkSkip).skipDirectInference = undefined;
558-
toMarkSkip = toMarkSkip.parent;
559-
} while (toMarkSkip && toMarkSkip !== containingCall);
560-
getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature;
544+
if (contextFlags! & ContextFlags.Completions) {
545+
return runWithInferenceBlockedFromSourceNode(node, () => getContextualType(node, contextFlags));
561546
}
562-
return result;
547+
return getContextualType(node, contextFlags);
563548
},
564549
getContextualTypeForObjectLiteralElement: nodeIn => {
565550
const node = getParseTreeNode(nodeIn, isObjectLiteralElementLike);
@@ -578,6 +563,8 @@ namespace ts {
578563
getFullyQualifiedName,
579564
getResolvedSignature: (node, candidatesOutArray, argumentCount) =>
580565
getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal),
566+
getResolvedSignatureForStringLiteralCompletions: (call, editingArgument, candidatesOutArray) =>
567+
getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, CheckMode.IsForStringLiteralArgumentCompletions, editingArgument),
581568
getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) =>
582569
getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp),
583570
getExpandedParameters,
@@ -747,10 +734,36 @@ namespace ts {
747734
getMemberOverrideModifierStatus,
748735
};
749736

750-
function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined {
737+
function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T {
738+
const containingCall = findAncestor(node, isCallLikeExpression);
739+
const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature;
740+
if (containingCall) {
741+
let toMarkSkip = node!;
742+
do {
743+
getNodeLinks(toMarkSkip).skipDirectInference = true;
744+
toMarkSkip = toMarkSkip.parent;
745+
} while (toMarkSkip && toMarkSkip !== containingCall);
746+
getNodeLinks(containingCall).resolvedSignature = undefined;
747+
}
748+
const result = fn();
749+
if (containingCall) {
750+
let toMarkSkip = node!;
751+
do {
752+
getNodeLinks(toMarkSkip).skipDirectInference = undefined;
753+
toMarkSkip = toMarkSkip.parent;
754+
} while (toMarkSkip && toMarkSkip !== containingCall);
755+
getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature;
756+
}
757+
return result;
758+
}
759+
760+
function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode, editingArgument?: Node): Signature | undefined {
751761
const node = getParseTreeNode(nodeIn, isCallLikeExpression);
752762
apparentArgumentCount = argumentCount;
753-
const res = node ? getResolvedSignature(node, candidatesOutArray, checkMode) : undefined;
763+
const res =
764+
!node ? undefined :
765+
editingArgument ? runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignature(node, candidatesOutArray, checkMode)) :
766+
getResolvedSignature(node, candidatesOutArray, checkMode);
754767
apparentArgumentCount = undefined;
755768
return res;
756769
}
@@ -22664,7 +22677,7 @@ namespace ts {
2266422677
const properties = getPropertiesOfObjectType(target);
2266522678
for (const targetProp of properties) {
2266622679
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
22667-
if (sourceProp) {
22680+
if (sourceProp && !some(sourceProp.declarations, hasSkipDirectInferenceFlag)) {
2266822681
inferFromTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp));
2266922682
}
2267022683
}
@@ -29746,7 +29759,7 @@ namespace ts {
2974629759

2974729760
for (let i = 0; i < argCount; i++) {
2974829761
const arg = args[i];
29749-
if (arg.kind !== SyntaxKind.OmittedExpression) {
29762+
if (arg.kind !== SyntaxKind.OmittedExpression && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions && hasSkipDirectInferenceFlag(arg))) {
2975029763
const paramType = getTypeAtPosition(signature, i);
2975129764
const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode);
2975229765
inferTypes(context.inferences, argType, paramType);
@@ -30486,7 +30499,7 @@ namespace ts {
3048630499
}
3048730500
}
3048830501

30489-
return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray);
30502+
return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray, checkMode);
3049030503

3049130504
function addImplementationSuccessElaboration(failed: Signature, diagnostic: Diagnostic) {
3049230505
const oldCandidatesForArgumentError = candidatesForArgumentError;
@@ -30600,14 +30613,15 @@ namespace ts {
3060030613
candidates: Signature[],
3060130614
args: readonly Expression[],
3060230615
hasCandidatesOutArray: boolean,
30616+
checkMode: CheckMode,
3060330617
): Signature {
3060430618
Debug.assert(candidates.length > 0); // Else should not have called this.
3060530619
checkNodeDeferred(node);
3060630620
// Normally we will combine overloads. Skip this if they have type parameters since that's hard to combine.
3060730621
// Don't do this if there is a `candidatesOutArray`,
3060830622
// because then we want the chosen best candidate to be one of the overloads, not a combination.
3060930623
return hasCandidatesOutArray || candidates.length === 1 || candidates.some(c => !!c.typeParameters)
30610-
? pickLongestCandidateSignature(node, candidates, args)
30624+
? pickLongestCandidateSignature(node, candidates, args, checkMode)
3061130625
: createUnionOfSignaturesForOverloadFailure(candidates);
3061230626
}
3061330627

@@ -30661,7 +30675,7 @@ namespace ts {
3066130675
return createSymbolWithType(first(sources), type);
3066230676
}
3066330677

30664-
function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: readonly Expression[]): Signature {
30678+
function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: readonly Expression[], checkMode: CheckMode): Signature {
3066530679
// Pick the longest signature. This way we can get a contextual type for cases like:
3066630680
// declare function f(a: { xa: number; xb: number; }, b: number);
3066730681
// f({ |
@@ -30678,7 +30692,7 @@ namespace ts {
3067830692
const typeArgumentNodes: readonly TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined;
3067930693
const instantiated = typeArgumentNodes
3068030694
? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, isInJSFile(node)))
30681-
: inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args);
30695+
: inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args, checkMode);
3068230696
candidates[bestIndex] = instantiated;
3068330697
return instantiated;
3068430698
}
@@ -30694,9 +30708,9 @@ namespace ts {
3069430708
return typeArguments;
3069530709
}
3069630710

30697-
function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: Signature, args: readonly Expression[]): Signature {
30711+
function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: Signature, args: readonly Expression[], checkMode: CheckMode): Signature {
3069830712
const inferenceContext = createInferenceContext(typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None);
30699-
const typeArgumentTypes = inferTypeArguments(node, candidate, args, CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext);
30713+
const typeArgumentTypes = inferTypeArguments(node, candidate, args, checkMode | CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext);
3070030714
return createSignatureInstantiation(candidate, typeArgumentTypes);
3070130715
}
3070230716

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4284,6 +4284,7 @@ namespace ts {
42844284
*/
42854285
getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
42864286
/* @internal */ getResolvedSignatureForSignatureHelp(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
4287+
/* @internal */ getResolvedSignatureForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node, candidatesOutArray: Signature[]): Signature | undefined;
42874288
/* @internal */ getExpandedParameters(sig: Signature): readonly (readonly Symbol[])[];
42884289
/* @internal */ hasEffectiveRestParameter(sig: Signature): boolean;
42894290
/* @internal */ containsArgumentsReference(declaration: SignatureDeclaration): boolean;

src/harness/fourslashImpl.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ namespace FourSlash {
168168
// The position of the end of the current selection, or -1 if nothing is selected
169169
public selectionEnd = -1;
170170

171-
public lastKnownMarker = "";
171+
public lastKnownMarker: string | undefined;
172172

173173
// The file that's currently 'opened'
174174
public activeFile!: FourSlashFile;
@@ -400,7 +400,7 @@ namespace FourSlash {
400400
continue;
401401
}
402402
const memo = Utils.memoize(
403-
(_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string, ...args: any[]) => (ls[key] as Function)(...args),
403+
(_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string | undefined, ...args: any[]) => (ls[key] as Function)(...args),
404404
(...args) => args.map(a => a && typeof a === "object" ? JSON.stringify(a) : a).join("|,|")
405405
);
406406
proxy[key] = (...args: any[]) => memo(
@@ -540,8 +540,8 @@ namespace FourSlash {
540540
}
541541

542542
private messageAtLastKnownMarker(message: string) {
543-
const locationDescription = this.lastKnownMarker ? this.lastKnownMarker : this.getLineColStringAtPosition(this.currentCaretPosition);
544-
return `At ${locationDescription}: ${message}`;
543+
const locationDescription = this.lastKnownMarker !== undefined ? this.lastKnownMarker : this.getLineColStringAtPosition(this.currentCaretPosition);
544+
return `At marker '${locationDescription}': ${message}`;
545545
}
546546

547547
private assertionMessageAtLastKnownMarker(msg: string) {
@@ -864,7 +864,7 @@ namespace FourSlash {
864864
else {
865865
for (const marker of toArray(options.marker)) {
866866
this.goToMarker(marker);
867-
this.verifyCompletionsWorker(options);
867+
this.verifyCompletionsWorker({ ...options, marker });
868868
}
869869
}
870870
}

src/services/stringCompletions.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,13 @@ namespace ts.Completions.StringCompletions {
210210

211211
case SyntaxKind.CallExpression:
212212
case SyntaxKind.NewExpression:
213+
case SyntaxKind.JsxAttribute:
213214
if (!isRequireCallArgument(node) && !isImportCall(parent)) {
214-
const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(node, position, sourceFile);
215+
const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile);
215216
// Get string literal completions from specialized signatures of the target
216217
// i.e. declare function f(a: 'A');
217218
// f("/*completion position*/")
218-
return argumentInfo ? getStringLiteralCompletionsFromSignature(argumentInfo, typeChecker) : fromContextualType();
219+
return argumentInfo ? getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) : fromContextualType();
219220
}
220221
// falls through (is `require("")` or `require(""` or `import("")`)
221222

@@ -257,15 +258,21 @@ namespace ts.Completions.StringCompletions {
257258
type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined);
258259
}
259260

260-
function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes {
261+
function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes {
261262
let isNewIdentifier = false;
262-
263263
const uniques = new Map<string, true>();
264264
const candidates: Signature[] = [];
265-
checker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount);
265+
const editingArgument = isJsxOpeningLikeElement(call) ? Debug.checkDefined(findAncestor(arg.parent, isJsxAttribute)) : arg;
266+
checker.getResolvedSignatureForStringLiteralCompletions(call, editingArgument, candidates);
266267
const types = flatMap(candidates, candidate => {
267268
if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return;
268-
const type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex);
269+
let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex);
270+
if (isJsxOpeningLikeElement(call)) {
271+
const propType = checker.getTypeOfPropertyOfType(type, (editingArgument as JsxAttribute).name.text);
272+
if (propType) {
273+
type = propType;
274+
}
275+
}
269276
isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String);
270277
return getStringLiteralTypes(type, uniques);
271278
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @allowJs: true
4+
5+
// @Filename: /a.tsx
6+
//// interface Events {
7+
//// "": any;
8+
//// drag: any;
9+
//// dragenter: any;
10+
//// }
11+
//// declare function addListener<K extends keyof Events>(type: K, listener: (ev: Events[K]) => any): void;
12+
////
13+
//// declare function ListenerComponent<K extends keyof Events>(props: { type: K, onWhatever: (ev: Events[K]) => void }): JSX.Element;
14+
////
15+
//// addListener("/*ts*/");
16+
//// (<ListenerComponent type="/*tsx*/" />);
17+
18+
// @Filename: /b.js
19+
//// addListener("/*js*/");
20+
21+
verify.completions({ marker: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] });
22+
edit.insert("drag");
23+
verify.completions({ marker: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] });

0 commit comments

Comments
 (0)