Skip to content

Commit 0d266cd

Browse files
committed
Fix string literal completions when a partially-typed string fixes inference to a type parameter
Ugghhghgh Revert "Ugghhghgh" This reverts commit cad98b9. FSDjkl;afsdjklsfdjksdfkjjk Revert "FSDjkl;afsdjklsfdjksdfkjjk" This reverts commit 0cc19c6. Revert "Revert "Ugghhghgh"" This reverts commit 44434a3. It works
1 parent c13d850 commit 0d266cd

File tree

5 files changed

+84
-53
lines changed

5 files changed

+84
-53
lines changed

src/compiler/checker.ts

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

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

189190
const enum SignatureCheckMode {
@@ -532,26 +533,10 @@ namespace ts {
532533
if (!node) {
533534
return undefined;
534535
}
535-
const containingCall = findAncestor(node, isCallLikeExpression);
536-
const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature;
537-
if (contextFlags! & ContextFlags.Completions && containingCall) {
538-
let toMarkSkip = node as Node;
539-
do {
540-
getNodeLinks(toMarkSkip).skipDirectInference = true;
541-
toMarkSkip = toMarkSkip.parent;
542-
} while (toMarkSkip && toMarkSkip !== containingCall);
543-
getNodeLinks(containingCall).resolvedSignature = undefined;
544-
}
545-
const result = getContextualType(node, contextFlags);
546-
if (contextFlags! & ContextFlags.Completions && containingCall) {
547-
let toMarkSkip = node as Node;
548-
do {
549-
getNodeLinks(toMarkSkip).skipDirectInference = undefined;
550-
toMarkSkip = toMarkSkip.parent;
551-
} while (toMarkSkip && toMarkSkip !== containingCall);
552-
getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature;
536+
if (contextFlags! & ContextFlags.Completions) {
537+
return runWithInferenceBlockedFromSourceNode(node, () => getContextualType(node, contextFlags));
553538
}
554-
return result;
539+
return getContextualType(node, contextFlags);
555540
},
556541
getContextualTypeForObjectLiteralElement: nodeIn => {
557542
const node = getParseTreeNode(nodeIn, isObjectLiteralElementLike);
@@ -570,6 +555,8 @@ namespace ts {
570555
getFullyQualifiedName,
571556
getResolvedSignature: (node, candidatesOutArray, argumentCount) =>
572557
getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal),
558+
getResolvedSignatureForStringLiteralCompletions: (call, editingArgument, candidatesOutArray) =>
559+
getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, CheckMode.IsForStringLiteralArgumentCompletions, editingArgument),
573560
getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) =>
574561
getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp),
575562
getExpandedParameters,
@@ -739,10 +726,36 @@ namespace ts {
739726
getMemberOverrideModifierStatus,
740727
};
741728

742-
function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined {
729+
function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T {
730+
const containingCall = findAncestor(node, isCallLikeExpression);
731+
const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature;
732+
if (containingCall) {
733+
let toMarkSkip = node!;
734+
do {
735+
getNodeLinks(toMarkSkip).skipDirectInference = true;
736+
toMarkSkip = toMarkSkip.parent;
737+
} while (toMarkSkip && toMarkSkip !== containingCall);
738+
getNodeLinks(containingCall).resolvedSignature = undefined;
739+
}
740+
const result = fn();
741+
if (containingCall) {
742+
let toMarkSkip = node!;
743+
do {
744+
getNodeLinks(toMarkSkip).skipDirectInference = undefined;
745+
toMarkSkip = toMarkSkip.parent;
746+
} while (toMarkSkip && toMarkSkip !== containingCall);
747+
getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature;
748+
}
749+
return result;
750+
}
751+
752+
function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode, editingArgument?: Node): Signature | undefined {
743753
const node = getParseTreeNode(nodeIn, isCallLikeExpression);
744754
apparentArgumentCount = argumentCount;
745-
const res = node ? getResolvedSignature(node, candidatesOutArray, checkMode) : undefined;
755+
const res =
756+
!node ? undefined :
757+
editingArgument ? runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignature(node, candidatesOutArray, checkMode)) :
758+
getResolvedSignature(node, candidatesOutArray, checkMode);
746759
apparentArgumentCount = undefined;
747760
return res;
748761
}
@@ -22662,7 +22675,7 @@ namespace ts {
2266222675
const properties = getPropertiesOfObjectType(target);
2266322676
for (const targetProp of properties) {
2266422677
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
22665-
if (sourceProp) {
22678+
if (sourceProp && !some(sourceProp.declarations, hasSkipDirectInferenceFlag)) {
2266622679
inferFromTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp));
2266722680
}
2266822681
}
@@ -29760,7 +29773,7 @@ namespace ts {
2976029773

2976129774
for (let i = 0; i < argCount; i++) {
2976229775
const arg = args[i];
29763-
if (arg.kind !== SyntaxKind.OmittedExpression) {
29776+
if (arg.kind !== SyntaxKind.OmittedExpression && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions && hasSkipDirectInferenceFlag(arg))) {
2976429777
const paramType = getTypeAtPosition(signature, i);
2976529778
const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode);
2976629779
inferTypes(context.inferences, argType, paramType);
@@ -30500,7 +30513,7 @@ namespace ts {
3050030513
}
3050130514
}
3050230515

30503-
return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray);
30516+
return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray, checkMode);
3050430517

3050530518
function addImplementationSuccessElaboration(failed: Signature, diagnostic: Diagnostic) {
3050630519
const oldCandidatesForArgumentError = candidatesForArgumentError;
@@ -30614,14 +30627,15 @@ namespace ts {
3061430627
candidates: Signature[],
3061530628
args: readonly Expression[],
3061630629
hasCandidatesOutArray: boolean,
30630+
checkMode: CheckMode,
3061730631
): Signature {
3061830632
Debug.assert(candidates.length > 0); // Else should not have called this.
3061930633
checkNodeDeferred(node);
3062030634
// Normally we will combine overloads. Skip this if they have type parameters since that's hard to combine.
3062130635
// Don't do this if there is a `candidatesOutArray`,
3062230636
// because then we want the chosen best candidate to be one of the overloads, not a combination.
3062330637
return hasCandidatesOutArray || candidates.length === 1 || candidates.some(c => !!c.typeParameters)
30624-
? pickLongestCandidateSignature(node, candidates, args)
30638+
? pickLongestCandidateSignature(node, candidates, args, checkMode)
3062530639
: createUnionOfSignaturesForOverloadFailure(candidates);
3062630640
}
3062730641

@@ -30675,7 +30689,7 @@ namespace ts {
3067530689
return createSymbolWithType(first(sources), type);
3067630690
}
3067730691

30678-
function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: readonly Expression[]): Signature {
30692+
function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: readonly Expression[], checkMode: CheckMode): Signature {
3067930693
// Pick the longest signature. This way we can get a contextual type for cases like:
3068030694
// declare function f(a: { xa: number; xb: number; }, b: number);
3068130695
// f({ |
@@ -30692,7 +30706,7 @@ namespace ts {
3069230706
const typeArgumentNodes: readonly TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined;
3069330707
const instantiated = typeArgumentNodes
3069430708
? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, isInJSFile(node)))
30695-
: inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args);
30709+
: inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args, checkMode);
3069630710
candidates[bestIndex] = instantiated;
3069730711
return instantiated;
3069830712
}
@@ -30708,9 +30722,9 @@ namespace ts {
3070830722
return typeArguments;
3070930723
}
3071030724

30711-
function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: Signature, args: readonly Expression[]): Signature {
30725+
function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: Signature, args: readonly Expression[], checkMode: CheckMode): Signature {
3071230726
const inferenceContext = createInferenceContext(typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None);
30713-
const typeArgumentTypes = inferTypeArguments(node, candidate, args, CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext);
30727+
const typeArgumentTypes = inferTypeArguments(node, candidate, args, checkMode | CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext);
3071430728
return createSignatureInstantiation(candidate, typeArgumentTypes);
3071530729
}
3071630730

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4279,6 +4279,7 @@ namespace ts {
42794279
*/
42804280
getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
42814281
/* @internal */ getResolvedSignatureForSignatureHelp(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
4282+
/* @internal */ getResolvedSignatureForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node, candidatesOutArray: Signature[]): Signature | undefined;
42824283
/* @internal */ getExpandedParameters(sig: Signature): readonly (readonly Symbol[])[];
42834284
/* @internal */ hasEffectiveRestParameter(sig: Signature): boolean;
42844285
/* @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
@@ -1,14 +1,23 @@
11
/// <reference path="fourslash.ts" />
22

3+
// @allowJs: true
4+
5+
// @Filename: /a.tsx
36
//// interface Events {
7+
//// "": any;
48
//// drag: any;
59
//// dragenter: any;
610
//// }
711
//// declare function addListener<K extends keyof Events>(type: K, listener: (ev: Events[K]) => any): void;
8-
//// declare function addListener(type: string, listener: (ev: any) => any): void;
912
////
10-
//// addListener("/**/");
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*/");
1120

12-
verify.completions({ marker: "", isNewIdentifierLocation: true, exact: ["drag", "dragenter"] });
21+
verify.completions({ marker: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] });
1322
edit.insert("drag");
14-
verify.completions({ isNewIdentifierLocation: true, exact: ["drag", "dragenter"] });
23+
verify.completions({ marker: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] });

0 commit comments

Comments
 (0)