From 2f798a548c08f214de48d8988e13aee7585a3d4b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 10 Jan 2020 10:10:28 -0800 Subject: [PATCH 1/4] Exclude indexed access type object literal completions from mixing in base constraint completions --- src/compiler/checker.ts | 12 ++++- src/compiler/types.ts | 1 + src/services/completions.ts | 11 +++-- .../completionsGenericIndexedAccess3.ts | 35 +++++++++++++++ .../completionsGenericIndexedAccess4.ts | 45 +++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 tests/cases/fourslash/completionsGenericIndexedAccess3.ts create mode 100644 tests/cases/fourslash/completionsGenericIndexedAccess4.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b1f516dad69d9..65e43c1278944 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -21337,11 +21337,19 @@ namespace ts { return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex, contextFlags); } - function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number, contextFlags?: ContextFlags): Type { + function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number, contextFlags?: ContextFlags): Type | undefined { // If we're already in the process of resolving the given signature, don't resolve again as // that could cause infinite recursion. Instead, return anySignature. let signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget); - if (contextFlags && contextFlags & ContextFlags.BaseConstraint && signature.target && !hasTypeArguments(callTarget)) { + + if (contextFlags && contextFlags & ContextFlags.Uninstantiated) { + return signature.target ? getTypeAtPosition(signature.target, argIndex) : undefined; + } + + if (contextFlags && contextFlags & ContextFlags.BaseConstraint) { + if (!signature.target || hasTypeArguments(callTarget)) { + return undefined; + } signature = getBaseSignature(signature.target); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 98f2531b6c6d1..17f080385b58b 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3605,6 +3605,7 @@ namespace ts { Signature = 1 << 0, // Obtaining contextual signature NoConstraints = 1 << 1, // Don't obtain type variable constraints BaseConstraint = 1 << 2, // Use base constraint type for completions + Uninstantiated = 1 << 3, // Attempt to get the type from an uninstantiated signature } // NOTE: If modifying this enum, must modify `TypeFormatFlags` too! diff --git a/src/services/completions.ts b/src/services/completions.ts index 471d8e4c142c6..112edab3e322e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1800,9 +1800,14 @@ namespace ts.Completions { if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) { const instantiatedType = typeChecker.getContextualType(objectLikeContainer); - const baseType = instantiatedType && typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint); - if (!instantiatedType || !baseType) return GlobalsSearch.Fail; - isNewIdentifierLocation = hasIndexSignature(instantiatedType || baseType); + if (!instantiatedType) return GlobalsSearch.Fail; + const uninstantiatedType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.Uninstantiated); + let baseType; + if (!(uninstantiatedType && uninstantiatedType.flags & TypeFlags.IndexedAccess)) { + baseType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint); + } + + isNewIdentifierLocation = hasIndexSignature(instantiatedType); typeMembers = getPropertiesForObjectExpression(instantiatedType, baseType, objectLikeContainer, typeChecker); existingMembers = objectLikeContainer.properties; } diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess3.ts b/tests/cases/fourslash/completionsGenericIndexedAccess3.ts new file mode 100644 index 0000000000000..730cdd8f631dd --- /dev/null +++ b/tests/cases/fourslash/completionsGenericIndexedAccess3.ts @@ -0,0 +1,35 @@ +/// + +////interface CustomElements { +//// 'component-one': { +//// foo?: string; +//// }, +//// 'component-two': { +//// bar?: string; +//// } +////} +//// +////interface Options { +//// props: CustomElements[T]; +////} +//// +////declare function create(name: T, options: Options): void; +//// +////create('component-one', { props: { /*1*/ } }); +////create('component-two', { props: { /*2*/ } }); + +verify.completions({ + marker: "1", + exact: [{ + name: "foo", + sortText: completion.SortText.OptionalMember + }] +}); + +verify.completions({ + marker: "2", + exact: [{ + name: "bar", + sortText: completion.SortText.OptionalMember + }] +}); diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess4.ts b/tests/cases/fourslash/completionsGenericIndexedAccess4.ts new file mode 100644 index 0000000000000..0edeaedf9821f --- /dev/null +++ b/tests/cases/fourslash/completionsGenericIndexedAccess4.ts @@ -0,0 +1,45 @@ +/// + +////interface CustomElements { +//// 'component-one': { +//// foo?: string; +//// }, +//// 'component-two': { +//// bar?: string; +//// } +////} +//// +////interface Options { +//// props: CustomElements[T]; +////} +//// +////declare function create(name: T, options: Options): void; +////declare function create(name: T, options: Options): void; +//// +////create('hello', { props: { /*1*/ } }) +////create('goodbye', { props: { /*2*/ } }) +////create('component-one', { props: { /*3*/ } }); + +verify.completions({ + marker: "1", + exact: [{ + name: "foo", + sortText: completion.SortText.OptionalMember + }] +}); + +verify.completions({ + marker: "2", + exact: [{ + name: "bar", + sortText: completion.SortText.OptionalMember + }] +}); + +verify.completions({ + marker: "3", + exact: [{ + name: "foo", + sortText: completion.SortText.OptionalMember + }] +}); From 83ce8a4df526bf7ad650158f9fe2358ea1eca297 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 22 Jan 2020 16:21:14 -0800 Subject: [PATCH 2/4] Walk index type structure a bit to find type parameter in it --- src/services/completions.ts | 72 ++++++++++++++++++- .../completionsGenericIndexedAccess5.ts | 28 ++++++++ .../completionsGenericIndexedAccess6.ts | 23 ++++++ 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/cases/fourslash/completionsGenericIndexedAccess5.ts create mode 100644 tests/cases/fourslash/completionsGenericIndexedAccess6.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 112edab3e322e..bb1f4b4818fc0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1279,7 +1279,14 @@ namespace ts.Completions { // Cursor is inside a JSX self-closing element or opening element const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes); if (!attrsType) return GlobalsSearch.Continue; - const baseType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes, ContextFlags.BaseConstraint); + const uninstantiatedType = typeChecker.getContextualType(jsxContainer!, ContextFlags.Uninstantiated); + let baseType; + if (uninstantiatedType) { + const signature = tryGetContextualTypeProvidingSignature(jsxContainer!, typeChecker)?.target; + if (signature && !isIndexedAccessTypeWithTypeParameterIndex(uninstantiatedType, signature)) { + baseType = typeChecker.getContextualType(jsxContainer!.attributes, ContextFlags.BaseConstraint); + } + } symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, baseType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties); setSortTextToOptionalMember(); completionKind = CompletionKind.MemberLike; @@ -1803,8 +1810,11 @@ namespace ts.Completions { if (!instantiatedType) return GlobalsSearch.Fail; const uninstantiatedType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.Uninstantiated); let baseType; - if (!(uninstantiatedType && uninstantiatedType.flags & TypeFlags.IndexedAccess)) { - baseType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint); + if (uninstantiatedType) { + const signature = tryGetContextualTypeProvidingSignature(objectLikeContainer, typeChecker)?.target; + if (signature && !isIndexedAccessTypeWithTypeParameterIndex(uninstantiatedType, signature)) { + baseType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint); + } } isNewIdentifierLocation = hasIndexSignature(instantiatedType); @@ -1857,6 +1867,62 @@ namespace ts.Completions { return GlobalsSearch.Success; } + function tryGetContextualTypeProvidingSignature(node: Node, checker: TypeChecker): Signature | undefined { + loop: while (true) { + switch (node.kind) { + case SyntaxKind.SpreadAssignment: + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.ParenthesizedExpression: + case SyntaxKind.ConditionalExpression: + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.JsxAttribute: + case SyntaxKind.JsxAttributes: + node = node.parent; + break; + default: + break loop; + } + } + if (!isCallLikeExpression(node) && !isJsxOpeningLikeElement(node)) { + return; + } + return checker.getResolvedSignature(node); + } + + function isIndexedAccessTypeWithTypeParameterIndex(type: Type, signature: Signature): boolean { + if (type.isUnionOrIntersection()) { + return some(type.types, t => isIndexedAccessTypeWithTypeParameterIndex(t, signature)); + } + if (type.flags & TypeFlags.IndexedAccess) { + return typeIsTypeParameterFromSignature((type as IndexedAccessType).indexType, signature); + } + return false; + } + + function typeIsTypeParameterFromSignature(type: Type, signature: Signature): boolean { + if (!signature.typeParameters) { + return false; + } + if (type.isUnionOrIntersection()) { + return some(type.types, t => typeIsTypeParameterFromSignature(t, signature)); + } + if (type.flags & TypeFlags.Conditional) { + return typeIsTypeParameterFromSignature((type as ConditionalType).checkType, signature) + || typeIsTypeParameterFromSignature((type as ConditionalType).extendsType, signature) + || typeIsTypeParameterFromSignature((type as ConditionalType).resolvedTrueType, signature) + || typeIsTypeParameterFromSignature((type as ConditionalType).resolvedFalseType, signature); + } + if (type.flags & TypeFlags.Index) { + return typeIsTypeParameterFromSignature((type as IndexType).type, signature); + } + if (type.flags & TypeFlags.TypeParameter) { + return some(signature.typeParameters, p => p.symbol === type.symbol); + } + return false; + } + /** * Aggregates relevant symbols for completion in import clauses and export clauses * whose declarations have a module specifier; for instance, symbols will be aggregated for diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess5.ts b/tests/cases/fourslash/completionsGenericIndexedAccess5.ts new file mode 100644 index 0000000000000..7f3a0aa369094 --- /dev/null +++ b/tests/cases/fourslash/completionsGenericIndexedAccess5.ts @@ -0,0 +1,28 @@ +////interface CustomElements { +//// 'component-one': { +//// foo?: string; +//// }, +//// 'component-two': { +//// bar?: string; +//// } +////} +//// +////interface Options { +//// props?: {} & { x: CustomElements[(T extends string ? T : never) & string][] }['x']; +////} +//// +////declare function f(k: T, options: Options): void; +//// +////f("component-one", { +//// props: [{ +//// /**/ +//// }] +////}) + +verify.completions({ + marker: "", + exact: [{ + name: "foo", + sortText: completion.SortText.OptionalMember + }] +}); diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess6.ts b/tests/cases/fourslash/completionsGenericIndexedAccess6.ts new file mode 100644 index 0000000000000..496ebf7b50ecf --- /dev/null +++ b/tests/cases/fourslash/completionsGenericIndexedAccess6.ts @@ -0,0 +1,23 @@ +// @Filename: component.tsx + +////interface CustomElements { +//// 'component-one': { +//// foo?: string; +//// }, +//// 'component-two': { +//// bar?: string; +//// } +////} +//// +////type Options = { kind: T } & Required<{ x: CustomElements[(T extends string ? T : never) & string] }['x']>; +//// +////declare function Component(props: Options): void; +//// +////const c = + +verify.completions({ + marker: "", + exact: [{ + name: "foo" + }] +}) From 56fdf948756d9cf5e1f0d5d47bde8fbdb4430f4c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 22 Jan 2020 17:10:39 -0800 Subject: [PATCH 3/4] Fix refactor typo --- src/services/completions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index bb1f4b4818fc0..e0413c2b802e3 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1279,7 +1279,7 @@ namespace ts.Completions { // Cursor is inside a JSX self-closing element or opening element const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes); if (!attrsType) return GlobalsSearch.Continue; - const uninstantiatedType = typeChecker.getContextualType(jsxContainer!, ContextFlags.Uninstantiated); + const uninstantiatedType = typeChecker.getContextualType(jsxContainer!.attributes, ContextFlags.Uninstantiated); let baseType; if (uninstantiatedType) { const signature = tryGetContextualTypeProvidingSignature(jsxContainer!, typeChecker)?.target; From 28cfb4c7e6ee4c19d56b1e179e600f79666a123b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 22 Jan 2020 17:48:00 -0800 Subject: [PATCH 4/4] Also look past simple mapped types --- src/services/completions.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index e0413c2b802e3..ef445a5a79d24 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1885,7 +1885,7 @@ namespace ts.Completions { break loop; } } - if (!isCallLikeExpression(node) && !isJsxOpeningLikeElement(node)) { + if (!isCallLikeExpression(node)) { return; } return checker.getResolvedSignature(node); @@ -1898,6 +1898,12 @@ namespace ts.Completions { if (type.flags & TypeFlags.IndexedAccess) { return typeIsTypeParameterFromSignature((type as IndexedAccessType).indexType, signature); } + if (getObjectFlags(type) & ObjectFlags.Mapped) { + const { constraintType } = (type as MappedType); + if (constraintType && constraintType.flags & TypeFlags.Index) { + return isIndexedAccessTypeWithTypeParameterIndex((constraintType as IndexType).type, signature); + } + } return false; }