From c13d850e7e2e56a84f7183517cd0b79679143bb2 Mon Sep 17 00:00:00 2001 From: Andrew Branch <andrew@wheream.io> Date: Fri, 11 Feb 2022 10:11:08 -0800 Subject: [PATCH 1/2] Add failing test --- .../cases/fourslash/completionsLiteralOverload.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/cases/fourslash/completionsLiteralOverload.ts diff --git a/tests/cases/fourslash/completionsLiteralOverload.ts b/tests/cases/fourslash/completionsLiteralOverload.ts new file mode 100644 index 0000000000000..09486afe1ab62 --- /dev/null +++ b/tests/cases/fourslash/completionsLiteralOverload.ts @@ -0,0 +1,14 @@ +/// <reference path="fourslash.ts" /> + +//// interface Events { +//// drag: any; +//// dragenter: any; +//// } +//// declare function addListener<K extends keyof Events>(type: K, listener: (ev: Events[K]) => any): void; +//// declare function addListener(type: string, listener: (ev: any) => any): void; +//// +//// addListener("/**/"); + +verify.completions({ marker: "", isNewIdentifierLocation: true, exact: ["drag", "dragenter"] }); +edit.insert("drag"); +verify.completions({ isNewIdentifierLocation: true, exact: ["drag", "dragenter"] }); \ No newline at end of file From 0d266cd0f0dea867b64540fe1a41e098d20c0d00 Mon Sep 17 00:00:00 2001 From: Andrew Branch <andrew@wheream.io> Date: Wed, 23 Mar 2022 10:40:12 -0700 Subject: [PATCH 2/2] Fix string literal completions when a partially-typed string fixes inference to a type parameter Ugghhghgh Revert "Ugghhghgh" This reverts commit cad98b988bf05b98b91daaf69a148bf92e4a98f7. FSDjkl;afsdjklsfdjksdfkjjk Revert "FSDjkl;afsdjklsfdjksdfkjjk" This reverts commit 0cc19c6bb6c88ad45bcd154ccf13d85a97f7850e. Revert "Revert "Ugghhghgh"" This reverts commit 44434a34965151375416652928e35a3f8c444020. It works --- src/compiler/checker.ts | 90 +++++++++++-------- src/compiler/types.ts | 1 + src/harness/fourslashImpl.ts | 10 +-- src/services/stringCompletions.ts | 19 ++-- .../fourslash/completionsLiteralOverload.ts | 17 +++- 5 files changed, 84 insertions(+), 53 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0d7adfafede23..50382f5b62d84 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -175,15 +175,16 @@ namespace ts { } const enum CheckMode { - Normal = 0, // Normal type checking - Contextual = 1 << 0, // Explicitly assigned contextual type, therefore not cacheable - Inferential = 1 << 1, // Inferential typing - SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions - SkipGenericFunctions = 1 << 3, // Skip single signature generic functions - IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help - RestBindingElement = 1 << 5, // Checking a type that is going to be used to determine the type of a rest binding element - // e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`, - // we need to preserve generic types instead of substituting them for constraints + Normal = 0, // Normal type checking + Contextual = 1 << 0, // Explicitly assigned contextual type, therefore not cacheable + Inferential = 1 << 1, // Inferential typing + SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions + SkipGenericFunctions = 1 << 3, // Skip single signature generic functions + IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help + IsForStringLiteralArgumentCompletions = 1 << 5, // Do not infer from the argument currently being typed + RestBindingElement = 1 << 6, // Checking a type that is going to be used to determine the type of a rest binding element + // e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`, + // we need to preserve generic types instead of substituting them for constraints } const enum SignatureCheckMode { @@ -532,26 +533,10 @@ namespace ts { if (!node) { return undefined; } - const containingCall = findAncestor(node, isCallLikeExpression); - const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature; - if (contextFlags! & ContextFlags.Completions && containingCall) { - let toMarkSkip = node as Node; - do { - getNodeLinks(toMarkSkip).skipDirectInference = true; - toMarkSkip = toMarkSkip.parent; - } while (toMarkSkip && toMarkSkip !== containingCall); - getNodeLinks(containingCall).resolvedSignature = undefined; - } - const result = getContextualType(node, contextFlags); - if (contextFlags! & ContextFlags.Completions && containingCall) { - let toMarkSkip = node as Node; - do { - getNodeLinks(toMarkSkip).skipDirectInference = undefined; - toMarkSkip = toMarkSkip.parent; - } while (toMarkSkip && toMarkSkip !== containingCall); - getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature; + if (contextFlags! & ContextFlags.Completions) { + return runWithInferenceBlockedFromSourceNode(node, () => getContextualType(node, contextFlags)); } - return result; + return getContextualType(node, contextFlags); }, getContextualTypeForObjectLiteralElement: nodeIn => { const node = getParseTreeNode(nodeIn, isObjectLiteralElementLike); @@ -570,6 +555,8 @@ namespace ts { getFullyQualifiedName, getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal), + getResolvedSignatureForStringLiteralCompletions: (call, editingArgument, candidatesOutArray) => + getResolvedSignatureWorker(call, candidatesOutArray, /*argumentCount*/ undefined, CheckMode.IsForStringLiteralArgumentCompletions, editingArgument), getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp), getExpandedParameters, @@ -739,10 +726,36 @@ namespace ts { getMemberOverrideModifierStatus, }; - function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined { + function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T { + const containingCall = findAncestor(node, isCallLikeExpression); + const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature; + if (containingCall) { + let toMarkSkip = node!; + do { + getNodeLinks(toMarkSkip).skipDirectInference = true; + toMarkSkip = toMarkSkip.parent; + } while (toMarkSkip && toMarkSkip !== containingCall); + getNodeLinks(containingCall).resolvedSignature = undefined; + } + const result = fn(); + if (containingCall) { + let toMarkSkip = node!; + do { + getNodeLinks(toMarkSkip).skipDirectInference = undefined; + toMarkSkip = toMarkSkip.parent; + } while (toMarkSkip && toMarkSkip !== containingCall); + getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature; + } + return result; + } + + function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode, editingArgument?: Node): Signature | undefined { const node = getParseTreeNode(nodeIn, isCallLikeExpression); apparentArgumentCount = argumentCount; - const res = node ? getResolvedSignature(node, candidatesOutArray, checkMode) : undefined; + const res = + !node ? undefined : + editingArgument ? runWithInferenceBlockedFromSourceNode(editingArgument, () => getResolvedSignature(node, candidatesOutArray, checkMode)) : + getResolvedSignature(node, candidatesOutArray, checkMode); apparentArgumentCount = undefined; return res; } @@ -22662,7 +22675,7 @@ namespace ts { const properties = getPropertiesOfObjectType(target); for (const targetProp of properties) { const sourceProp = getPropertyOfType(source, targetProp.escapedName); - if (sourceProp) { + if (sourceProp && !some(sourceProp.declarations, hasSkipDirectInferenceFlag)) { inferFromTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); } } @@ -29760,7 +29773,7 @@ namespace ts { 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); const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode); inferTypes(context.inferences, argType, paramType); @@ -30500,7 +30513,7 @@ namespace ts { } } - return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray); + return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray, checkMode); function addImplementationSuccessElaboration(failed: Signature, diagnostic: Diagnostic) { const oldCandidatesForArgumentError = candidatesForArgumentError; @@ -30614,6 +30627,7 @@ namespace ts { candidates: Signature[], args: readonly Expression[], hasCandidatesOutArray: boolean, + checkMode: CheckMode, ): Signature { Debug.assert(candidates.length > 0); // Else should not have called this. checkNodeDeferred(node); @@ -30621,7 +30635,7 @@ namespace ts { // 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) + ? pickLongestCandidateSignature(node, candidates, args, checkMode) : createUnionOfSignaturesForOverloadFailure(candidates); } @@ -30675,7 +30689,7 @@ namespace ts { return createSymbolWithType(first(sources), type); } - function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: readonly Expression[]): Signature { + function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: readonly Expression[], checkMode: CheckMode): 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({ | @@ -30692,7 +30706,7 @@ namespace ts { const typeArgumentNodes: readonly TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined; const instantiated = typeArgumentNodes ? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, isInJSFile(node))) - : inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args); + : inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args, checkMode); candidates[bestIndex] = instantiated; return instantiated; } @@ -30708,9 +30722,9 @@ namespace ts { return typeArguments; } - function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: Signature, args: readonly Expression[]): Signature { + function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: Signature, args: readonly Expression[], checkMode: CheckMode): Signature { const inferenceContext = createInferenceContext(typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); - const typeArgumentTypes = inferTypeArguments(node, candidate, args, CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext); + const typeArgumentTypes = inferTypeArguments(node, candidate, args, checkMode | CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext); return createSignatureInstantiation(candidate, typeArgumentTypes); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3fee2e6eac415..5b83673b91415 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4279,6 +4279,7 @@ namespace ts { */ 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[]): Signature | undefined; /* @internal */ getExpandedParameters(sig: Signature): readonly (readonly Symbol[])[]; /* @internal */ hasEffectiveRestParameter(sig: Signature): boolean; /* @internal */ containsArgumentsReference(declaration: SignatureDeclaration): boolean; diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index b8513c3744297..24587abed5ce9 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -168,7 +168,7 @@ namespace FourSlash { // The position of the end of the current selection, or -1 if nothing is selected public selectionEnd = -1; - public lastKnownMarker = ""; + public lastKnownMarker: string | undefined; // The file that's currently 'opened' public activeFile!: FourSlashFile; @@ -400,7 +400,7 @@ namespace FourSlash { continue; } const memo = Utils.memoize( - (_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string, ...args: any[]) => (ls[key] as Function)(...args), + (_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string | undefined, ...args: any[]) => (ls[key] as Function)(...args), (...args) => args.map(a => a && typeof a === "object" ? JSON.stringify(a) : a).join("|,|") ); proxy[key] = (...args: any[]) => memo( @@ -540,8 +540,8 @@ namespace FourSlash { } private messageAtLastKnownMarker(message: string) { - const locationDescription = this.lastKnownMarker ? this.lastKnownMarker : this.getLineColStringAtPosition(this.currentCaretPosition); - return `At ${locationDescription}: ${message}`; + const locationDescription = this.lastKnownMarker !== undefined ? this.lastKnownMarker : this.getLineColStringAtPosition(this.currentCaretPosition); + return `At marker '${locationDescription}': ${message}`; } private assertionMessageAtLastKnownMarker(msg: string) { @@ -864,7 +864,7 @@ namespace FourSlash { else { for (const marker of toArray(options.marker)) { this.goToMarker(marker); - this.verifyCompletionsWorker(options); + this.verifyCompletionsWorker({ ...options, marker }); } } } diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index ecf3fd73642ce..61f90c9817d7d 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -210,12 +210,13 @@ namespace ts.Completions.StringCompletions { case SyntaxKind.CallExpression: case SyntaxKind.NewExpression: + case SyntaxKind.JsxAttribute: if (!isRequireCallArgument(node) && !isImportCall(parent)) { - const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(node, position, sourceFile); + const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile); // Get string literal completions from specialized signatures of the target // i.e. declare function f(a: 'A'); // f("/*completion position*/") - return argumentInfo ? getStringLiteralCompletionsFromSignature(argumentInfo, typeChecker) : fromContextualType(); + return argumentInfo ? getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) : fromContextualType(); } // falls through (is `require("")` or `require(""` or `import("")`) @@ -257,15 +258,21 @@ namespace ts.Completions.StringCompletions { type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined); } - function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes { + function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes { let isNewIdentifier = false; - const uniques = new Map<string, true>(); const candidates: Signature[] = []; - checker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount); + const editingArgument = isJsxOpeningLikeElement(call) ? Debug.checkDefined(findAncestor(arg.parent, isJsxAttribute)) : arg; + checker.getResolvedSignatureForStringLiteralCompletions(call, editingArgument, candidates); const types = flatMap(candidates, candidate => { if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return; - const type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex); + let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex); + if (isJsxOpeningLikeElement(call)) { + const propType = checker.getTypeOfPropertyOfType(type, (editingArgument as JsxAttribute).name.text); + if (propType) { + type = propType; + } + } isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String); return getStringLiteralTypes(type, uniques); }); diff --git a/tests/cases/fourslash/completionsLiteralOverload.ts b/tests/cases/fourslash/completionsLiteralOverload.ts index 09486afe1ab62..c8d51a52bf63c 100644 --- a/tests/cases/fourslash/completionsLiteralOverload.ts +++ b/tests/cases/fourslash/completionsLiteralOverload.ts @@ -1,14 +1,23 @@ /// <reference path="fourslash.ts" /> +// @allowJs: true + +// @Filename: /a.tsx //// interface Events { +//// "": any; //// drag: any; //// dragenter: any; //// } //// declare function addListener<K extends keyof Events>(type: K, listener: (ev: Events[K]) => any): void; -//// declare function addListener(type: string, listener: (ev: any) => any): void; //// -//// addListener("/**/"); +//// declare function ListenerComponent<K extends keyof Events>(props: { type: K, onWhatever: (ev: Events[K]) => void }): JSX.Element; +//// +//// addListener("/*ts*/"); +//// (<ListenerComponent type="/*tsx*/" />); + +// @Filename: /b.js +//// addListener("/*js*/"); -verify.completions({ marker: "", isNewIdentifierLocation: true, exact: ["drag", "dragenter"] }); +verify.completions({ marker: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] }); edit.insert("drag"); -verify.completions({ isNewIdentifierLocation: true, exact: ["drag", "dragenter"] }); \ No newline at end of file +verify.completions({ marker: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] }); \ No newline at end of file