Skip to content
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

Avoid adding the CompletionItem when the alias type exists #49488

Merged
merged 14 commits into from
Feb 8, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,135 @@ class C
await VerifyTypeImportItemExistsAsync(markup, "Bar", displayTextSuffix: "<>", glyph: (int)Glyph.ClassPublic, inlineDescription: "Baz");
}

[InlineData(true)]
[InlineData(false)]
[Theory, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task TestNoCompletionItemWhenThereIsAlias(bool isProjectReference)
{
var file1 = @"
using AliasFoo1 = Foo1.Foo2.Foo3.Foo4;
using AliasFoo2 = Foo1.Foo2.Foo3.Foo4.Foo6;

namespace Bar
{
using AliasFoo3 = Foo1.Foo2.Foo3.Foo5;
using AliasFoo4 = Foo1.Foo2.Foo3.Foo5.Foo7;
public class CC
{
public static void Main()
{
F$$
}
}
}";
var file2 = @"
namespace Foo1
{
namespace Foo2
{
namespace Foo3
{
public class Foo4
{
public class Foo6
{
}
}

public class Foo5
{
public class Foo7
{
}
}
}
}
}";

var markup = GetMarkupWithReference(file1, file2, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference);
await VerifyTypeImportItemIsAbsentAsync(markup, "Foo4", "Foo1.Foo2.Foo3");
await VerifyTypeImportItemIsAbsentAsync(markup, "Foo6", "Foo1.Foo2.Foo3");
await VerifyTypeImportItemIsAbsentAsync(markup, "Foo5", "Foo1.Foo2.Foo3");
await VerifyTypeImportItemIsAbsentAsync(markup, "Foo7", "Foo1.Foo2.Foo3");
}

[InlineData(true)]
[InlineData(false)]
[Theory, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task TestAttributesAlias(bool isProjectReference)
{
var file1 = @"
using myAlias = Foo.BarAttribute;
using myAlia2 = Foo.BarAttributeDifferentEnding;

namespace Foo2
{
public class Main
{
$$
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to test attribute items, the trigger needs to be in attribute context. Otherwise, it's just behaves as regular types

}
}";

var file2 = @"
namespace Foo
{
public class BarAttribute: System.Attribute
{
}

public class BarAttributeDifferentEnding: System.Attribute
{
}
}";

var markup = GetMarkupWithReference(file1, file2, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference);
await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", "Foo");
await VerifyTypeImportItemIsAbsentAsync(markup, "BarAttribute", "Foo");
await VerifyTypeImportItemIsAbsentAsync(markup, "BarAttributeDifferentEnding", "Foo");
}

[InlineData(true)]
[InlineData(false)]
[Theory, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task TestGenericsAliasHasNoEffect(bool isProjectReference)
{
var file1 = @"
using AliasFoo1 = Foo1.Foo2.Foo3.Foo4<int>;

namespace Bar
{
using AliasFoo2 = Foo1.Foo2.Foo3.Foo5<string>;
public class CC
{
public static void Main()
{
F$$
}
}
}";
var file2 = @"
namespace Foo1
{
namespace Foo2
{
namespace Foo3
{
public class Foo4<T>
{
}

public class Foo5<U>
{
}
}
}
}";

var markup = GetMarkupWithReference(file1, file2, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference);
await VerifyTypeImportItemExistsAsync(markup, "Foo4", (int)Glyph.ClassPublic, "Foo1.Foo2.Foo3", displayTextSuffix: "<>");
await VerifyTypeImportItemExistsAsync(markup, "Foo5", (int)Glyph.ClassPublic, "Foo1.Foo2.Foo3", displayTextSuffix: "<>");
}

#endregion

#region "Commit Change Tests"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,62 @@ End Namespace</Text>.Value
Dim markup = CreateMarkupForSingleProject(file2, file1, LanguageNames.VisualBasic)
Await VerifyCustomCommitProviderAsync(markup, "Bar", expectedCodeAfterCommit, sourceCodeKind:=kind)
End Function

<Fact, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function TestNoCompletionItemWhenAliasExists() As Task
Dim file1 = "
Imports FFF = Foo1.Foo2.Foo3.Foo4
Imports FFF1 = Foo1.Foo2.Foo3.Foo4.Foo5

Namespace Bar
Public Class Bar1
Private Sub EE()
F$$
End Sub
End Class
End Namespace"

Dim file2 = "
Namespace Foo1
Namespace Foo2
Namespace Foo3
Public Class Foo4
Public Class Foo5
End Class
End Class
End Namespace
End Namespace
End Namespace
"
Dim markup = CreateMarkupForSingleProject(file1, file2, LanguageNames.VisualBasic)
Await VerifyItemIsAbsentAsync(markup, "Foo4", inlineDescription:="Foo1.Foo2.Foo3")
Await VerifyItemIsAbsentAsync(markup, "Foo5", inlineDescription:="Foo1.Foo2.Foo3")
End Function

<Fact, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function TestAliasHasNoEffectOnGenerics() As Task
Dim file1 = "
Imports FFF = Foo1.Foo2.Foo3.Foo4(Of Int)
Namespace Bar
Public Class Bar1
Private Sub EE()
F$$
End Sub
End Class
End Namespace"

Dim file2 = "
Namespace Foo1
Namespace Foo2
Namespace Foo3
Public Class Foo4(Of T)
End Class
End Namespace
End Namespace
End Namespace"

Dim markup = CreateMarkupForSingleProject(file1, file2, LanguageNames.VisualBasic)
Await VerifyItemExistsAsync(markup, "Foo4", glyph:=Glyph.ClassPublic, inlineDescription:="Foo1.Foo2.Foo3", displayTextSuffix:="(Of ...)")
End Function
End Class
End Namespace
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,30 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers
{
[ExportCompletionProvider(nameof(TypeImportCompletionProvider), LanguageNames.CSharp)]
[ExtensionOrder(After = nameof(PropertySubpatternCompletionProvider))]
[Shared]
internal sealed class TypeImportCompletionProvider : AbstractTypeImportCompletionProvider
internal sealed class TypeImportCompletionProvider : AbstractTypeImportCompletionProvider<UsingDirectiveSyntax>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
Expand Down Expand Up @@ -74,5 +78,10 @@ protected override async Task<bool> ShouldProvideParenthesisCompletionAsync(

return false;
}

protected override ImmutableArray<UsingDirectiveSyntax> GetAliasDeclarationNodes(SyntaxNode node)
=> node.GetEnclosingUsingDirectives()
.Where(n => n.Alias != null)
.ToImmutableArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,24 @@
using Microsoft.CodeAnalysis.Completion.Log;
using Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Completion.Providers
{
internal abstract class AbstractTypeImportCompletionProvider : AbstractImportCompletionProvider
internal abstract class AbstractTypeImportCompletionProvider<AliasDeclarationTypeNode> : AbstractImportCompletionProvider
where AliasDeclarationTypeNode : SyntaxNode
{
protected override bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext)
=> syntaxContext.IsTypeContext;

protected override void LogCommit()
=> CompletionProvidersLogger.LogCommitOfTypeImportCompletionItem();

protected abstract ImmutableArray<AliasDeclarationTypeNode> GetAliasDeclarationNodes(SyntaxNode node);

protected override async Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet<string> namespacesInScope, bool isExpandedCompletion, CancellationToken cancellationToken)
{
using (Logger.LogBlock(FunctionId.Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, cancellationToken))
Expand All @@ -42,32 +47,118 @@ protected override async Task AddCompletionItemsAsync(CompletionContext completi
}
else
{
var aliasTargetNamespaceToTypeNameMap = GetAliasTypeDictionary(completionContext.Document, syntaxContext, cancellationToken);
foreach (var items in itemsFromAllAssemblies)
{
AddItems(items, completionContext, namespacesInScope, telemetryCounter);
AddItems(items, completionContext, namespacesInScope, aliasTargetNamespaceToTypeNameMap, telemetryCounter);
}
}

telemetryCounter.Report();
}
}

private static void AddItems(ImmutableArray<CompletionItem> items, CompletionContext completionContext, HashSet<string> namespacesInScope, TelemetryCounter counter)
/// <summary>
/// Get a multi-Dictionary stores the information about the target of all alias Symbol in the syntax tree.
/// Multiple aliases might live under same namespace.
/// Key is the namespace of the symbol, value is the name of the symbol.
/// </summary>
private MultiDictionary<string, string> GetAliasTypeDictionary(
Document document,
SyntaxContext syntaxContext,
CancellationToken cancellationToken)
{
var syntaxFactsService = document.GetRequiredLanguageService<ISyntaxFactsService>();
var dictionary = new MultiDictionary<string, string>(syntaxFactsService.StringComparer);

var nodeToCheck = syntaxContext.LeftToken.Parent;
if (nodeToCheck == null)
{
return dictionary;
}

// In case the caret is at the beginning of the file, take the root node.
var aliasDeclarations = GetAliasDeclarationNodes(nodeToCheck);
foreach (var aliasNode in aliasDeclarations)
{
var symbol = syntaxContext.SemanticModel.GetDeclaredSymbol(aliasNode, cancellationToken);
if (symbol is IAliasSymbol { Target: ITypeSymbol { TypeKind: not TypeKind.Error } target })
{
// If the target type is a type constructs from generics type, e.g.
// using AliasBar = Bar<int>
// namespace Foo
// {
// public class Bar<T>
// {
// }
// }
// namespace Foo2
// {
// public class Main
// {
// $$
// }
// }
// In such case, user might want to type Bar<string> and still want 'using Foo'.
// We shouldn't try to filter the CompletionItem for Bar<T> later.
// so just ignore the Bar<int> here.
var typeParameter = target.GetTypeParameters();
if (typeParameter.IsEmpty)
{
var namespaceOfTarget = target.ContainingNamespace.ToDisplayString(SymbolDisplayFormats.NameFormat);
var typeNameOfTarget = target.Name;
dictionary.Add(namespaceOfTarget, typeNameOfTarget);
}
}
}

return dictionary;
}

private static void AddItems(
ImmutableArray<CompletionItem> items,
CompletionContext completionContext,
HashSet<string> namespacesInScope,
MultiDictionary<string, string> aliasTargetNamespaceToTypeNameMap,
TelemetryCounter counter)
{
counter.ReferenceCount++;
foreach (var item in items)
{
var containingNamespace = ImportCompletionItem.GetContainingNamespace(item);
if (!namespacesInScope.Contains(containingNamespace))
if (ShouldAddItem(item, namespacesInScope, aliasTargetNamespaceToTypeNameMap))
{
// We can return cached item directly, item's span will be fixed by completion service.
// On the other hand, because of this (i.e. mutating the span of cached item for each run),
// the provider can not be used as a service by components that might be run in parallel
// the provider can not be used as a service by components that might be run in parallel
// with completion, which would be a race.
completionContext.AddItem(item);
counter.ItemsCount++;
}
}

static bool ShouldAddItem(
CompletionItem item,
HashSet<string> namespacesInScope,
MultiDictionary<string, string> aliasTargetNamespaceToTypeNameMap)
{
var containingNamespace = ImportCompletionItem.GetContainingNamespace(item);
// 1. if the namespace of the item is in scoop. Don't add the item
if (namespacesInScope.Contains(containingNamespace))
{
return false;
}

// 2. If the item might be an alias target. First check if the target alias map has any value then
// check if the type name is in the dictionary.
// It is done in this way to avoid calling ImportCompletionItem.GetTypeName for all the CompletionItems
if (!aliasTargetNamespaceToTypeNameMap.IsEmpty
&& aliasTargetNamespaceToTypeNameMap[containingNamespace].Contains(ImportCompletionItem.GetTypeName(item)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I guess we really don't care if it's a generic type, since no generic target will be in the dictionary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need the aliasTargetNamespaceToTypeNameMap.IsEmpty check?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it cheaper than indexing into the dictionary. We are dealing with large amount of item and it's very rare there's using alias in the document

{
return false;
}

return true;
}
}

private class TelemetryCounter
Expand Down
Loading