-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix string literal completions when a partially-typed string fixes inference to a type parameter #48410
Fix string literal completions when a partially-typed string fixes inference to a type parameter #48410
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it would be nice to skip this at least in non-services calls, but it would be a lot of plumbing to get There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure you could translate the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A dummy |
||
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,14 +30627,15 @@ 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); | ||
// Normally we will combine overloads. Skip this if they have type parameters since that's hard to combine. | ||
// 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); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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 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: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] }); | ||
edit.insert("drag"); | ||
verify.completions({ marker: ["ts", "tsx", "js"], exact: ["", "drag", "dragenter"] }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also have a test where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do output There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (The test you’re suggesting is already what’s happening on this line) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh, ok, I misread - thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just add an
@internal
overload forgetResolvedSignature
that allowsCheckMode
to be passed in?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
editingArgument
is also needed, andargumentCount
isn’t needed... and I like that the name implies the signature you’re going to get is in fact not at all the same signature you would get for normal checking purposes.