diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs index 02db6d4f64e73..eda538f85757b 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs @@ -14333,4 +14333,43 @@ class Foo2 { } } """, commitChar: commitChar, sourceCodeKind: SourceCodeKind.Regular); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78766")] + public async Task ExtensionMethodAndPropertyWithSameName() + { + var markup = """ + using NS1; + + var x = new MyClass(); + x.$$ + + namespace NS1 + { + public class MyClass + { + public int MyMember { get; set; } + } + + public static class Extensions + { + public static MyClass MyMember(this MyClass c) + => c; + } + } + """; + + MarkupTestFile.GetPosition(markup, out var code, out int position); + + using var workspace = EditorTestWorkspace.CreateCSharp(code); + var document = workspace.CurrentSolution.Projects.First().Documents.First(); + + var service = GetCompletionService(document.Project); + var completionList = await GetCompletionListAsync(service, document, position, Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke); + + var myMemberItems = completionList.ItemsList.Where(item => item.DisplayText == "MyMember").ToList(); + + // We should have two separate completion items for MyMember: + // one for the property and one for the extension method + Assert.Equal(2, myMemberItems.Count); + } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs index 1a11cdd1da824..d630a984eeaef 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs @@ -158,9 +158,13 @@ private ImmutableArray CreateItems( // Use SymbolReferenceEquivalenceComparer.Instance as the value comparer as we // don't want symbols with just the same name to necessarily match // (as the default comparer on SymbolAndSelectionInfo does) - var symbolGroups = new MultiDictionary<(string displayText, string suffix, string insertionText), SymbolAndSelectionInfo>( + // + // Include whether the symbol is a method in the key to avoid merging methods with non-methods + // that have the same name (e.g., a property and an extension method with the same name). + // https://github.com/dotnet/roslyn/issues/78766 + var symbolGroups = new MultiDictionary<(string displayText, string suffix, string insertionText, bool isMethod), SymbolAndSelectionInfo>( capacity: symbols.Length, - comparer: EqualityComparer<(string, string, string)>.Default, + comparer: EqualityComparer<(string, string, string, bool)>.Default, valueComparer: SymbolReferenceEquivalenceComparer.Instance); foreach (var symbol in symbols) @@ -168,7 +172,7 @@ private ImmutableArray CreateItems( var texts = GetDisplayAndSuffixAndInsertionText(symbol.Symbol, contextLookup(symbol)); if (!string.IsNullOrWhiteSpace(texts.displayText)) { - symbolGroups.Add(texts, symbol); + symbolGroups.Add((texts.displayText, texts.suffix, texts.insertionText, symbol.Symbol.Kind == SymbolKind.Method), symbol); } }