Skip to content

Commit ef8eb0c

Browse files
Fix contextually typed object literal completions where the object being edited affects its own inference (#36556)
* Conditionally elide a parameter from contextual type signature calculation * Slightly different approach to forbid inference to specific expressions * Handle nested literals and mapped types correctly * Delete unused cache * Rename ContextFlags.BaseConstraint and related usage * Add tests from my PR * Update ContextFlags comment Co-Authored-By: Wesley Wigham <[email protected]> * Update comments and fourslash triple slash refs Co-authored-by: Wesley Wigham <[email protected]>
1 parent ad24904 commit ef8eb0c

8 files changed

+253
-25
lines changed

src/compiler/checker.ts

+40-13
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,29 @@ namespace ts {
467467
getRootSymbols,
468468
getContextualType: (nodeIn: Expression, contextFlags?: ContextFlags) => {
469469
const node = getParseTreeNode(nodeIn, isExpression);
470-
return node ? getContextualType(node, contextFlags) : undefined;
470+
if (!node) {
471+
return undefined;
472+
}
473+
const containingCall = findAncestor(node, isCallLikeExpression);
474+
const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature;
475+
if (contextFlags! & ContextFlags.Completions && containingCall) {
476+
let toMarkSkip = node as Node;
477+
do {
478+
getNodeLinks(toMarkSkip).skipDirectInference = true;
479+
toMarkSkip = toMarkSkip.parent;
480+
} while (toMarkSkip && toMarkSkip !== containingCall);
481+
getNodeLinks(containingCall).resolvedSignature = undefined;
482+
}
483+
const result = getContextualType(node, contextFlags);
484+
if (contextFlags! & ContextFlags.Completions && containingCall) {
485+
let toMarkSkip = node as Node;
486+
do {
487+
getNodeLinks(toMarkSkip).skipDirectInference = undefined;
488+
toMarkSkip = toMarkSkip.parent;
489+
} while (toMarkSkip && toMarkSkip !== containingCall);
490+
getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature;
491+
}
492+
return result;
471493
},
472494
getContextualTypeForObjectLiteralElement: nodeIn => {
473495
const node = getParseTreeNode(nodeIn, isObjectLiteralElementLike);
@@ -17796,6 +17818,14 @@ namespace ts {
1779617818
undefined;
1779717819
}
1779817820

17821+
function hasSkipDirectInferenceFlag(node: Node) {
17822+
return !!getNodeLinks(node).skipDirectInference;
17823+
}
17824+
17825+
function isFromInferenceBlockedSource(type: Type) {
17826+
return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag));
17827+
}
17828+
1779917829
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
1780017830
let symbolStack: Symbol[];
1780117831
let visited: Map<number>;
@@ -17886,7 +17916,7 @@ namespace ts {
1788617916
// of inference. Also, we exclude inferences for silentNeverType (which is used as a wildcard
1788717917
// when constructing types from type parameters that had no inference candidates).
1788817918
if (getObjectFlags(source) & ObjectFlags.NonInferrableType || source === nonInferrableAnyType || source === silentNeverType ||
17889-
(priority & InferencePriority.ReturnType && (source === autoType || source === autoArrayType))) {
17919+
(priority & InferencePriority.ReturnType && (source === autoType || source === autoArrayType)) || isFromInferenceBlockedSource(source)) {
1789017920
return;
1789117921
}
1789217922
const inference = getInferenceInfoForType(target);
@@ -18190,7 +18220,7 @@ namespace ts {
1819018220
// type and then make a secondary inference from that type to T. We make a secondary inference
1819118221
// such that direct inferences to T get priority over inferences to Partial<T>, for example.
1819218222
const inference = getInferenceInfoForType((<IndexType>constraintType).type);
18193-
if (inference && !inference.isFixed) {
18223+
if (inference && !inference.isFixed && !isFromInferenceBlockedSource(source)) {
1819418224
const inferredType = inferTypeForHomomorphicMappedType(source, target, <IndexType>constraintType);
1819518225
if (inferredType) {
1819618226
// We assign a lower priority to inferences made from types containing non-inferrable
@@ -21449,19 +21479,16 @@ namespace ts {
2144921479
}
2145021480

2145121481
// In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter.
21452-
function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression, contextFlags?: ContextFlags): Type | undefined {
21482+
function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression): Type | undefined {
2145321483
const args = getEffectiveCallArguments(callTarget);
2145421484
const argIndex = args.indexOf(arg); // -1 for e.g. the expression of a CallExpression, or the tag of a TaggedTemplateExpression
21455-
return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex, contextFlags);
21485+
return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex);
2145621486
}
2145721487

21458-
function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number, contextFlags?: ContextFlags): Type {
21488+
function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number): Type {
2145921489
// If we're already in the process of resolving the given signature, don't resolve again as
2146021490
// that could cause infinite recursion. Instead, return anySignature.
21461-
let signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget);
21462-
if (contextFlags && contextFlags & ContextFlags.BaseConstraint && signature.target && !hasTypeArguments(callTarget)) {
21463-
signature = getBaseSignature(signature.target);
21464-
}
21491+
const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget);
2146521492

2146621493
if (isJsxOpeningLikeElement(callTarget) && argIndex === 0) {
2146721494
return getEffectiveFirstArgumentForJsxSignature(signature, callTarget);
@@ -21857,7 +21884,7 @@ namespace ts {
2185721884
}
2185821885
/* falls through */
2185921886
case SyntaxKind.NewExpression:
21860-
return getContextualTypeForArgument(<CallExpression | NewExpression>parent, node, contextFlags);
21887+
return getContextualTypeForArgument(<CallExpression | NewExpression>parent, node);
2186121888
case SyntaxKind.TypeAssertionExpression:
2186221889
case SyntaxKind.AsExpression:
2186321890
return isConstTypeReference((<AssertionExpression>parent).type) ? undefined : getTypeFromTypeNode((<AssertionExpression>parent).type);
@@ -21901,13 +21928,13 @@ namespace ts {
2190121928
}
2190221929

2190321930
function getContextualJsxElementAttributesType(node: JsxOpeningLikeElement, contextFlags?: ContextFlags) {
21904-
if (isJsxOpeningElement(node) && node.parent.contextualType && contextFlags !== ContextFlags.BaseConstraint) {
21931+
if (isJsxOpeningElement(node) && node.parent.contextualType && contextFlags !== ContextFlags.Completions) {
2190521932
// Contextually applied type is moved from attributes up to the outer jsx attributes so when walking up from the children they get hit
2190621933
// _However_ to hit them from the _attributes_ we must look for them here; otherwise we'll used the declared type
2190721934
// (as below) instead!
2190821935
return node.parent.contextualType;
2190921936
}
21910-
return getContextualTypeForArgumentAtIndex(node, 0, contextFlags);
21937+
return getContextualTypeForArgumentAtIndex(node, 0);
2191121938
}
2191221939

2191321940
function getEffectiveFirstArgumentForJsxSignature(signature: Signature, node: JsxOpeningLikeElement) {

src/compiler/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3611,7 +3611,8 @@ namespace ts {
36113611
None = 0,
36123612
Signature = 1 << 0, // Obtaining contextual signature
36133613
NoConstraints = 1 << 1, // Don't obtain type variable constraints
3614-
BaseConstraint = 1 << 2, // Use base constraint type for completions
3614+
Completions = 1 << 2, // Ignore inference to current node and parent nodes out to the containing call for completions
3615+
36153616
}
36163617

36173618
// NOTE: If modifying this enum, must modify `TypeFormatFlags` too!
@@ -4249,6 +4250,7 @@ namespace ts {
42494250
outerTypeParameters?: TypeParameter[]; // Outer type parameters of anonymous object type
42504251
instantiations?: Map<Type>; // Instantiations of generic type alias (undefined if non-generic)
42514252
isExhaustive?: boolean; // Is node an exhaustive switch statement
4253+
skipDirectInference?: true; // Flag set by the API `getContextualType` call on a node when `Completions` is passed to force the checker to skip making inferences to a node's type
42524254
}
42534255

42544256
export const enum TypeFlags {

src/services/completions.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -1280,8 +1280,8 @@ namespace ts.Completions {
12801280
// Cursor is inside a JSX self-closing element or opening element
12811281
const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes);
12821282
if (!attrsType) return GlobalsSearch.Continue;
1283-
const baseType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes, ContextFlags.BaseConstraint);
1284-
symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, baseType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties);
1283+
const completionsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes, ContextFlags.Completions);
1284+
symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties);
12851285
setSortTextToOptionalMember();
12861286
completionKind = CompletionKind.MemberLike;
12871287
isNewIdentifierLocation = false;
@@ -1800,10 +1800,10 @@ namespace ts.Completions {
18001800

18011801
if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) {
18021802
const instantiatedType = typeChecker.getContextualType(objectLikeContainer);
1803-
const baseType = instantiatedType && typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint);
1804-
if (!instantiatedType || !baseType) return GlobalsSearch.Fail;
1805-
isNewIdentifierLocation = hasIndexSignature(instantiatedType || baseType);
1806-
typeMembers = getPropertiesForObjectExpression(instantiatedType, baseType, objectLikeContainer, typeChecker);
1803+
const completionsType = instantiatedType && typeChecker.getContextualType(objectLikeContainer, ContextFlags.Completions);
1804+
if (!instantiatedType || !completionsType) return GlobalsSearch.Fail;
1805+
isNewIdentifierLocation = hasIndexSignature(instantiatedType || completionsType);
1806+
typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker);
18071807
existingMembers = objectLikeContainer.properties;
18081808
}
18091809
else {
@@ -2549,10 +2549,10 @@ namespace ts.Completions {
25492549
return jsdoc && jsdoc.tags && (rangeContainsPosition(jsdoc, position) ? findLast(jsdoc.tags, tag => tag.pos < position) : undefined);
25502550
}
25512551

2552-
function getPropertiesForObjectExpression(contextualType: Type, baseConstrainedType: Type | undefined, obj: ObjectLiteralExpression | JsxAttributes, checker: TypeChecker): Symbol[] {
2553-
const hasBaseType = baseConstrainedType && baseConstrainedType !== contextualType;
2554-
const type = hasBaseType && !(baseConstrainedType!.flags & TypeFlags.AnyOrUnknown)
2555-
? checker.getUnionType([contextualType, baseConstrainedType!])
2552+
function getPropertiesForObjectExpression(contextualType: Type, completionsType: Type | undefined, obj: ObjectLiteralExpression | JsxAttributes, checker: TypeChecker): Symbol[] {
2553+
const hasCompletionsType = completionsType && completionsType !== contextualType;
2554+
const type = hasCompletionsType && !(completionsType!.flags & TypeFlags.AnyOrUnknown)
2555+
? checker.getUnionType([contextualType, completionsType!])
25562556
: contextualType;
25572557

25582558
const properties = type.isUnion()
@@ -2564,7 +2564,7 @@ namespace ts.Completions {
25642564
checker.isTypeInvalidDueToUnionDiscriminant(memberType, obj))))
25652565
: type.getApparentProperties();
25662566

2567-
return hasBaseType ? properties.filter(hasDeclarationOtherThanSelf) : properties;
2567+
return hasCompletionsType ? properties.filter(hasDeclarationOtherThanSelf) : properties;
25682568

25692569
// Filter out members whose only declaration is the object literal itself to avoid
25702570
// self-fulfilling completions like:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface CustomElements {
4+
//// 'component-one': {
5+
//// foo?: string;
6+
//// },
7+
//// 'component-two': {
8+
//// bar?: string;
9+
//// }
10+
////}
11+
////
12+
////interface Options<T extends keyof CustomElements> {
13+
//// props: CustomElements[T];
14+
////}
15+
////
16+
////declare function create<T extends keyof CustomElements>(name: T, options: Options<T>): void;
17+
////
18+
////create('component-one', { props: { /*1*/ } });
19+
////create('component-two', { props: { /*2*/ } });
20+
21+
verify.completions({
22+
marker: "1",
23+
exact: [{
24+
name: "foo",
25+
sortText: completion.SortText.OptionalMember
26+
}]
27+
});
28+
29+
verify.completions({
30+
marker: "2",
31+
exact: [{
32+
name: "bar",
33+
sortText: completion.SortText.OptionalMember
34+
}]
35+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface CustomElements {
4+
//// 'component-one': {
5+
//// foo?: string;
6+
//// },
7+
//// 'component-two': {
8+
//// bar?: string;
9+
//// }
10+
////}
11+
////
12+
////interface Options<T extends keyof CustomElements> {
13+
//// props: CustomElements[T];
14+
////}
15+
////
16+
////declare function create<T extends 'hello' | 'goodbye'>(name: T, options: Options<T extends 'hello' ? 'component-one' : 'component-two'>): void;
17+
////declare function create<T extends keyof CustomElements>(name: T, options: Options<T>): void;
18+
////
19+
////create('hello', { props: { /*1*/ } })
20+
////create('goodbye', { props: { /*2*/ } })
21+
////create('component-one', { props: { /*3*/ } });
22+
23+
verify.completions({
24+
marker: "1",
25+
exact: [{
26+
name: "foo",
27+
sortText: completion.SortText.OptionalMember
28+
}]
29+
});
30+
31+
verify.completions({
32+
marker: "2",
33+
exact: [{
34+
name: "bar",
35+
sortText: completion.SortText.OptionalMember
36+
}]
37+
});
38+
39+
verify.completions({
40+
marker: "3",
41+
exact: [{
42+
name: "foo",
43+
sortText: completion.SortText.OptionalMember
44+
}]
45+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface CustomElements {
4+
//// 'component-one': {
5+
//// foo?: string;
6+
//// },
7+
//// 'component-two': {
8+
//// bar?: string;
9+
//// }
10+
////}
11+
////
12+
////interface Options<T extends keyof CustomElements> {
13+
//// props?: {} & { x: CustomElements[(T extends string ? T : never) & string][] }['x'];
14+
////}
15+
////
16+
////declare function f<T extends keyof CustomElements>(k: T, options: Options<T>): void;
17+
////
18+
////f("component-one", {
19+
//// props: [{
20+
//// /**/
21+
//// }]
22+
////})
23+
24+
verify.completions({
25+
marker: "",
26+
exact: [{
27+
name: "foo",
28+
sortText: completion.SortText.OptionalMember
29+
}]
30+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: component.tsx
4+
5+
////interface CustomElements {
6+
//// 'component-one': {
7+
//// foo?: string;
8+
//// },
9+
//// 'component-two': {
10+
//// bar?: string;
11+
//// }
12+
////}
13+
////
14+
////type Options<T extends keyof CustomElements> = { kind: T } & Required<{ x: CustomElements[(T extends string ? T : never) & string] }['x']>;
15+
////
16+
////declare function Component<T extends keyof CustomElements>(props: Options<T>): void;
17+
////
18+
////const c = <Component /**/ kind="component-one" />
19+
20+
verify.completions({
21+
marker: "",
22+
exact: [{
23+
name: "foo"
24+
}]
25+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface MyOptions {
4+
//// hello?: boolean;
5+
//// world?: boolean;
6+
////}
7+
////declare function bar<T extends MyOptions>(options?: Partial<T>): void;
8+
////bar({ hello: true, /*1*/ });
9+
////
10+
////interface Test {
11+
//// keyPath?: string;
12+
//// autoIncrement?: boolean;
13+
////}
14+
////
15+
////function test<T extends Record<string, Test>>(opt: T) { }
16+
////
17+
////test({
18+
//// a: {
19+
//// keyPath: 'x.y',
20+
//// autoIncrement: true
21+
//// },
22+
//// b: {
23+
//// /*2*/
24+
//// }
25+
////});
26+
////type Colors = {
27+
//// rgb: { r: number, g: number, b: number };
28+
//// hsl: { h: number, s: number, l: number }
29+
////};
30+
////
31+
////function createColor<T extends keyof Colors>(kind: T, values: Colors[T]) { }
32+
////
33+
////createColor('rgb', {
34+
//// /*3*/
35+
////});
36+
////
37+
////declare function f<T extends 'a' | 'b', U extends { a?: string }, V extends { b?: string }>(x: T, y: { a: U, b: V }[T]): void;
38+
////
39+
////f('a', {
40+
//// /*4*/
41+
////});
42+
////
43+
////declare function f2<T extends { x?: string }>(x: T): void;
44+
////f2({
45+
//// /*5*/
46+
////});
47+
////
48+
////type X = { a: { a }, b: { b } }
49+
////
50+
////function f4<T extends 'a' | 'b'>(p: { kind: T } & X[T]) { }
51+
////
52+
////f4({
53+
//// kind: "a",
54+
//// /*6*/
55+
////})
56+
57+
verify.completions(
58+
{ marker: "1", exact: [{ name: "world", sortText: completion.SortText.OptionalMember }] },
59+
{ marker: "2", exact: [{ name: "keyPath", sortText: completion.SortText.OptionalMember }, { name: "autoIncrement", sortText: completion.SortText.OptionalMember }] },
60+
{ marker: "3", exact: ["r", "g", "b"] },
61+
{ marker: "4", exact: [{ name: "a", sortText: completion.SortText.OptionalMember }] },
62+
{ marker: "5", exact: [{ name: "x", sortText: completion.SortText.OptionalMember }] },
63+
{ marker: "6", exact: ["a"] },
64+
);

0 commit comments

Comments
 (0)