Skip to content

Don’t mix in base constraint completions on indexed access types with type parameter index types #36364

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
85 changes: 81 additions & 4 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!.attributes, 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;
Expand Down Expand Up @@ -1800,9 +1807,17 @@ 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) {
const signature = tryGetContextualTypeProvidingSignature(objectLikeContainer, typeChecker)?.target;
if (signature && !isIndexedAccessTypeWithTypeParameterIndex(uninstantiatedType, signature)) {
baseType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint);
}
}

isNewIdentifierLocation = hasIndexSignature(instantiatedType);
typeMembers = getPropertiesForObjectExpression(instantiatedType, baseType, objectLikeContainer, typeChecker);
existingMembers = objectLikeContainer.properties;
}
Expand Down Expand Up @@ -1852,6 +1867,68 @@ 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)) {
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);
}
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;
}

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
Expand Down
35 changes: 35 additions & 0 deletions tests/cases/fourslash/completionsGenericIndexedAccess3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/// <reference path="fourslash.ts" />

////interface CustomElements {
//// 'component-one': {
//// foo?: string;
//// },
//// 'component-two': {
//// bar?: string;
//// }
////}
////
////interface Options<T extends keyof CustomElements> {
//// props: CustomElements[T];
////}
////
////declare function create<T extends keyof CustomElements>(name: T, options: Options<T>): 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
}]
});
45 changes: 45 additions & 0 deletions tests/cases/fourslash/completionsGenericIndexedAccess4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/// <reference path="fourslash.ts" />

////interface CustomElements {
//// 'component-one': {
//// foo?: string;
//// },
//// 'component-two': {
//// bar?: string;
//// }
////}
////
////interface Options<T extends keyof CustomElements> {
//// props: CustomElements[T];
////}
////
////declare function create<T extends 'hello' | 'goodbye'>(name: T, options: Options<T extends 'hello' ? 'component-one' : 'component-two'>): void;
////declare function create<T extends keyof CustomElements>(name: T, options: Options<T>): 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
}]
});
28 changes: 28 additions & 0 deletions tests/cases/fourslash/completionsGenericIndexedAccess5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
////interface CustomElements {
//// 'component-one': {
//// foo?: string;
//// },
//// 'component-two': {
//// bar?: string;
//// }
////}
////
////interface Options<T extends keyof CustomElements> {
//// props?: {} & { x: CustomElements[(T extends string ? T : never) & string][] }['x'];
////}
////
////declare function f<T extends keyof CustomElements>(k: T, options: Options<T>): void;
////
////f("component-one", {
//// props: [{
//// /**/
//// }]
////})

verify.completions({
marker: "",
exact: [{
name: "foo",
sortText: completion.SortText.OptionalMember
}]
});
23 changes: 23 additions & 0 deletions tests/cases/fourslash/completionsGenericIndexedAccess6.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @Filename: component.tsx

////interface CustomElements {
//// 'component-one': {
//// foo?: string;
//// },
//// 'component-two': {
//// bar?: string;
//// }
////}
////
////type Options<T extends keyof CustomElements> = { kind: T } & Required<{ x: CustomElements[(T extends string ? T : never) & string] }['x']>;
////
////declare function Component<T extends keyof CustomElements>(props: Options<T>): void;
////
////const c = <Component /**/ kind="component-one" />

verify.completions({
marker: "",
exact: [{
name: "foo"
}]
})