diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0991030bff6cc..9fbf69c56fec2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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(); + 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(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; - } - const result = fn(); - for (const [nodeLinks, resolvedSignature] of cachedResolvedSignatures) { - nodeLinks.resolvedSignature = resolvedSignature; - } - for (const [symbolLinks, type] of cachedTypes) { - symbolLinks.type = type; + for (const [symbolLinks, type] of cachedTypes) { + symbolLinks.type = type; + } + return result; } - return result; + return fn(); } function runWithInferenceBlockedFromSourceNode(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 diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1f796f8b2a0da..135f022cdee20 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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; diff --git a/src/compiler/utilitiesPublic.ts b/src/compiler/utilitiesPublic.ts index 1b150d28e0f3b..37257c679eb6e 100644 --- a/src/compiler/utilitiesPublic.ts +++ b/src/compiler/utilitiesPublic.ts @@ -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: diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index c19aaf541beb8..47ec1126728d0 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -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(); - 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); diff --git a/tests/cases/fourslash/stringLiteralCompletionsForGenericConditionalTypesUsingTemplateLiteralTypes.ts b/tests/cases/fourslash/stringLiteralCompletionsForGenericConditionalTypesUsingTemplateLiteralTypes.ts new file mode 100644 index 0000000000000..0074976300ce3 --- /dev/null +++ b/tests/cases/fourslash/stringLiteralCompletionsForGenericConditionalTypesUsingTemplateLiteralTypes.ts @@ -0,0 +1,14 @@ +/// + +// repro from https://github.com/microsoft/TypeScript/issues/49680 + +//// type PathOf = +//// K extends `${infer U}.${infer V}` +//// ? U extends keyof T ? PathOf : `${P}${keyof T & (string | number)}` +//// : K extends keyof T ? `${P}${K}` : `${P}${keyof T & (string | number)}`; +//// +//// declare function consumer(path: PathOf<{a: string, b: {c: string}}, K>) : number; +//// +//// consumer('b./*ts*/') + +verify.completions({ marker: ["ts"], exact: ["a", "b", "b.c"] }); diff --git a/tests/cases/fourslash/stringLiteralCompletionsInPositionTypedUsingRest.ts b/tests/cases/fourslash/stringLiteralCompletionsInPositionTypedUsingRest.ts new file mode 100644 index 0000000000000..bd4a11eac9e00 --- /dev/null +++ b/tests/cases/fourslash/stringLiteralCompletionsInPositionTypedUsingRest.ts @@ -0,0 +1,21 @@ +/// + +// repro from https://github.com/microsoft/TypeScript/issues/49680#issuecomment-1249191842 + +//// declare function pick(obj: T, ...keys: K[]): Pick; +//// declare function pick2(obj: T, ...keys: K): Pick; +//// +//// 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 { +//// public select(...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"] });