Skip to content

Commit e078eb7

Browse files
authored
Don't add parenthesis when committing type with accessible nested type using dot (#80846)
fix #78515, #51629
1 parent 93eb9f2 commit e078eb7

File tree

3 files changed

+94
-5
lines changed

3 files changed

+94
-5
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13621,4 +13621,47 @@ private static string MakeMarkup(
1362113621

1362213622
public static IEnumerable<object[]> ValidEnumUnderlyingTypeNames()
1362313623
=> [["Byte"], ["SByte"], ["Int16"], ["UInt16"], ["Int32"], ["UInt32"], ["Int64"], ["UInt64"]];
13624+
13625+
[Theory, CombinatorialData]
13626+
public Task CompletionInObjectCreationContextForTypeWithNestedTypes(
13627+
[CombinatorialValues(".", ";")] char commitChar,
13628+
[CombinatorialValues("public", "private", "internal", "")] string modifier)
13629+
{
13630+
// always add "()" if committed via ";", or when nested types are inaccessible if via "."
13631+
var parens = commitChar is ';'
13632+
? "()"
13633+
: modifier is "private" or ""
13634+
? "()"
13635+
: "";
13636+
13637+
return VerifyProviderCommitAsync($$"""
13638+
class Program
13639+
{
13640+
void M()
13641+
{
13642+
var a = new Ba$$
13643+
}
13644+
}
13645+
13646+
class Bar
13647+
{
13648+
{{modifier}} class Foo1 { }
13649+
class Foo2 { }
13650+
}
13651+
""", "Bar", $$"""
13652+
class Program
13653+
{
13654+
void M()
13655+
{
13656+
var a = new Bar{{parens}}{{commitChar}}
13657+
}
13658+
}
13659+
13660+
class Bar
13661+
{
13662+
{{modifier}} class Foo1 { }
13663+
class Foo2 { }
13664+
}
13665+
""", commitChar: commitChar, sourceCodeKind: SourceCodeKind.Regular);
13666+
}
1362413667
}

src/Features/CSharp/Portable/Completion/CompletionProviders/SymbolCompletionProvider.cs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,25 +254,65 @@ protected override CompletionItem CreateItem(
254254
item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item);
255255
}
256256
}
257-
else if (symbol.IsKind(SymbolKind.NamedType) || symbol is IAliasSymbol aliasSymbol && aliasSymbol.Target.IsType)
257+
else if (context.IsObjectCreationTypeContext)
258258
{
259259
// If this is a type symbol/alias symbol, also consider appending parenthesis when later, it is committed by using special characters,
260260
// and the type is used as constructor
261-
if (context.IsObjectCreationTypeContext)
261+
var typeSymbol = symbol as INamedTypeSymbol ?? (symbol as IAliasSymbol)?.Target as INamedTypeSymbol;
262+
if (typeSymbol is not null)
263+
{
262264
item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item);
265+
266+
// When committing with `.`, we don't want to automatically appending parenthesis for types
267+
// that has accessible nested types (user might want to access nested types like Bar.Foo)
268+
if (HasAccessibleNestedTypes(typeSymbol))
269+
{
270+
item = SymbolCompletionItem.AddHasAccessibleNestedTypes(item);
271+
}
272+
}
263273
}
264274

265275
return item;
276+
277+
bool HasAccessibleNestedTypes(INamedTypeSymbol typeSymbol)
278+
{
279+
// This is intended to be a heuristic to cover common cases.
280+
// Because we might be passed a speculative member semantic model, we won't be able to get
281+
// symbols for containing type/member symbol to check accessibility using `IsAccessibleWithin`.
282+
// While this check is inaccurate in some cases (e.g. from within nested/derived types),
283+
// it's likely fine because people typically don't access the nested types via the outer one
284+
// when they are within the inner or derived type.
285+
foreach (var typeMember in typeSymbol.GetTypeMembers())
286+
{
287+
if (typeMember.DeclaredAccessibility <= Accessibility.Protected)
288+
continue;
289+
290+
if (typeMember.DeclaredAccessibility is Accessibility.Public)
291+
return true;
292+
293+
if (typeMember.IsAccessibleWithin(context.SemanticModel.Compilation.Assembly))
294+
return true;
295+
}
296+
297+
return false;
298+
}
266299
}
267300

268301
protected override string GetInsertionText(CompletionItem item, char ch)
269302
{
270-
if (ch is ';' or '.' && SymbolCompletionItem.GetShouldProvideParenthesisCompletion(item))
303+
if (SymbolCompletionItem.GetShouldProvideParenthesisCompletion(item))
271304
{
272-
CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(ch);
273-
return SymbolCompletionItem.GetInsertionText(item) + "()";
305+
// For '.', don't add parentheses if the type has accessible nested types
306+
// (user might want to access nested types like Bar.Foo)
307+
if (ch is ';' ||
308+
ch is '.' && !SymbolCompletionItem.GetHasAccessibleNestedTypes(item))
309+
{
310+
CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(ch);
311+
return SymbolCompletionItem.GetInsertionText(item) + "()";
312+
}
274313
}
275314

276315
return base.GetInsertionText(item, ch);
277316
}
278317
}
318+

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ public static bool GetShouldProvideParenthesisCompletion(CompletionItem item)
104104
return false;
105105
}
106106

107+
public static CompletionItem AddHasAccessibleNestedTypes(CompletionItem item)
108+
=> item.AddProperty("HasAccessibleNestedTypes", true.ToString());
109+
110+
public static bool GetHasAccessibleNestedTypes(CompletionItem item)
111+
=> item.TryGetProperty("HasAccessibleNestedTypes", out _);
112+
107113
public static string EncodeSymbols(ImmutableArray<ISymbol> symbols)
108114
{
109115
if (symbols.Length > 1)

0 commit comments

Comments
 (0)