Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ef8eb0c

Browse filesBrowse files
Fix contextually typed object literal completions where the object being edited affects its own inference (microsoft#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 <wwigham@gmail.com> * Update comments and fourslash triple slash refs Co-authored-by: Wesley Wigham <wwigham@gmail.com>
1 parent ad24904 commit ef8eb0c
Copy full SHA for ef8eb0c

File tree

Expand file treeCollapse file tree

8 files changed

+253
-25
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

8 files changed

+253
-25
lines changed
Open diff view settings
Collapse file

‎src/compiler/checker.ts‎

Copy file name to clipboardExpand all lines: src/compiler/checker.ts
+40-13Lines changed: 40 additions & 13 deletions
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) {
Collapse file

‎src/compiler/types.ts‎

Copy file name to clipboardExpand all lines: src/compiler/types.ts
+3-1Lines changed: 3 additions & 1 deletion
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 {
Collapse file

‎src/services/completions.ts‎

Copy file name to clipboardExpand all lines: src/services/completions.ts
+11-11Lines changed: 11 additions & 11 deletions
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:
Collapse file
+35Lines changed: 35 additions & 0 deletions
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+
});
Collapse file
+45Lines changed: 45 additions & 0 deletions
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+
});
Collapse file
+30Lines changed: 30 additions & 0 deletions
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+
});
Collapse file
+25Lines changed: 25 additions & 0 deletions
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+
})
Collapse file
+64Lines changed: 64 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.