Skip to content

Commit efa4d2c

Browse files
authored
Support target type completion tags in object creation contexts (#76786)
* Support target type completion tags in object creation contexts This allows "new |" completion to populate the target type filter with types that derive from the specified target type. Fixes #45109
1 parent 07fab35 commit efa4d2c

File tree

6 files changed

+201
-11
lines changed

6 files changed

+201
-11
lines changed

src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

55
using System;
66
using System.Collections.Generic;
77
using System.Diagnostics.CodeAnalysis;
8+
using System.Linq;
89
using System.Threading.Tasks;
910
using Microsoft.CodeAnalysis.Completion.Providers;
1011
using Microsoft.CodeAnalysis.CSharp;
@@ -12789,6 +12790,64 @@ await VerifyItemExistsAsync(
1278912790
matchingFilters: [FilterSet.MethodFilter, FilterSet.TargetTypedFilter]);
1279012791
}
1279112792

12793+
[InlineData("IGoo", new string[] { "Goo", "GooDerived", "GooGeneric" })]
12794+
[InlineData("IGoo[]", new string[] { "IGoo", "IGooGeneric", "Goo", "GooAbstract", "GooDerived", "GooGeneric" })]
12795+
[InlineData("IGooGeneric<int>", new string[] { "GooGeneric" })]
12796+
[InlineData("IGooGeneric<int>[]", new string[] { "IGooGeneric", "GooGeneric" })]
12797+
[InlineData("IOther", new string[] { })]
12798+
[InlineData("Goo", new string[] { "Goo" })]
12799+
[InlineData("GooAbstract", new string[] { "GooDerived" })]
12800+
[InlineData("GooDerived", new string[] { "GooDerived" })]
12801+
[InlineData("GooGeneric<int>", new string[] { "GooGeneric" })]
12802+
[InlineData("object", new string[] { "C", "Goo", "GooDerived", "GooGeneric" })]
12803+
[Theory, Trait(Traits.Feature, Traits.Features.TargetTypedCompletion)]
12804+
public async Task TestTargetTypeCompletionInCreationContext(string targetType, string[] expectedItems)
12805+
{
12806+
ShowTargetTypedCompletionFilter = true;
12807+
12808+
var markup =
12809+
$$"""
12810+
interface IGoo { }
12811+
interface IGooGeneric<T> : IGoo { }
12812+
interface IOther { }
12813+
class Goo : IGoo { }
12814+
abstract class GooAbstract : IGoo { }
12815+
class GooDerived : GooAbstract { }
12816+
class GooGeneric<T> : IGooGeneric<T> { }
12817+
12818+
class C
12819+
{
12820+
void M1({{targetType}} arg) { }
12821+
12822+
void M2()
12823+
=> M1(new $$);
12824+
}
12825+
""";
12826+
12827+
(string Name, bool IsClass, string? DisplaySuffix)[] types = [
12828+
("IGoo", false, null),
12829+
("IGooGeneric", false, "<>"),
12830+
("IOther", false, null),
12831+
("Goo", true, null),
12832+
("GooAbstract", true, null),
12833+
("GooDerived", true, null),
12834+
("GooGeneric", true, "<>"),
12835+
("C", true, null)
12836+
];
12837+
12838+
foreach (var item in types.Where(t => t.IsClass && expectedItems.Contains(t.Name)))
12839+
await VerifyItemExistsAsync(markup, item.Name, matchingFilters: [FilterSet.ClassFilter, FilterSet.TargetTypedFilter], displayTextSuffix: item.DisplaySuffix);
12840+
12841+
foreach (var item in types.Where(t => t.IsClass && !expectedItems.Contains(t.Name)))
12842+
await VerifyItemExistsAsync(markup, item.Name, matchingFilters: [FilterSet.ClassFilter], displayTextSuffix: item.DisplaySuffix);
12843+
12844+
foreach (var item in types.Where(t => !t.IsClass && expectedItems.Contains(t.Name)))
12845+
await VerifyItemExistsAsync(markup, item.Name, matchingFilters: [FilterSet.InterfaceFilter, FilterSet.TargetTypedFilter], displayTextSuffix: item.DisplaySuffix);
12846+
12847+
foreach (var item in types.Where(t => !t.IsClass && !expectedItems.Contains(t.Name)))
12848+
await VerifyItemExistsAsync(markup, item.Name, matchingFilters: [FilterSet.InterfaceFilter], displayTextSuffix: item.DisplaySuffix);
12849+
}
12850+
1279212851
[Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)]
1279312852
public async Task TestTypesNotSuggestedInDeclarationDeconstruction()
1279412853
{

src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/SymbolCompletionProviderTests.vb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8305,6 +8305,82 @@ End Class"
83058305
matchingFilters:=New List(Of CompletionFilter) From {FilterSet.MethodFilter})
83068306
End Function
83078307

8308+
<InlineData("IGoo", New String() {"Goo", "GooDerived", "GooGeneric"})>
8309+
<InlineData("IGoo()", New String() {"IGoo", "IGooGeneric", "Goo", "GooAbstract", "GooDerived", "GooGeneric"})>
8310+
<InlineData("IGooGeneric(Of Integer)", New String() {"GooGeneric"})>
8311+
<InlineData("IGooGeneric(Of Integer)()", New String() {"IGooGeneric", "GooGeneric"})>
8312+
<InlineData("IOther", New String() {})>
8313+
<InlineData("Goo", New String() {"Goo"})>
8314+
<InlineData("GooAbstract", New String() {"GooDerived"})>
8315+
<InlineData("GooDerived", New String() {"GooDerived"})>
8316+
<InlineData("GooGeneric(Of Integer)", New String() {"GooGeneric"})>
8317+
<InlineData("object", New String() {"C", "Goo", "GooDerived", "GooGeneric"})>
8318+
<Theory, Trait(Traits.Feature, Traits.Features.TargetTypedCompletion)>
8319+
Public Async Function TestTargetTypeFilter_InCreationContext(targetType As String, expectedItems As String()) As Task
8320+
ShowTargetTypedCompletionFilter = True
8321+
Dim markup =
8322+
$"Interface IGoo
8323+
End Interface
8324+
8325+
Interface IGooGeneric(Of T)
8326+
Inherits IGoo
8327+
End Interface
8328+
8329+
Interface IOther
8330+
End Interface
8331+
8332+
Class Goo
8333+
Implements IGoo
8334+
End Class
8335+
8336+
MustInherit Class GooAbstract
8337+
Implements IGoo
8338+
End Class
8339+
8340+
Class GooDerived
8341+
Inherits GooAbstract
8342+
End Class
8343+
8344+
Class GooGeneric(Of T)
8345+
Implements IGooGeneric(Of T)
8346+
End Class
8347+
8348+
Class C
8349+
Sub M1(arg As {targetType})
8350+
End Sub
8351+
8352+
Sub M2()
8353+
M1(New $$)
8354+
End Sub
8355+
End Class"
8356+
Dim types As New List(Of (Name As String, IsClass As Boolean, DisplaySuffix As String)) From {
8357+
("IGoo", False, Nothing),
8358+
("IGooGeneric", False, "(Of …)"),
8359+
("IOther", False, Nothing),
8360+
("Goo", True, Nothing),
8361+
("GooAbstract", True, Nothing),
8362+
("GooDerived", True, Nothing),
8363+
("GooGeneric", True, "(Of …)"),
8364+
("C", True, Nothing)
8365+
}
8366+
8367+
For Each item In types.Where(Function(t) t.IsClass AndAlso expectedItems.Contains(t.Name))
8368+
Await VerifyItemExistsAsync(markup, item.Name, matchingFilters:=New List(Of CompletionFilter) From {FilterSet.ClassFilter, FilterSet.TargetTypedFilter}, displayTextSuffix:=item.DisplaySuffix)
8369+
Next
8370+
8371+
For Each item In types.Where(Function(t) t.IsClass AndAlso Not expectedItems.Contains(t.Name))
8372+
Await VerifyItemExistsAsync(markup, item.Name, matchingFilters:=New List(Of CompletionFilter) From {FilterSet.ClassFilter}, displayTextSuffix:=item.DisplaySuffix)
8373+
Next
8374+
8375+
For Each item In types.Where(Function(t) Not t.IsClass AndAlso expectedItems.Contains(t.Name))
8376+
Await VerifyItemExistsAsync(markup, item.Name, matchingFilters:=New List(Of CompletionFilter) From {FilterSet.InterfaceFilter, FilterSet.TargetTypedFilter}, displayTextSuffix:=item.DisplaySuffix)
8377+
Next
8378+
8379+
For Each item In types.Where(Function(t) Not t.IsClass AndAlso Not expectedItems.Contains(t.Name))
8380+
Await VerifyItemExistsAsync(markup, item.Name, matchingFilters:=New List(Of CompletionFilter) From {FilterSet.InterfaceFilter}, displayTextSuffix:=item.DisplaySuffix)
8381+
Next
8382+
End Function
8383+
83088384
<Theory, MemberData(NameOf(ValidEnumUnderlyingTypeNames))>
83098385
Public Async Function TestEnumBaseList1(underlyingType As String) As Task
83108386
Dim markup = "Enum MyEnum As $$"

src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,13 @@ protected abstract CompletionItem CreateItem(
4747
/// because we ignore nullability.</param>
4848
private static bool ShouldIncludeInTargetTypedCompletionList(
4949
ISymbol symbol,
50-
ImmutableArray<ITypeSymbol> inferredTypes,
51-
SemanticModel semanticModel,
52-
int position,
50+
TSyntaxContext syntaxContext,
5351
Dictionary<ITypeSymbol, bool> typeConvertibilityCache)
5452
{
55-
// When searching for identifiers of type C, exclude the symbol for the `C` type itself.
53+
// When searching for identifiers of type C, exclude the symbol for the `C` type itself except in an object creation context.
5654
if (symbol.Kind == SymbolKind.NamedType)
5755
{
58-
return false;
56+
return ShouldIncludeInTargetTypedCompletionListForNamedType((INamedTypeSymbol)symbol, syntaxContext, typeConvertibilityCache);
5957
}
6058

6159
// Avoid offering members of object since they too commonly show up and are infrequently desired.
@@ -69,7 +67,7 @@ private static bool ShouldIncludeInTargetTypedCompletionList(
6967
{
7068
var local = (ILocalSymbol)symbol;
7169
var declarationSyntax = symbol.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).SingleOrDefault();
72-
if (declarationSyntax != null && position < declarationSyntax.FullSpan.End)
70+
if (declarationSyntax != null && syntaxContext.Position < declarationSyntax.FullSpan.End)
7371
{
7472
return false;
7573
}
@@ -86,10 +84,62 @@ private static bool ShouldIncludeInTargetTypedCompletionList(
8684
return isConvertible;
8785
}
8886

89-
typeConvertibilityCache[type] = CompletionUtilities.IsTypeImplicitlyConvertible(semanticModel.Compilation, type, inferredTypes);
87+
typeConvertibilityCache[type] = CompletionUtilities.IsTypeImplicitlyConvertible(syntaxContext.SemanticModel.Compilation, type, syntaxContext.InferredTypes);
9088
return typeConvertibilityCache[type];
9189
}
9290

91+
private static bool ShouldIncludeInTargetTypedCompletionListForNamedType(INamedTypeSymbol symbol, TSyntaxContext syntaxContext, Dictionary<ITypeSymbol, bool> typeConvertibilityCache)
92+
{
93+
// Only create target typed completion entries in the object creation context
94+
if (!syntaxContext.IsObjectCreationTypeContext)
95+
return false;
96+
97+
if (!typeConvertibilityCache.TryGetValue(symbol, out var isConvertible))
98+
{
99+
isConvertible = IsConvertible(symbol, syntaxContext);
100+
101+
typeConvertibilityCache[symbol] = isConvertible;
102+
}
103+
104+
return isConvertible;
105+
106+
static bool IsConvertible(INamedTypeSymbol symbol, TSyntaxContext syntaxContext)
107+
{
108+
for (var i = 0; i < syntaxContext.InferredTypes.Length; i++)
109+
{
110+
var inferredType = syntaxContext.InferredTypes[i];
111+
if (inferredType.IsArrayType())
112+
{
113+
while (inferredType is IArrayTypeSymbol arrayType)
114+
inferredType = arrayType.ElementType;
115+
}
116+
else
117+
{
118+
// Abstract types should not be offered in target typed completion except in array contexts
119+
if (symbol.IsAbstract)
120+
continue;
121+
}
122+
123+
if (inferredType.IsInterfaceType())
124+
{
125+
if (Equals(symbol, inferredType.OriginalDefinition) || symbol.AllInterfaces.Any(static (typeInterface, inferredType) => Equals(typeInterface.OriginalDefinition, inferredType.OriginalDefinition), inferredType))
126+
return true;
127+
}
128+
else
129+
{
130+
var typeToCheck = symbol;
131+
while (typeToCheck != null && !Equals(typeToCheck.OriginalDefinition, inferredType.OriginalDefinition))
132+
typeToCheck = typeToCheck.BaseType;
133+
134+
if (typeToCheck != null)
135+
return true;
136+
}
137+
}
138+
139+
return false;
140+
}
141+
}
142+
93143
/// <summary>
94144
/// Given a list of symbols, and a mapping from each symbol to its original SemanticModel,
95145
/// creates the list of completion items for them.
@@ -187,7 +237,7 @@ protected static bool TryFindFirstSymbolMatchesTargetTypes(
187237
{
188238
var symbol = symbolList[index];
189239
var syntaxContext = contextLookup(symbol);
190-
if (ShouldIncludeInTargetTypedCompletionList(symbol.Symbol, syntaxContext.InferredTypes, syntaxContext.SemanticModel, syntaxContext.Position, typeConvertibilityCache))
240+
if (ShouldIncludeInTargetTypedCompletionList(symbol.Symbol, syntaxContext, typeConvertibilityCache))
191241
break;
192242
}
193243

src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/CSharpSyntaxContext.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ internal sealed class CSharpSyntaxContext : SyntaxContext
3636
public readonly bool IsLocalFunctionDeclarationContext;
3737
public readonly bool IsLocalVariableDeclarationContext;
3838
public readonly bool IsNonAttributeExpressionContext;
39-
public readonly bool IsObjectCreationTypeContext;
4039
public readonly bool IsParameterTypeContext;
4140
public readonly bool IsPossibleLambdaOrAnonymousMethodParameterTypeContext;
4241
public readonly bool IsPreProcessorKeywordContext;
@@ -130,6 +129,7 @@ private CSharpSyntaxContext(
130129
isNameOfContext: isNameOfContext,
131130
isNamespaceContext: isNamespaceContext,
132131
isNamespaceDeclarationNameContext: isNamespaceDeclarationNameContext,
132+
isObjectCreationTypeContext: isObjectCreationTypeContext,
133133
isOnArgumentListBracketOrComma: isOnArgumentListBracketOrComma,
134134
isPossibleTupleContext: isPossibleTupleContext,
135135
isPreProcessorDirectiveContext: isPreProcessorDirectiveContext,
@@ -164,7 +164,6 @@ private CSharpSyntaxContext(
164164
this.IsLocalFunctionDeclarationContext = isLocalFunctionDeclarationContext;
165165
this.IsLocalVariableDeclarationContext = isLocalVariableDeclarationContext;
166166
this.IsNonAttributeExpressionContext = isNonAttributeExpressionContext;
167-
this.IsObjectCreationTypeContext = isObjectCreationTypeContext;
168167
this.IsParameterTypeContext = isParameterTypeContext;
169168
this.IsPossibleLambdaOrAnonymousMethodParameterTypeContext = isPossibleLambdaOrAnonymousMethodParameterTypeContext;
170169
this.IsPreProcessorKeywordContext = isPreProcessorKeywordContext;

src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ContextQuery/SyntaxContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ internal abstract class SyntaxContext
4242
public bool IsNameOfContext { get; }
4343
public bool IsNamespaceContext { get; }
4444
public bool IsNamespaceDeclarationNameContext { get; }
45+
public bool IsObjectCreationTypeContext { get; }
4546
public bool IsOnArgumentListBracketOrComma { get; }
4647
public bool IsPossibleTupleContext { get; }
4748
public bool IsPreProcessorDirectiveContext { get; }
@@ -76,6 +77,7 @@ protected SyntaxContext(
7677
bool isNameOfContext,
7778
bool isNamespaceContext,
7879
bool isNamespaceDeclarationNameContext,
80+
bool isObjectCreationTypeContext,
7981
bool isOnArgumentListBracketOrComma,
8082
bool isPossibleTupleContext,
8183
bool isPreProcessorDirectiveContext,
@@ -110,6 +112,7 @@ protected SyntaxContext(
110112
this.IsNameOfContext = isNameOfContext;
111113
this.IsNamespaceContext = isNamespaceContext;
112114
this.IsNamespaceDeclarationNameContext = isNamespaceDeclarationNameContext;
115+
this.IsObjectCreationTypeContext = isObjectCreationTypeContext;
113116
this.IsOnArgumentListBracketOrComma = isOnArgumentListBracketOrComma;
114117
this.IsPossibleTupleContext = isPossibleTupleContext;
115118
this.IsPreProcessorDirectiveContext = isPreProcessorDirectiveContext;

src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ContextQuery/VisualBasicSyntaxContext.vb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions.ContextQuery
7070
isNameOfContext As Boolean,
7171
isNamespaceContext As Boolean,
7272
isNamespaceDeclarationNameContext As Boolean,
73+
isObjectCreationTypeContext As Boolean,
7374
isPossibleTupleContext As Boolean,
7475
isPreProcessorDirectiveContext As Boolean,
7576
isPreProcessorExpressionContext As Boolean,
@@ -102,6 +103,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions.ContextQuery
102103
isNameOfContext:=isNameOfContext,
103104
isNamespaceContext,
104105
isNamespaceDeclarationNameContext,
106+
isObjectCreationTypeContext:=isObjectCreationTypeContext,
105107
isOnArgumentListBracketOrComma:=isOnArgumentListBracketOrComma,
106108
isPossibleTupleContext:=isPossibleTupleContext,
107109
isPreProcessorDirectiveContext:=isPreProcessorDirectiveContext,
@@ -185,6 +187,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions.ContextQuery
185187
isNameOfContext:=syntaxTree.IsNameOfContext(position, cancellationToken),
186188
isNamespaceContext:=syntaxTree.IsNamespaceContext(position, targetToken, cancellationToken, semanticModel),
187189
isNamespaceDeclarationNameContext:=syntaxTree.IsNamespaceDeclarationNameContext(position, cancellationToken),
190+
isObjectCreationTypeContext:=syntaxTree.IsObjectCreationTypeContext(position, targetToken, cancellationToken),
188191
isOnArgumentListBracketOrComma:=targetToken.Parent.IsKind(SyntaxKind.ArgumentList),
189192
isPossibleTupleContext:=syntaxTree.IsPossibleTupleContext(targetToken, position),
190193
isPreProcessorDirectiveContext:=syntaxTree.IsInPreprocessorDirectiveContext(position, cancellationToken),

0 commit comments

Comments
 (0)