diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/ExtensionMethodImportCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/ExtensionMethodImportCompletionProviderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..002d91442e7c9d2c5682b3c2951e23d2a9af1b66 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/ExtensionMethodImportCompletionProviderTests.cs @@ -0,0 +1,1234 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.CSharp.Completion.Providers; +using Microsoft.CodeAnalysis.Editor.UnitTests; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.Composition; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders +{ + [UseExportProvider] + public class ExtensionMethodImportCompletionProviderTests : AbstractCSharpCompletionProviderTests + { + public ExtensionMethodImportCompletionProviderTests(CSharpTestWorkspaceFixture workspaceFixture) : base(workspaceFixture) + { + } + + private bool? ShowImportCompletionItemsOptionValue { get; set; } = true; + + // -1 would disable timebox, whereas 0 means always timeout. + private int TimeoutInMilliseconds { get; set; } = -1; + + private bool IsExpandedCompletion { get; set; } = true; + + protected override void SetWorkspaceOptions(TestWorkspace workspace) + { + workspace.Options = workspace.Options + .WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, ShowImportCompletionItemsOptionValue) + .WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForImportCompletion, TimeoutInMilliseconds) + .WithChangedOption(CompletionServiceOptions.IsExpandedCompletion, IsExpandedCompletion); + } + + protected override ExportProvider GetExportProvider() + { + return ExportProviderCache + .GetOrCreateExportProviderFactory(TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic.WithPart(typeof(TestExperimentationService))) + .CreateExportProvider(); + } + + internal override CompletionProvider CreateCompletionProvider() + { + return new ExtensionMethodImportCompletionProvider(); + } + + public enum ReferenceType + { + None, + Project, + Metadata + } + + private static IEnumerable CombineWithReferenceTypeData(IEnumerable> data) + { + foreach (var refKind in Enum.GetValues(typeof(ReferenceType))) + { + foreach (var d in data) + { + d.Add(refKind); + yield return d.ToArray(); + } + } + } + + public static IEnumerable ReferenceTypeData + => (new[] { ReferenceType.None, ReferenceType.Project, ReferenceType.Metadata }).Select(refType => new[] { (object)refType }); + + public static IEnumerable AllTypeKindsWithReferenceTypeData + => CombineWithReferenceTypeData((new[] { "class", "struct", "interface", "enum", "abstract class" }).Select(kind => new List() { kind })); + + private static IEnumerable> BuiltInTypes + { + get + { + var predefinedTypes = new List> + { + new List() + { "int", "Int32", "System.Int32" }, + new List() + { "float", "Single", "System.Single" }, + new List() + { "uint", "UInt32", "System.UInt32" }, + new List() + { "bool", "Boolean", "System.Boolean"}, + new List() + { "string", "String", "System.String"}, + new List() + { "object", "Object", "System.Object"}, + }; + + var arraySuffixes = new[] { "", "[]", "[,]" }; + + foreach (var group in predefinedTypes) + { + foreach (var type1 in group) + { + foreach (var type2 in group) + { + foreach (var suffix in arraySuffixes) + { + yield return new List() { type1 + suffix, type2 + suffix }; + } + } + } + } + } + } + + private static string GetMarkup(string current, string referenced, ReferenceType refType, + string currentLanguage = LanguageNames.CSharp, + string referencedLanguage = LanguageNames.CSharp) + => refType switch + { + ReferenceType.None => CreateMarkupForSingleProject(current, referenced, currentLanguage), + ReferenceType.Project => GetMarkupWithReference(current, referenced, currentLanguage, referencedLanguage, true), + ReferenceType.Metadata => GetMarkupWithReference(current, referenced, currentLanguage, referencedLanguage, false), + _ => null, + }; + + public static IEnumerable BuiltInTypesWithReferenceTypeData + => CombineWithReferenceTypeData(BuiltInTypes); + + [MemberData(nameof(BuiltInTypesWithReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestPredefinedType(string type1, string type2, ReferenceType refType) + { + var file1 = $@" +using System; + +namespace Foo +{{ + public static class ExtensionClass + {{ + public static bool ExtentionMethod(this {type1} x) + => true; + }} +}}"; + var file2 = $@" +using System; + +namespace Baz +{{ + public class Bat + {{ + public void M({type2} x) + {{ + x.$$ + }} + }} +}}"; + + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UsingAliasInDeclaration(ReferenceType refType) + { + var file1 = @" +using System; +using MyInt = System.Int32; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this MyInt x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UsingAliasInDeclaration_PrimitiveType(ReferenceType refType) + { + var file1 = @" +using System; +using MyInt = System.Int32; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this MyInt x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UsingAliasInDeclaration_RegularType(ReferenceType refType) + { + var file1 = @" +using System; +using MyAlias = System.Exception; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this MyAlias x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(Exception x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UsingAliasInDeclaration_GenericType(ReferenceType refType) + { + var file1 = @" +using System; +using MyAlias = System.Collections.Generic.List; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this MyAlias x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(System.Collections.Generic.List x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UsingAliasInDeclaration_RegularTypeWithSameSimpleName(ReferenceType refType) + { + var file1 = @" +using DataTime = System.Exception; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this System.DateTime x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(DateTime x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UsingAliasInDeclaration_Namespace(ReferenceType refType) + { + var file1 = @" +using System; +using GenericCollection = System.Collections.Generic; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this GenericCollection.List x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(System.Collections.Generic.List x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + displayTextSuffix: "<>", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UsingAliasInUsage(ReferenceType refType) + { + var file1 = @" +using System; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this int x) + => true; + } +}"; + var file2 = @" +using System; +using MyInt = System.Int32; + +namespace Baz +{ + public class Bat + { + public void M(MyInt x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(AllTypeKindsWithReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task RegularType(string typeKind, ReferenceType refType) + { + var file1 = $@" +using System; + +public {typeKind} MyType {{ }} + +namespace Foo +{{ + public static class ExtensionClass + {{ + public static bool ExtentionMethod(this MyType t) + => true; + }} +}}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(MyType x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [MemberData(nameof(AllTypeKindsWithReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task ObjectType(string typeKind, ReferenceType refType) + { + var file1 = $@" +using System; + +public {typeKind} MyType {{ }} + +namespace Foo +{{ + public static class ExtensionClass + {{ + public static bool ExtentionMethod(this object t) + => true; + }} +}}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(MyType x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + public static IEnumerable TupleWithRefTypeData => CombineWithReferenceTypeData( + (new[] + { + "(int, int)", + "(int, (int, int))", + "(string a, string b)" + }).Select(tuple => new List() { tuple })); + + [MemberData(nameof(TupleWithRefTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task ValueTupleType(string tupleType, ReferenceType refType) + { + var file1 = $@" +using System; + +namespace Foo +{{ + public static class ExtensionClass + {{ + public static bool ExtentionMethod(this {tupleType} t) + => true; + }} +}}"; + var file2 = $@" +using System; + +namespace Baz +{{ + public class Bat + {{ + public void M({tupleType} x) + {{ + x.$$ + }} + }} +}}"; + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + public static IEnumerable DerivableTypeKindsWithReferenceTypeData + => CombineWithReferenceTypeData((new[] { "class", "interface", "abstract class" }).Select(kind => new List() { kind })); + + [MemberData(nameof(DerivableTypeKindsWithReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task RegularTypeAsBase(string baseType, ReferenceType refType) + { + var file1 = $@" +using System; + +public {baseType} MyBase {{ }} + +public class MyType : MyBase {{ }} + +namespace Foo +{{ + public static class ExtensionClass + {{ + public static bool ExtentionMethod(this MyBase t) + => true; + }} +}}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(MyType x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + public static IEnumerable BounedGenericTypeWithRefTypeData => CombineWithReferenceTypeData( + (new[] + { + "IEnumerable", + "List", + "string[]" + }).Select(tuple => new List() { tuple })); + + [MemberData(nameof(BounedGenericTypeWithRefTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task BoundedGenericType(string type, ReferenceType refType) + { + var file1 = @" +using System; +using System.Collections.Generic; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this IEnumerable t) + => true; + } +}"; + var file2 = $@" +using System; +using System.Collections.Generic; + +namespace Baz +{{ + public class Bat + {{ + public void M({type} x) + {{ + x.$$ + }} + }} +}}"; + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + public static IEnumerable TypeParameterWithRefTypeData => CombineWithReferenceTypeData( + (new[] + { + "IEnumerable", + "int", + "Bat", + "Bat" + }).Select(tuple => new List() { tuple })); + + [MemberData(nameof(TypeParameterWithRefTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task MatchingTypeParameter(string type, ReferenceType refType) + { + var file1 = @" +using System; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this T t) + => true; + } +}"; + var file2 = $@" +using System; +using System.Collections.Generic; + +namespace Baz +{{ + public interface Bar {{}} + + public class Bat + {{ + public void M({type} x) + {{ + x.$$ + }} + }} +}}"; + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + displayTextSuffix: "<>", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [InlineData(ReferenceType.Project)] + [InlineData(ReferenceType.Metadata)] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestInternalExtensionMethods_NoIVT_InReference(ReferenceType refType) + { + var file1 = @" +using System; + +namespace Foo +{ + internal static class ExtensionClass + { + public static bool ExtentionMethod(this int x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemIsAbsentAsync( + markup, + "ExtentionMethod", + inlineDescription: "Foo"); + } + + [InlineData(ReferenceType.Project)] + [InlineData(ReferenceType.Metadata)] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestConflictingInternalExtensionMethods_NoIVT_InReference(ReferenceType refType) + { + var file1 = @" +using System; + +namespace Foo +{ + internal static class ExtensionClass + { + public static bool ExtentionMethod(this int x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Foo +{ + internal static class ExtensionClass + { + public static bool ExtentionMethod(this int x) + => true; + } +} + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestInternalExtensionMethods_NoIVT_InSameProject() + { + var file1 = @" +using System; + +namespace Foo +{ + internal static class ExtensionClass + { + internal static bool ExtentionMethod(this int x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + + var markup = GetMarkup(file2, file1, ReferenceType.None); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodInternal, // This is based on declared accessibility + inlineDescription: "Foo"); + } + + // SymbolTreeInfo explicitly ignores non-public types from metadata(likely for perf reasons). So we don't need to test internals in PE reference + [InlineData(ReferenceType.None)] + [InlineData(ReferenceType.Project)] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestInternalExtensionMethods_WithIVT(ReferenceType refType) + { + var file1 = @" +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(""Project1"")] + +namespace Foo +{ + internal static class ExtensionClass + { + internal static bool ExtentionMethod(this int x) + => true; + } +}"; + var file2 = @" +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + + var markup = GetMarkup(file2, file1, refType); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodInternal, + inlineDescription: "Foo"); + } + + [MemberData(nameof(ReferenceTypeData))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task UserDefinedGenericType(ReferenceType refType) + { + var file1 = @" +using System; + +public class MyGeneric +{ +} + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this MyGeneric x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(MyGeneric x) + { + x.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, refType); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [InlineData("(1 + 1)")] + [InlineData("(new int())")] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task MethodSymbolReceiver(string expression) + { + var file1 = @" +using System; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtentionMethod(this int x) + => true; + } +}"; + var file2 = $@" +using System; + +namespace Baz +{{ + public class Bat + {{ + public void M() + {{ + {expression}.$$ + }} + }} +}}"; + var markup = GetMarkup(file2, file1, ReferenceType.None); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + public static IEnumerable VBBuiltInTypes + { + get + { + var predefinedTypes = new List<(string vbType, string csType)> + { + ( "Boolean", "bool" ), + ( "Byte", "byte" ), + ( "Char", "char" ), + ( "Date", "DateTime" ), + ( "Integer", "int" ), + ( "String", "string" ), + ( "Object", "object" ), + ( "Short", "short" ), + + }; + + var arraySuffixes = new (string vbSuffix, string csSuffix)[] { ("", ""), ("()", "[]"), ("(,)", "[,]") }; + + foreach (var type in predefinedTypes) + { + foreach (var suffix in arraySuffixes) + { + yield return new object[] { type.vbType + suffix.vbSuffix, type.csType + suffix.csSuffix }; + } + } + } + } + + [MemberData(nameof(VBBuiltInTypes))] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task ExtensionMethodDelcaredInVBSource(string vbType, string csType) + { + var file1 = $@" +Imports System +Imports System.Runtime.CompilerServices + +Namespace NS + Public Module Foo + + public Function ExtentionMethod(x As {vbType}) As Boolean + Return True + End Function + End Module +End Namespace"; + var file2 = $@" +using System; + +namespace Baz +{{ + public class Bat + {{ + public void M({csType} x) + {{ + x.$$ + }} + }} +}}"; + var markup = GetMarkup(file2, file1, ReferenceType.Project, currentLanguage: LanguageNames.CSharp, referencedLanguage: LanguageNames.VisualBasic); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "NS"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task ExtensionMethodDelcaredInRootNamespaceVBSource() + { + var file1 = @" +Imports System +Imports System.Runtime.CompilerServices + +Public Module Foo + + public Function ExtentionMethod(x As Integer) As Boolean + Return True + End Function +End Module"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootNamespace: "Root"); + + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Root"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task ExtensionMethodDelcaredInGlobalNamespaceVBSource() + { + var file1 = @" +Imports System +Imports System.Runtime.CompilerServices + +Public Module Foo + + public Function ExtentionMethod(x As Integer) As Boolean + Return True + End Function +End Module"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + } + } +}"; + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp); + + await VerifyTypeImportItemIsAbsentAsync( + markup, + "ExtentionMethod", + inlineDescription: ""); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestTriggerLocation() + { + var file1 = @" +using System; + +namespace Foo +{ + internal static class ExtensionClass + { + internal static bool ExtentionMethod(this int x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M(int x) + { + x.$$ + var z = 10; + } + } +}"; + + var markup = GetMarkup(file2, file1, ReferenceType.None); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtentionMethod", + glyph: (int)Glyph.ExtensionMethodInternal, // This is based on declared accessibility + inlineDescription: "Foo"); + } + + [InlineData("int", "Int32Method", "Foo")] + [InlineData("string", "StringMethod", "Bar")] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestIdenticalAliases(string type, string expectedMethodname, string expectedNamespace) + { + var file1 = @" +using X = System.String; + +namespace Foo +{ + using X = System.Int32; + + internal static class ExtensionClass + { + internal static bool Int32Method(this X x) + => true; + } +} + +namespace Bar +{ + internal static class ExtensionClass + { + internal static bool StringMethod(this X x) + => true; + } +} +"; + var file2 = $@" +using System; + +namespace Baz +{{ + public class Bat + {{ + public void M({type} x) + {{ + x.$$ + }} + }} +}}"; + + var markup = GetMarkup(file2, file1, ReferenceType.None); + await VerifyTypeImportItemExistsAsync( + markup, + expectedMethodname, + glyph: (int)Glyph.ExtensionMethodInternal, + inlineDescription: expectedNamespace); + } + + [InlineData("int")] + [InlineData("Exception")] + [Theory, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task TestIdenticalMethodName(string type) + { + var file1 = @" +using System; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtMethod(this int x) + => true; + + public static bool ExtMethod(this Exception x) + => true; + } +} +"; + var file2 = $@" +using System; + +namespace Baz +{{ + public class Bat + {{ + public void M({type} x) + {{ + x.$$ + }} + }} +}}"; + + var markup = GetMarkup(file2, file1, ReferenceType.None); + await VerifyTypeImportItemExistsAsync( + markup, + "ExtMethod", + glyph: (int)Glyph.ExtensionMethodPublic, + inlineDescription: "Foo"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task DoNotTriggerOnType() + { + var file1 = @" +using System; + +namespace Foo +{ + public static class ExtensionClass + { + public static bool ExtMethod(this string x) + => true; + } +}"; + var file2 = @" +using System; + +namespace Baz +{ + public class Bat + { + public void M() + { + string.$$ + } + } +}"; + var markup = GetMarkup(file2, file1, ReferenceType.None); + await VerifyTypeImportItemIsAbsentAsync( + markup, + "ExtMethod", + inlineDescription: "Foo"); + } + + private Task VerifyTypeImportItemExistsAsync(string markup, string expectedItem, int glyph, string inlineDescription, string displayTextSuffix = null, string expectedDescriptionOrNull = null) + { + return VerifyItemExistsAsync(markup, expectedItem, displayTextSuffix: displayTextSuffix, glyph: glyph, inlineDescription: inlineDescription, expectedDescriptionOrNull: expectedDescriptionOrNull); + } + + private Task VerifyTypeImportItemIsAbsentAsync(string markup, string expectedItem, string inlineDescription, string displayTextSuffix = null) + { + return VerifyItemIsAbsentAsync(markup, expectedItem, displayTextSuffix: displayTextSuffix, inlineDescription: inlineDescription); + } + } +} diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs index 25bde3153d21860fec1cc30beba6322c033f8294..f35a994a1a4c97190393bf753c4aee745c005624 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs @@ -266,13 +266,6 @@ class Bat await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "Foo"); } - private static string GetMarkupWithReference(string currentFile, string referencedFile, bool isProjectReference, string alias = null) - { - return isProjectReference - ? CreateMarkupForProjecWithProjectReference(currentFile, referencedFile, LanguageNames.CSharp, LanguageNames.CSharp) - : CreateMarkupForProjectWithMetadataReference(currentFile, referencedFile, LanguageNames.CSharp, LanguageNames.CSharp); - } - [InlineData(true)] [InlineData(false)] [Theory, Trait(Traits.Feature, Traits.Features.Completion)] @@ -292,7 +285,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo"); } @@ -316,7 +309,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "Foo"); } @@ -339,7 +332,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "Foo"); } @@ -365,7 +358,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", displayTextSuffix: "", inlineDescription: "Foo"); await VerifyTypeImportItemExistsAsync(markup, "Bar", displayTextSuffix: "<>", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo"); } @@ -393,7 +386,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", displayTextSuffix: "", inlineDescription: "Foo"); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", displayTextSuffix: "<>", inlineDescription: "Foo"); } @@ -422,7 +415,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassInternal, inlineDescription: "Foo"); await VerifyTypeImportItemExistsAsync(markup, "Bar", displayTextSuffix: "<>", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo"); } @@ -452,7 +445,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "Foo"); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", displayTextSuffix: "<>", inlineDescription: "Foo"); } @@ -482,7 +475,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo"); await VerifyTypeImportItemExistsAsync(markup, "Bar", displayTextSuffix: "<>", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo"); } @@ -512,7 +505,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "Foo"); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", displayTextSuffix: "<>", inlineDescription: "Foo"); } @@ -544,7 +537,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassInternal, inlineDescription: "Foo"); await VerifyTypeImportItemExistsAsync(markup, "Bar", displayTextSuffix: "<>", glyph: (int)Glyph.ClassInternal, inlineDescription: "Foo"); } @@ -570,7 +563,7 @@ class Bat $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassInternal, inlineDescription: "Foo"); } @@ -590,7 +583,7 @@ class Bat $$ } }"; - var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootnamespace: "Foo"); + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootNamespace: "Foo"); await VerifyTypeImportItemExistsAsync(markup, "Barr", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo.Bar"); } @@ -616,7 +609,7 @@ class Bat $$ } }"; - var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootnamespace: ""); + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootNamespace: ""); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassPublic, inlineDescription: "Na"); await VerifyTypeImportItemExistsAsync(markup, "Foo", glyph: (int)Glyph.ClassPublic, inlineDescription: "Na"); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "na"); @@ -646,7 +639,7 @@ class Bat $$ } }"; - var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootnamespace: ""); + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootNamespace: ""); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "Na"); await VerifyTypeImportItemIsAbsentAsync(markup, "Foo", inlineDescription: "Na"); await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "na"); @@ -669,7 +662,7 @@ class Bat $$ } }"; - var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootnamespace: "Foo"); + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootNamespace: "Foo"); await VerifyTypeImportItemIsAbsentAsync(markup, "Barr", inlineDescription: "Foo.Bar"); } @@ -690,7 +683,7 @@ class Bat $$ } }"; - var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootnamespace: "Foo"); + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootNamespace: "Foo"); await VerifyTypeImportItemIsAbsentAsync(markup, "Barr", inlineDescription: "Foo.Bar"); } @@ -724,7 +717,7 @@ class C $$ } }"; - var markup = GetMarkupWithReference(file2, file1, isProjectReference); + var markup = GetMarkupWithReference(file2, file1, LanguageNames.CSharp, LanguageNames.CSharp, isProjectReference); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo"); await VerifyTypeImportItemExistsAsync(markup, "Bar", displayTextSuffix: "<>", glyph: (int)Glyph.ClassPublic, inlineDescription: "Foo"); await VerifyTypeImportItemExistsAsync(markup, "Bar", glyph: (int)Glyph.ClassPublic, inlineDescription: "Baz"); @@ -797,7 +790,7 @@ class Bat Barr$$ } }"; - var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootnamespace: "Foo"); + var markup = CreateMarkupForProjecWithVBProjectReference(file2, file1, sourceLanguage: LanguageNames.CSharp, rootNamespace: "Foo"); await VerifyCustomCommitProviderAsync(markup, "Barr", expectedCodeAfterCommit, sourceCodeKind: kind); } @@ -1305,6 +1298,29 @@ public async Task TriggerCompletionInSubsequentSubmission() Assert.NotEmpty(completionList.Items); } + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task ShouldNotTriggerInsideTrivia() + { + var file1 = $@" +namespace Foo +{{ + public class Bar + {{}} +}}"; + + var file2 = @" +namespace Baz +{ + /// + /// + /// + class Bat + { + } +}"; + var markup = CreateMarkupForSingleProject(file2, file1, LanguageNames.CSharp); + await VerifyTypeImportItemIsAbsentAsync(markup, "Bar", inlineDescription: "Foo"); + } private static void AssertRelativeOrder(List expectedTypesInRelativeOrder, ImmutableArray allCompletionItems) { var hashset = new HashSet(expectedTypesInRelativeOrder); diff --git a/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs b/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs index e040fd8e0a76b5a683acf48f2929a49b52384d3a..6ed4ecd5e5e1f5dd2917d032ce95b91ec85a80e4 100644 --- a/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs +++ b/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs @@ -580,6 +580,13 @@ protected virtual void SetWorkspaceOptions(TestWorkspace workspace) return VerifyItemWithReferenceWorkerAsync(xmlString, expectedItem, expectedSymbols, hideAdvancedMembers); } + protected static string GetMarkupWithReference(string currentFile, string referencedFile, string sourceLanguage, string referenceLanguage, bool isProjectReference, string alias = null) + { + return isProjectReference + ? CreateMarkupForProjecWithProjectReference(currentFile, referencedFile, sourceLanguage, referenceLanguage) + : CreateMarkupForProjectWithMetadataReference(currentFile, referencedFile, sourceLanguage, referenceLanguage); + } + protected static string CreateMarkupForProjectWithMetadataReference(string markup, string metadataReferenceCode, string sourceLanguage, string referencedLanguage) { return string.Format(@" @@ -588,6 +595,7 @@ protected static string CreateMarkupForProjectWithMetadataReference(string marku {1} {3} + " + typeof(ValueTuple<>).Assembly.Location + @" ", sourceLanguage, SecurityElement.Escape(markup), referencedLanguage, SecurityElement.Escape(metadataReferenceCode)); @@ -608,8 +616,10 @@ protected static string CreateMarkupForProjectWithAliasedMetadataReference(strin {1} + " + typeof(ValueTuple<>).Assembly.Location + @" {4} + " + typeof(ValueTuple<>).Assembly.Location + @" ", sourceLanguage, SecurityElement.Escape(markup), referencedLanguage, SecurityElement.Escape(aliases), SecurityElement.Escape(referencedCode)); @@ -652,7 +662,7 @@ protected static string CreateMarkupForProjecWithProjectReference(string markup, ", sourceLanguage, SecurityElement.Escape(markup), referencedLanguage, SecurityElement.Escape(referencedCode)); } - protected static string CreateMarkupForProjecWithVBProjectReference(string markup, string referencedCode, string sourceLanguage, string rootnamespace = "") + protected static string CreateMarkupForProjecWithVBProjectReference(string markup, string referencedCode, string sourceLanguage, string rootNamespace = "") { return string.Format(@" @@ -665,7 +675,7 @@ protected static string CreateMarkupForProjecWithVBProjectReference(string marku -", sourceLanguage, SecurityElement.Escape(markup), LanguageNames.VisualBasic, SecurityElement.Escape(referencedCode), rootnamespace); +", sourceLanguage, SecurityElement.Escape(markup), LanguageNames.VisualBasic, SecurityElement.Escape(referencedCode), rootNamespace); } private Task VerifyItemInSameProjectAsync(string markup, string referencedCode, string expectedItem, int expectedSymbols, string sourceLanguage, bool hideAdvancedMembers) diff --git a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/ExtensionMethodImportCompletionProviderTests.vb b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/ExtensionMethodImportCompletionProviderTests.vb new file mode 100644 index 0000000000000000000000000000000000000000..649bfe3e8466e307d07b30351d272f7dbb11c888 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/ExtensionMethodImportCompletionProviderTests.vb @@ -0,0 +1,279 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports Microsoft.CodeAnalysis.Completion +Imports Microsoft.CodeAnalysis.Editor.UnitTests +Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces +Imports Microsoft.CodeAnalysis.VisualBasic.Completion.Providers +Imports Microsoft.VisualStudio.Composition + +Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Completion.CompletionProviders + + + Public Class ExtensionMethodImportCompletionProviderTests + Inherits AbstractVisualBasicCompletionProviderTests + + Public Sub New(workspaceFixture As VisualBasicTestWorkspaceFixture) + MyBase.New(workspaceFixture) + End Sub + + Private Property IsExpandedCompletion As Boolean = True + + Private Property ShowImportCompletionItemsOptionValue As Boolean = True + + ' -1 would disable timebox, whereas 0 means always timeout. + Private Property TimeoutInMilliseconds As Integer = -1 + + Protected Overrides Sub SetWorkspaceOptions(workspace As TestWorkspace) + workspace.Options = workspace.Options _ + .WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.VisualBasic, ShowImportCompletionItemsOptionValue) _ + .WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForImportCompletion, TimeoutInMilliseconds) _ + .WithChangedOption(CompletionServiceOptions.IsExpandedCompletion, IsExpandedCompletion) + End Sub + + Protected Overrides Function GetExportProvider() As ExportProvider + Return ExportProviderCache.GetOrCreateExportProviderFactory(TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic.WithPart(GetType(TestExperimentationService))).CreateExportProvider() + End Function + + Friend Overrides Function CreateCompletionProvider() As CompletionProvider + Return New ExtensionMethodImportCompletionProvider() + End Function + + Public Enum ReferenceType + None + Project + Metadata + End Enum + + Public Shared Function ReferenceTypeData() As IEnumerable(Of Object()) + Return (New ReferenceType() {ReferenceType.None, ReferenceType.Project, ReferenceType.Metadata}).Select(Function(refType) + Return New Object() {refType} + End Function) + End Function + + Private Shared Function GetMarkup(current As String, referenced As String, refType As ReferenceType, Optional currentLanguage As String = LanguageNames.VisualBasic, Optional referencedLanguage As String = LanguageNames.VisualBasic) As String + If refType = ReferenceType.None Then + Return CreateMarkupForSingleProject(current, referenced, currentLanguage) + ElseIf refType = ReferenceType.Project Then + Return GetMarkupWithReference(current, referenced, currentLanguage, referencedLanguage, True) + ElseIf refType = ReferenceType.Metadata Then + Return GetMarkupWithReference(current, referenced, currentLanguage, referencedLanguage, False) + Else + Return Nothing + End If + End Function + + + + + Public Async Function TestExtensionAttribute(refType As ReferenceType) As Task + + ' attribute suffix isn't capitalized + Dim file1 = + Public Sub ExtensionMethod1(aString As String) + Console.WriteLine(aString) + End Sub + + + Public Sub ExtensionMethod2(aString As String) + Console.WriteLine(aString) + End Sub + + + Public Sub ExtensionMethod3(aString As String) + Console.WriteLine(aString) + End Sub + + + Public Sub ExtensionMethod4(aString As String) + Console.WriteLine(aString) + End Sub + + + Public Sub ExtensionMethod5(aString As String) + Console.WriteLine(aString) + End Sub + + + Public Sub ExtensionMethod6(aString As String) + Console.WriteLine(aString) + End Sub + + Public Sub ExtensionMethod7(aString As String) + Console.WriteLine(aString) + End Sub + End Module +End Namespace]]>.Value + + Dim file2 = .Value + + Dim markup = GetMarkup(file2, file1, refType) + Await VerifyItemExistsAsync(markup, "ExtensionMethod1", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + Await VerifyItemExistsAsync(markup, "ExtensionMethod2", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + Await VerifyItemExistsAsync(markup, "ExtensionMethod3", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + Await VerifyItemExistsAsync(markup, "ExtensionMethod4", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + Await VerifyItemExistsAsync(markup, "ExtensionMethod5", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + Await VerifyItemExistsAsync(markup, "ExtensionMethod6", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + Await VerifyItemIsAbsentAsync(markup, "ExtensionMethod7", inlineDescription:="Foo") + End Function + + + + + Public Async Function TestCaseMismatchInTargetType(refType As ReferenceType) As Task + + ' attribute suffix isn't capitalized + Dim file1 = + Public Sub ExtensionMethod1(exp As exception) + End Sub + + + Public Sub ExtensionMethod2(exp As Exception) + Console.WriteLine(aString) + End Sub + End Module +End Namespace]]>.Value + + Dim file2 = .Value + + Dim markup = GetMarkup(file2, file1, refType) + Await VerifyItemExistsAsync(markup, "ExtensionMethod1", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + Await VerifyItemExistsAsync(markup, "ExtensionMethod2", glyph:=Glyph.ExtensionMethodPublic, inlineDescription:="Foo") + End Function + + + + + Public Async Function TestCaseMismatchInNamespaceImport(refType As ReferenceType) As Task + + ' attribute suffix isn't capitalized + Dim file1 = + Public Sub ExtensionMethod1(exp As exception) + End Sub + + + Public Sub ExtensionMethod2(exp As Exception) + Console.WriteLine(aString) + End Sub + End Module +End Namespace]]>.Value + + Dim file2 = .Value + + Dim markup = GetMarkup(file2, file1, refType) + Await VerifyItemIsAbsentAsync(markup, "ExtensionMethod1", inlineDescription:="Foo") + Await VerifyItemIsAbsentAsync(markup, "ExtensionMethod2", inlineDescription:="Foo") + End Function + + + + + Public Async Function TestImplicitTarget1(refType As ReferenceType) As Task + + Dim file1 = + Public Function ExtentionMethod(x As Bar) As Boolean + Return True + End Function + End Module + + Public Class Bar + Public X As Boolean + End Class +End Namespace]]>.Value + + Dim file2 = .Value + + Dim markup = GetMarkup(file2, file1, refType) + Await VerifyItemIsAbsentAsync(markup, "ExtentionMethod", inlineDescription:="NS") + End Function + + + + + Public Async Function TestImplicitTarget2(refType As ReferenceType) As Task + + Dim file1 = + Public Function ExtentionMethod(x As Bar) As Boolean + Return True + End Function + End Module + + Public Class Bar + Public X As Boolean + End Class +End Namespace]]>.Value + + Dim file2 = .Value + + Dim markup = GetMarkup(file2, file1, refType) + Await VerifyItemIsAbsentAsync(markup, "ExtentionMethod", inlineDescription:="NS") + End Function + End Class +End Namespace diff --git a/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs b/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs index 4bc4869a02977f48db0ea7fb11c7da0315d4c45f..7095e57447c5c7f075fbdaeb73ff89adce360db1 100644 --- a/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs +++ b/src/Features/CSharp/Portable/Completion/CSharpCompletionService.cs @@ -61,7 +61,9 @@ internal class CSharpCompletionService : CommonCompletionService new TupleNameCompletionProvider(), new DeclarationNameCompletionProvider(), new InternalsVisibleToCompletionProvider(), - new PropertySubpatternCompletionProvider()); + new PropertySubpatternCompletionProvider(), + new TypeImportCompletionProvider(), + new ExtensionMethodImportCompletionProvider()); var languageServices = workspace.Services.GetLanguageServices(LanguageNames.CSharp); var languagesProvider = languageServices.GetService(); @@ -71,8 +73,6 @@ internal class CSharpCompletionService : CommonCompletionService new EmbeddedLanguageCompletionProvider(languagesProvider)); } - defaultCompletionProviders = defaultCompletionProviders.Add(new TypeImportCompletionProvider()); - _defaultCompletionProviders = defaultCompletionProviders; } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..e0292001844e283352b7c3229467a4f44fb2e2d3 --- /dev/null +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion.Providers; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +{ + internal sealed class ExtensionMethodImportCompletionProvider : AbstractExtensionMethodImportCompletionProvider + { + protected override string GenericSuffix => "<>"; + + internal override bool IsInsertionTrigger(SourceText text, int characterPosition, OptionSet options) + => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); + + protected override ImmutableArray GetImportedNamespaces( + SyntaxNode location, + SemanticModel semanticModel, + CancellationToken cancellationToken) + => ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel, cancellationToken); + + protected override Task CreateContextAsync(Document document, int position, CancellationToken cancellationToken) + => ImportCompletionProviderHelper.CreateContextAsync(document, position, cancellationToken); + } +} diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..270e5c821e603401d8362dd7ed0dbcd8926d1c8f --- /dev/null +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers +{ + internal static class ImportCompletionProviderHelper + { + public static ImmutableArray GetImportedNamespaces( + SyntaxNode location, + SemanticModel semanticModel, + CancellationToken cancellationToken) + => semanticModel.GetUsingNamespacesInScope(location) + .SelectAsArray(namespaceSymbol => namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); + + public static async Task CreateContextAsync(Document document, int position, CancellationToken cancellationToken) + { + // Need regular semantic model because we will use it to get imported namespace symbols. Otherwise we will try to + // reach outside of the span and ended up with "node not within syntax tree" error from the speculative model. + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + return CSharpSyntaxContext.CreateContext(document.Project.Solution.Workspace, semanticModel, position, cancellationToken); + } + } +} diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs index d62f7f45583904963ea0e2d1f1dc9d90032dcf81..0ff2976e2eaff4b83c91cc674f1c3f339bb471ab 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs @@ -1,21 +1,18 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Completion.Providers; -using Microsoft.CodeAnalysis.CSharp.Extensions; -using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; -using Microsoft.CodeAnalysis.CSharp.Syntax; 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 { - internal class TypeImportCompletionProvider : AbstractTypeImportCompletionProvider + internal sealed class TypeImportCompletionProvider : AbstractTypeImportCompletionProvider { internal override bool IsInsertionTrigger(SourceText text, int characterPosition, OptionSet options) => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options); @@ -24,25 +21,9 @@ internal override bool IsInsertionTrigger(SourceText text, int characterPosition SyntaxNode location, SemanticModel semanticModel, CancellationToken cancellationToken) - { - // Get namespaces from usings - return semanticModel.GetUsingNamespacesInScope(location) - .SelectAsArray(namespaceSymbol => namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); - } - - protected override async Task CreateContextAsync(Document document, int position, CancellationToken cancellationToken) - { - // Need regular semantic model because we will use it to get imported namespace symbols. Otherwise we will try to - // reach outside of the span and ended up with "node not within syntax tree" error from the speculative model. - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return CSharpSyntaxContext.CreateContext(document.Project.Solution.Workspace, semanticModel, position, cancellationToken); - } + => ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel, cancellationToken); - protected override async Task IsInImportsDirectiveAsync(Document document, int position, CancellationToken cancellationToken) - { - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDirectives: true); - return leftToken.GetAncestor() != null; - } + protected override Task CreateContextAsync(Document document, int position, CancellationToken cancellationToken) + => ImportCompletionProviderHelper.CreateContextAsync(document, position, cancellationToken); } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/CSharpTypeImportCompletionServiceFactory.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionServiceFactory.cs similarity index 88% rename from src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/CSharpTypeImportCompletionServiceFactory.cs rename to src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionServiceFactory.cs index a0323cb6ec733f78d9f774566f8e20e3837cba9c..59c6c88432df3cd45ebb45c6cc624d1c24719d4f 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/CSharpTypeImportCompletionServiceFactory.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionServiceFactory.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System.Composition; using Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion; using Microsoft.CodeAnalysis.Host; @@ -8,10 +10,10 @@ namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers { [ExportLanguageServiceFactory(typeof(ITypeImportCompletionService), LanguageNames.CSharp), Shared] - internal sealed class CSharpTypeImportCompletionServiceFactory : ILanguageServiceFactory + internal sealed class TypeImportCompletionServiceFactory : ILanguageServiceFactory { [ImportingConstructor] - public CSharpTypeImportCompletionServiceFactory() + public TypeImportCompletionServiceFactory() { } diff --git a/src/Features/Core/Portable/Completion/CompletionHelper.cs b/src/Features/Core/Portable/Completion/CompletionHelper.cs index 72087cb73202b24a71bca3cfba2d856d096e35b5..3abb405f06f1dde579c9e5e440c04a1e98838d1a 100644 --- a/src/Features/Core/Portable/Completion/CompletionHelper.cs +++ b/src/Features/Core/Portable/Completion/CompletionHelper.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -26,7 +28,7 @@ public CompletionHelper(bool isCaseSensitive) public static CompletionHelper GetHelper(Document document) { - return document.Project.Solution.Workspace.Services.GetService() + return document.Project.Solution.Workspace.Services.GetRequiredService() .GetCompletionHelper(document); } @@ -309,5 +311,8 @@ private static int CompareExpandedItem(CompletionItem item1, PatternMatch match1 // Non-expanded item is the only exact match, so we definitely prefer it. return isItem1Expanded ? 1 : -1; } + + public static string ConcatNamespace(string? containingNamespace, string name) + => string.IsNullOrEmpty(containingNamespace) ? name : containingNamespace + "." + name; } } diff --git a/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs b/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs index 9b5643a42cf197f80c13b965a96cb2c4b57c53c0..22fd975d7ab66ac1d72738d6b94dbb7617bf996d 100644 --- a/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs +++ b/src/Features/Core/Portable/Completion/Log/CompletionProvidersLogger.cs @@ -22,7 +22,16 @@ internal enum ActionInfo TypeImportCompletionReferenceCount, TypeImportCompletionTimeoutCount, - TargetTypeCompletionTicks + TargetTypeCompletionTicks, + + ExtensionMethodCompletionSuccessCount, + // following are only reported when sucessful (i.e. filter is available) + ExtensionMethodCompletionTicks, + ExtensionMethodCompletionMethodsProvided, + ExtensionMethodCompletionGetFilterTicks, + ExtensionMethodCompletionGetSymbolTicks, + ExtensionMethodCompletionTypesChecked, + ExtensionMethodCompletionMethodsChecked, } internal static void LogTypeImportCompletionTicksDataPoint(int count) => @@ -40,6 +49,29 @@ internal enum ActionInfo internal static void LogTargetTypeCompletionTicksDataPoint(int count) => s_statisticLogAggregator.AddDataPoint((int)ActionInfo.TargetTypeCompletionTicks, count); + + internal static void LogExtensionMethodCompletionSuccess() => + s_logAggregator.IncreaseCount((int)ActionInfo.ExtensionMethodCompletionSuccessCount); + + internal static void LogExtensionMethodCompletionTicksDataPoint(int count) => + s_statisticLogAggregator.AddDataPoint((int)ActionInfo.ExtensionMethodCompletionTicks, count); + + internal static void LogExtensionMethodCompletionMethodsProvidedDataPoint(int count) => + s_statisticLogAggregator.AddDataPoint((int)ActionInfo.ExtensionMethodCompletionMethodsProvided, count); + + internal static void LogExtensionMethodCompletionGetFilterTicksDataPoint(int count) => + s_statisticLogAggregator.AddDataPoint((int)ActionInfo.ExtensionMethodCompletionGetFilterTicks, count); + + internal static void LogExtensionMethodCompletionGetSymbolTicksDataPoint(int count) => + s_statisticLogAggregator.AddDataPoint((int)ActionInfo.ExtensionMethodCompletionGetSymbolTicks, count); + + internal static void LogExtensionMethodCompletionTypesCheckedDataPoint(int count) => + s_statisticLogAggregator.AddDataPoint((int)ActionInfo.ExtensionMethodCompletionTypesChecked, count); + + internal static void LogExtensionMethodCompletionMethodsCheckedDataPoint(int count) => + s_statisticLogAggregator.AddDataPoint((int)ActionInfo.ExtensionMethodCompletionMethodsChecked, count); + + internal static void ReportTelemetry() { Logger.Log(FunctionId.Intellisense_CompletionProviders_Data, KeyValueLogMessage.Create(m => diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractExtensionMethodImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractExtensionMethodImportCompletionProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..5246696d24ed5783c8cd349ec148177616a1d767 --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractExtensionMethodImportCompletionProvider.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; + +namespace Microsoft.CodeAnalysis.Completion.Providers +{ + internal abstract class AbstractExtensionMethodImportCompletionProvider : AbstractImportCompletionProvider + { + protected abstract string GenericSuffix { get; } + + protected override bool ShouldProvideCompletion(Document document, SyntaxContext syntaxContext) + => syntaxContext.IsRightOfNameSeparator && IsAddingImportsSupported(document); + + protected async override Task AddCompletionItemsAsync( + CompletionContext completionContext, + SyntaxContext syntaxContext, + HashSet namespaceInScope, + bool isExpandedCompletion, + CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.Completion_ExtensionMethodImportCompletionProvider_GetCompletionItemsAsync, cancellationToken)) + { + var syntaxFacts = completionContext.Document.GetRequiredLanguageService(); + if (TryGetReceiverTypeSymbol(syntaxContext, syntaxFacts, cancellationToken, out var receiverTypeSymbol)) + { + var items = await ExtensionMethodImportCompletionHelper.GetUnimportedExtensionMethodsAsync( + completionContext.Document, + completionContext.Position, + receiverTypeSymbol, + namespaceInScope, + forceIndexCreation: isExpandedCompletion, + cancellationToken).ConfigureAwait(false); + + completionContext.AddItems(items.Select(i => Convert(i))); + } + else + { + // If we can't get a valid receiver type, then we don't show expander as available. + // We need to set this explicitly here bacause we didn't do the (more expensive) symbol check inside + // `ShouldProvideCompletion` method above, which is intended for quick syntax based check. + completionContext.ExpandItemsAvailable = false; + } + } + } + + private static bool TryGetReceiverTypeSymbol( + SyntaxContext syntaxContext, + ISyntaxFactsService syntaxFacts, + CancellationToken cancellationToken, + [NotNullWhen(true)] out ITypeSymbol? receiverTypeSymbol) + { + var parentNode = syntaxContext.TargetToken.Parent; + + // Even though implicit access to extension method is allowed, we decide not support it for simplicity + // e.g. we will not provide completion for unimport extension method in this case + // New Bar() {.X = .$$ } + var expressionNode = syntaxFacts.GetLeftSideOfDot(parentNode, allowImplicitTarget: false); + + if (expressionNode != null) + { + // Check if we are accessing members of a type, no extension methods are exposed off of types. + if (!(syntaxContext.SemanticModel.GetSymbolInfo(expressionNode, cancellationToken).GetAnySymbol() is ITypeSymbol)) + { + // The expression we're calling off of needs to have an actual instance type. + // We try to be more tolerant to errors here so completion would still be available in certain case of partially typed code. + receiverTypeSymbol = syntaxContext.SemanticModel.GetTypeInfo(expressionNode, cancellationToken).Type; + if (receiverTypeSymbol is IErrorTypeSymbol errorTypeSymbol) + { + receiverTypeSymbol = errorTypeSymbol.CandidateSymbols.Select(s => GetSymbolType(s)).FirstOrDefault(s => s != null); + } + + return receiverTypeSymbol != null; + } + } + + receiverTypeSymbol = null; + return false; + } + + private static ITypeSymbol? GetSymbolType(ISymbol symbol) + => symbol switch + { + ILocalSymbol localSymbol => localSymbol.Type, + IFieldSymbol fieldSymbol => fieldSymbol.Type, + IPropertySymbol propertySymbol => propertySymbol.Type, + IParameterSymbol parameterSymbol => parameterSymbol.Type, + IAliasSymbol aliasSymbol => aliasSymbol.Target as ITypeSymbol, + _ => symbol as ITypeSymbol, + }; + + private CompletionItem Convert(SerializableImportCompletionItem serializableItem) + => ImportCompletionItem.Create( + serializableItem.Name, + serializableItem.Arity, + serializableItem.ContainingNamespace, + serializableItem.Glyph, + GenericSuffix, + CompletionItemFlags.Expanded, + serializableItem.SymbolKeyData); + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..c023d757b36ad388316a8b7c522480690a2272a8 --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion +{ + internal abstract class AbstractImportCompletionCacheServiceFactory : IWorkspaceServiceFactory + { + private readonly ConcurrentDictionary _peItemsCache + = new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _projectItemsCache + = new ConcurrentDictionary(); + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + var workspace = workspaceServices.Workspace; + if (workspace.Kind == WorkspaceKind.Host) + { + var cacheService = workspaceServices.GetService(); + if (cacheService != null) + { + cacheService.CacheFlushRequested += OnCacheFlushRequested; + } + } + + return new ImportCompletionCacheService(_peItemsCache, _projectItemsCache); + } + + private void OnCacheFlushRequested(object sender, EventArgs e) + { + _peItemsCache.Clear(); + _projectItemsCache.Clear(); + } + + private class ImportCompletionCacheService : IImportCompletionCacheService + { + public IDictionary PEItemsCache { get; } + + public IDictionary ProjectItemsCache { get; } + + public ImportCompletionCacheService( + ConcurrentDictionary peCache, + ConcurrentDictionary projectCache) + { + PEItemsCache = peCache; + ProjectItemsCache = projectCache; + } + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..1d26b13c465c88869a7865b7367a2b9628dd83d1 --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.AddImports; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Experiments; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Completion.Providers +{ + internal abstract class AbstractImportCompletionProvider : CommonCompletionProvider + { + protected abstract Task CreateContextAsync(Document document, int position, CancellationToken cancellationToken); + protected abstract ImmutableArray GetImportedNamespaces(SyntaxNode location, SemanticModel semanticModel, CancellationToken cancellationToken); + protected abstract bool ShouldProvideCompletion(Document document, SyntaxContext syntaxContext); + protected abstract Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet namespacesInScope, bool isExpandedCompletion, CancellationToken cancellationToken); + + internal override bool IsExpandItemProvider => true; + + private bool? _isImportCompletionExperimentEnabled = null; + + private bool IsExperimentEnabled(Workspace workspace) + { + if (!_isImportCompletionExperimentEnabled.HasValue) + { + var experimentationService = workspace.Services.GetRequiredService(); + _isImportCompletionExperimentEnabled = experimentationService.IsExperimentEnabled(WellKnownExperimentNames.TypeImportCompletion); + } + + return _isImportCompletionExperimentEnabled == true; + } + + public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + { + var cancellationToken = completionContext.CancellationToken; + var document = completionContext.Document; + + // We need to check for context before option values, so we can tell completion service that we are in a context to provide expanded items + // even though import completion might be disabled. This would show the expander in completion list which user can then use to explicitly ask for unimported items. + var syntaxContext = await CreateContextAsync(document, completionContext.Position, cancellationToken).ConfigureAwait(false); + if (!ShouldProvideCompletion(document, syntaxContext)) + { + return; + } + + completionContext.ExpandItemsAvailable = true; + + // We will trigger import completion regardless of the option/experiment if extended items is being requested explicitly (via expander in completion list) + var isExpandedCompletion = completionContext.Options.GetOption(CompletionServiceOptions.IsExpandedCompletion); + if (!isExpandedCompletion) + { + var importCompletionOptionValue = completionContext.Options.GetOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, document.Project.Language); + + // Don't trigger import completion if the option value is "default" and the experiment is disabled for the user. + if (importCompletionOptionValue == false || + (importCompletionOptionValue == null && !IsExperimentEnabled(document.Project.Solution.Workspace))) + { + return; + } + } + + // Find all namespaces in scope at current cursor location, + // which will be used to filter so the provider only returns out-of-scope types. + var namespacesInScope = GetNamespacesInScope(document, syntaxContext, cancellationToken); + await AddCompletionItemsAsync(completionContext, syntaxContext, namespacesInScope, isExpandedCompletion, cancellationToken).ConfigureAwait(false); + } + + private HashSet GetNamespacesInScope(Document document, SyntaxContext syntaxContext, CancellationToken cancellationToken) + { + var semanticModel = syntaxContext.SemanticModel; + var importedNamespaces = GetImportedNamespaces(syntaxContext.LeftToken.Parent, semanticModel, cancellationToken); + + // This hashset will be used to match namespace names, so it must have the same case-sensitivity as the source language. + var syntaxFacts = document.GetRequiredLanguageService(); + var namespacesInScope = new HashSet(importedNamespaces, syntaxFacts.StringComparer); + + // Get containing namespaces. + var namespaceSymbol = semanticModel.GetEnclosingNamespace(syntaxContext.Position, cancellationToken); + while (namespaceSymbol != null) + { + namespacesInScope.Add(namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); + namespaceSymbol = namespaceSymbol.ContainingNamespace; + } + + return namespacesInScope; + } + + internal override async Task GetChangeAsync(Document document, CompletionItem completionItem, TextSpan completionListSpan, char? commitKey, CancellationToken cancellationToken) + { + var containingNamespace = ImportCompletionItem.GetContainingNamespace(completionItem); + + if (await ShouldCompleteWithFullyQualifyTypeName().ConfigureAwait(false)) + { + var fullyQualifiedName = $"{containingNamespace}.{completionItem.DisplayText}"; + var change = new TextChange(completionListSpan, fullyQualifiedName); + + return CompletionChange.Create(change); + } + + // Find context node so we can use it to decide where to insert using/imports. + var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var addImportContextNode = root.FindToken(completionListSpan.Start, findInsideTrivia: true).Parent; + + // Add required using/imports directive. + var addImportService = document.GetRequiredLanguageService(); + var optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + var placeSystemNamespaceFirst = optionSet.GetOption(GenerationOptions.PlaceSystemNamespaceFirst, document.Project.Language); + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var importNode = CreateImport(document, containingNamespace); + + var rootWithImport = addImportService.AddImport(compilation, root, addImportContextNode, importNode, placeSystemNamespaceFirst, cancellationToken); + var documentWithImport = document.WithSyntaxRoot(rootWithImport); + // This only formats the annotated import we just added, not the entire document. + var formattedDocumentWithImport = await Formatter.FormatAsync(documentWithImport, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); + + var builder = ArrayBuilder.GetInstance(); + + // Get text change for add import + var importChanges = await formattedDocumentWithImport.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); + builder.AddRange(importChanges); + + // Create text change for complete type name. + // + // Note: Don't try to obtain TextChange for completed type name by replacing the text directly, + // then use Document.GetTextChangesAsync on document created from the changed text. This is + // because it will do a diff and return TextChanges with minimum span instead of actual + // replacement span. + // + // For example: If I'm typing "asd", the completion provider could be triggered after "a" + // is typed. Then if I selected type "AsnEncodedData" to commit, by using the approach described + // above, we will get a TextChange of "AsnEncodedDat" with 0 length span, instead of a change of + // the full display text with a span of length 1. This will later mess up span-tracking and end up + // with "AsnEncodedDatasd" in the code. + builder.Add(new TextChange(completionListSpan, completionItem.DisplayText)); + + // Then get the combined change + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newText = text.WithChanges(builder); + + return CompletionChange.Create(Utilities.Collapse(newText, builder.ToImmutableAndFree())); + + async Task ShouldCompleteWithFullyQualifyTypeName() + { + if (!IsAddingImportsSupported(document)) + { + return true; + } + + // We might need to qualify unimported types to use them in an import directive, because they only affect members of the containing + // import container (e.g. namespace/class/etc. declarations). + // + // For example, `List` and `StringBuilder` both need to be fully qualified below: + // + // using CollectionOfStringBuilders = System.Collections.Generic.List; + // + // However, if we are typing in an C# using directive that is inside a nested import container (i.e. inside a namespace declaration block), + // then we can add an using in the outer import container instead (this is not allowed in VB). + // + // For example: + // + // using System.Collections.Generic; + // using System.Text; + // + // namespace Foo + // { + // using CollectionOfStringBuilders = List; + // } + // + // Here we will always choose to qualify the unimported type, just to be consistent and keeps things simple. + return await IsInImportsDirectiveAsync(document, completionListSpan.Start, cancellationToken).ConfigureAwait(false); + } + } + + private static async Task IsInImportsDirectiveAsync(Document document, int position, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDirectives: true); + return leftToken.GetAncestor(syntaxFacts.IsUsingOrExternOrImport) != null; + } + + protected static bool IsAddingImportsSupported(Document document) + { + var workspace = document.Project.Solution.Workspace; + + // Certain types of workspace don't support document change, e.g. DebuggerIntellisense + if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) + { + return false; + } + + // During an EnC session, adding import is not supported. + var encService = workspace.Services.GetService(); + if (encService?.IsDebuggingSessionInProgress == true) + { + return false; + } + + // Certain documents, e.g. Razor document, don't support adding imports + var documentSupportsFeatureService = workspace.Services.GetRequiredService(); + if (!documentSupportsFeatureService.SupportsRefactorings(document)) + { + return false; + } + + return true; + } + + private static SyntaxNode CreateImport(Document document, string namespaceName) + { + var syntaxGenerator = SyntaxGenerator.GetGenerator(document); + return syntaxGenerator.NamespaceImportDeclaration(namespaceName).WithAdditionalAnnotations(Formatter.Annotation); + } + + protected override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + => ImportCompletionItem.GetCompletionDescriptionAsync(document, item, cancellationToken); + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs index 8786f7b551b2a7ba334d5d61463e9f5838e17464..dcdc1f9f90b69eb4b2e1c15ace9b091dd06b027c 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionProvider.cs @@ -1,107 +1,41 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.AddImports; using Microsoft.CodeAnalysis.Completion.Log; using Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion; -using Microsoft.CodeAnalysis.EditAndContinue; -using Microsoft.CodeAnalysis.Editing; -using Microsoft.CodeAnalysis.Experiments; -using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; -using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Completion.Providers { - internal abstract partial class AbstractTypeImportCompletionProvider : CommonCompletionProvider + internal abstract class AbstractTypeImportCompletionProvider : AbstractImportCompletionProvider { - private bool? _isTypeImportCompletionExperimentEnabled = null; - - protected abstract Task CreateContextAsync(Document document, int position, CancellationToken cancellationToken); - - protected abstract ImmutableArray GetImportedNamespaces( - SyntaxNode location, - SemanticModel semanticModel, - CancellationToken cancellationToken); - - protected abstract Task IsInImportsDirectiveAsync(Document document, int position, CancellationToken cancellationToken); - - internal override bool IsExpandItemProvider => true; + protected override bool ShouldProvideCompletion(Document document, SyntaxContext syntaxContext) + => syntaxContext.IsTypeContext; - public override async Task ProvideCompletionsAsync(CompletionContext completionContext) + protected override async Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet namespacesInScope, bool isExpandedCompletion, CancellationToken cancellationToken) { - var cancellationToken = completionContext.CancellationToken; - var document = completionContext.Document; - var workspace = document.Project.Solution.Workspace; - - // We need to check for context before option values, so we can tell completion service that we are in a context to provide expanded items - // even though import completion might be disabled. This would show the expander in completion list which user can then use to explicitly ask for unimported items. - var syntaxContext = await CreateContextAsync(document, completionContext.Position, cancellationToken).ConfigureAwait(false); - if (!syntaxContext.IsTypeContext) - { - return; - } - - completionContext.ExpandItemsAvailable = true; + using var _ = Logger.LogBlock(FunctionId.Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, cancellationToken); + var telemetryCounter = new TelemetryCounter(); - // We will trigger import completion regardless of the option/experiment if extended items is being requested explicitly (via expander in completion list) - var isExpandedCompletion = completionContext.Options.GetOption(CompletionServiceOptions.IsExpandedCompletion); - if (!isExpandedCompletion) - { - var importCompletionOptionValue = completionContext.Options.GetOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, document.Project.Language); - - // Don't trigger import completion if the option value is "default" and the experiment is disabled for the user. - if (importCompletionOptionValue == false || - (importCompletionOptionValue == null && !IsTypeImportCompletionExperimentEnabled(workspace))) - { - return; - } - } - - using (Logger.LogBlock(FunctionId.Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, cancellationToken)) - using (var telemetryCounter = new TelemetryCounter()) - { - await AddCompletionItemsAsync(completionContext, syntaxContext, isExpandedCompletion, telemetryCounter, cancellationToken).ConfigureAwait(false); - } - } - - private bool IsTypeImportCompletionExperimentEnabled(Workspace workspace) - { - if (!_isTypeImportCompletionExperimentEnabled.HasValue) - { - var experimentationService = workspace.Services.GetService(); - _isTypeImportCompletionExperimentEnabled = experimentationService.IsExperimentEnabled(WellKnownExperimentNames.TypeImportCompletion); - } - - return _isTypeImportCompletionExperimentEnabled == true; - } - - private async Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, bool isExpandedCompletion, TelemetryCounter telemetryCounter, CancellationToken cancellationToken) - { var document = completionContext.Document; var project = document.Project; var workspace = project.Solution.Workspace; - var typeImportCompletionService = document.GetLanguageService(); - - // Find all namespaces in scope at current cursor location, - // which will be used to filter so the provider only returns out-of-scope types. - var namespacesInScope = GetNamespacesInScope(document, syntaxContext, cancellationToken); + var typeImportCompletionService = document.GetLanguageService()!; var tasksToGetCompletionItems = ArrayBuilder>>.GetInstance(); // Get completion items from current project. - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var compilation = (await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false))!; tasksToGetCompletionItems.Add(Task.Run(() => typeImportCompletionService.GetTopLevelTypesAsync( project, syntaxContext, @@ -145,6 +79,7 @@ private async Task AddCompletionItemsAsync(CompletionContext completionContext, } telemetryCounter.ReferenceCount = referencedAssemblySymbols.Length; + telemetryCounter.Report(); return; @@ -186,7 +121,7 @@ static void AddItems(ImmutableArray items, CompletionContext com { foreach (var item in items) { - var containingNamespace = TypeImportCompletionItem.GetContainingNamespace(item); + var containingNamespace = ImportCompletionItem.GetContainingNamespace(item); if (!namespacesInScope.Contains(containingNamespace)) { // We can return cached item directly, item's span will be fixed by completion service. @@ -200,159 +135,21 @@ static void AddItems(ImmutableArray items, CompletionContext com } } - private HashSet GetNamespacesInScope(Document document, SyntaxContext syntaxContext, CancellationToken cancellationToken) + private class TelemetryCounter { - var semanticModel = syntaxContext.SemanticModel; - var importedNamespaces = GetImportedNamespaces(syntaxContext.LeftToken.Parent, semanticModel, cancellationToken); - - // This hashset will be used to match namespace names, so it must have the same case-sensitivity as the source language. - var syntaxFacts = document.GetLanguageService(); - var namespacesInScope = new HashSet(importedNamespaces, syntaxFacts.StringComparer); - - // Get containing namespaces. - var namespaceSymbol = semanticModel.GetEnclosingNamespace(syntaxContext.Position, cancellationToken); - while (namespaceSymbol != null) - { - namespacesInScope.Add(namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); - namespaceSymbol = namespaceSymbol.ContainingNamespace; - } - - return namespacesInScope; - } - - internal override async Task GetChangeAsync(Document document, CompletionItem completionItem, TextSpan completionListSpan, char? commitKey, CancellationToken cancellationToken) - { - var containingNamespace = TypeImportCompletionItem.GetContainingNamespace(completionItem); - Debug.Assert(containingNamespace != null); - - if (await ShouldCompleteWithFullyQualifyTypeName().ConfigureAwait(false)) - { - var fullyQualifiedName = $"{containingNamespace}.{completionItem.DisplayText}"; - var change = new TextChange(completionListSpan, fullyQualifiedName); - - return CompletionChange.Create(change); - } - else - { - // Find context node so we can use it to decide where to insert using/imports. - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var addImportContextNode = root.FindToken(completionListSpan.Start, findInsideTrivia: true).Parent; - - // Add required using/imports directive. - var addImportService = document.GetLanguageService(); - var optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); - var placeSystemNamespaceFirst = optionSet.GetOption(GenerationOptions.PlaceSystemNamespaceFirst, document.Project.Language); - var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var importNode = CreateImport(document, containingNamespace); - - var rootWithImport = addImportService.AddImport(compilation, root, addImportContextNode, importNode, placeSystemNamespaceFirst, cancellationToken); - var documentWithImport = document.WithSyntaxRoot(rootWithImport); - var formattedDocumentWithImport = await Formatter.FormatAsync(documentWithImport, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); - - var builder = ArrayBuilder.GetInstance(); - - // Get text change for add improt - var importChanges = await formattedDocumentWithImport.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); - builder.AddRange(importChanges); - - // Create text change for complete type name. - // - // Note: Don't try to obtain TextChange for completed type name by replacing the text directly, - // then use Document.GetTextChangesAsync on document created from the changed text. This is - // because it will do a diff and return TextChanges with minimum span instead of actual - // replacement span. - // - // For example: If I'm typing "asd", the completion provider could be triggered after "a" - // is typed. Then if I selected type "AsnEncodedData" to commit, by using the approach described - // above, we will get a TextChange of "AsnEncodedDat" with 0 length span, instead of a change of - // the full display text with a span of length 1. This will later mess up span-tracking and end up - // with "AsnEncodedDatasd" in the code. - builder.Add(new TextChange(completionListSpan, completionItem.DisplayText)); - - // Then get the combined change - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - var newText = text.WithChanges(builder); - - return CompletionChange.Create(Utilities.Collapse(newText, builder.ToImmutableAndFree())); - } - - async Task ShouldCompleteWithFullyQualifyTypeName() - { - var workspace = document.Project.Solution.Workspace; - - // Certain types of workspace don't support document change, e.g. DebuggerIntellisense - if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) - { - return true; - } - - // During an EnC session, adding import is not supported. - var encService = workspace.Services.GetService(); - if (encService?.IsDebuggingSessionInProgress == true) - { - return true; - } - - // Certain documents, e.g. Razor document, don't support adding imports - var documentSupportsFeatureService = workspace.Services.GetService(); - if (!documentSupportsFeatureService.SupportsRefactorings(document)) - { - return true; - } - - // We might need to qualify unimported types to use them in an import directive, because they only affect members of the containing - // import container (e.g. namespace/class/etc. declarations). - // - // For example, `List` and `StringBuilder` both need to be fully qualified below: - // - // using CollectionOfStringBuilders = System.Collections.Generic.List; - // - // However, if we are typing in an C# using directive that is inside a nested import container (i.e. inside a namespace declaration block), - // then we can add an using in the outer import container instead (this is not allowed in VB). - // - // For example: - // - // using System.Collections.Generic; - // using System.Text; - // - // namespace Foo - // { - // using CollectionOfStringBuilders = List; - // } - // - // Here we will always choose to qualify the unimported type, just to be consistent and keeps things simple. - return await IsInImportsDirectiveAsync(document, completionListSpan.Start, cancellationToken).ConfigureAwait(false); - } - } - - private static SyntaxNode CreateImport(Document document, string namespaceName) - { - var syntaxGenerator = SyntaxGenerator.GetGenerator(document); - return syntaxGenerator.NamespaceImportDeclaration(namespaceName).WithAdditionalAnnotations(Formatter.Annotation); - } - - protected override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CancellationToken cancellationToken) - => TypeImportCompletionItem.GetCompletionDescriptionAsync(document, item, cancellationToken); - - private class TelemetryCounter : IDisposable - { - private readonly int _tick; - + protected int Tick { get; } public int ItemsCount { get; set; } - public int ReferenceCount { get; set; } - public bool TimedOut { get; set; } public TelemetryCounter() { - _tick = Environment.TickCount; + Tick = Environment.TickCount; } - public void Dispose() + public void Report() { - var delta = Environment.TickCount - _tick; + var delta = Environment.TickCount - Tick; CompletionProvidersLogger.LogTypeImportCompletionTicksDataPoint(delta); CompletionProvidersLogger.LogTypeImportCompletionItemCountDataPoint(ItemsCount); CompletionProvidersLogger.LogTypeImportCompletionReferenceCountDataPoint(ReferenceCount); diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.CacheEntry.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.CacheEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..daeeba81c37b2afe27c4a6b8e8093cf8a03c84aa --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.CacheEntry.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion +{ + internal abstract partial class AbstractTypeImportCompletionService + { + private readonly struct CacheEntry + { + public string Language { get; } + + public Checksum Checksum { get; } + + private ImmutableArray ItemInfos { get; } + + private CacheEntry( + Checksum checksum, + string language, + ImmutableArray items) + { + Checksum = checksum; + Language = language; + + ItemInfos = items; + } + + public ImmutableArray GetItemsForContext( + string language, + string genericTypeSuffix, + bool isInternalsVisible, + bool isAttributeContext, + bool isCaseSensitive) + { + // We will need to adjust some items if the request is made in: + // 1. attribute context, then we will not show or complete with "Attribute" suffix. + // 2. a project with different langauge than when the cache entry was created, + // then we will change the generic suffix accordingly. + // Otherwise, we can simply return cached items. + var isSameLanguage = Language == language; + if (isSameLanguage && !isAttributeContext) + { + return ItemInfos.Where(info => info.IsPublic || isInternalsVisible).SelectAsArray(info => info.Item); + } + + var builder = ArrayBuilder.GetInstance(); + foreach (var info in ItemInfos) + { + if (info.IsPublic || isInternalsVisible) + { + var item = info.Item; + if (isAttributeContext) + { + if (!info.IsAttribute) + { + continue; + } + + item = GetAppropriateAttributeItem(info.Item, isCaseSensitive); + } + + if (!isSameLanguage && info.IsGeneric) + { + // We don't want to cache this item. + item = ImportCompletionItem.CreateItemWithGenericDisplaySuffix(item, genericTypeSuffix); + } + + builder.Add(item); + } + } + + return builder.ToImmutableAndFree(); + + static CompletionItem GetAppropriateAttributeItem(CompletionItem attributeItem, bool isCaseSensitive) + { + if (attributeItem.DisplayText.TryGetWithoutAttributeSuffix(isCaseSensitive: isCaseSensitive, out var attributeNameWithoutSuffix)) + { + // We don't want to cache this item. + return ImportCompletionItem.CreateAttributeItemWithoutSuffix(attributeItem, attributeNameWithoutSuffix); + } + + return attributeItem; + } + } + + public class Builder : IDisposable + { + private readonly string _language; + private readonly string _genericTypeSuffix; + private readonly Checksum _checksum; + + private readonly ArrayBuilder _itemsBuilder; + + public Builder(Checksum checksum, string language, string genericTypeSuffix) + { + _checksum = checksum; + _language = language; + _genericTypeSuffix = genericTypeSuffix; + + _itemsBuilder = ArrayBuilder.GetInstance(); + } + + public CacheEntry ToReferenceCacheEntry() + { + return new CacheEntry( + _checksum, + _language, + _itemsBuilder.ToImmutable()); + } + + public void AddItem(INamedTypeSymbol symbol, string containingNamespace, bool isPublic) + { + var isGeneric = symbol.Arity > 0; + + // Need to determine if a type is an attribute up front since we want to filter out + // non-attribute types when in attribute context. We can't do this lazily since we don't hold + // on to symbols. However, the cost of calling `IsAttribute` on every top-level type symbols + // is prohibitively high, so we opt for the heuristic that would do the simple textual "Attribute" + // suffix check first, then the more expensive symbolic check. As a result, all unimported + // attribute types that don't have "Attribute" suffix would be filtered out when in attribute context. + var isAttribute = symbol.Name.HasAttributeSuffix(isCaseSensitive: false) && symbol.IsAttribute(); + + var item = ImportCompletionItem.Create(symbol, containingNamespace, _genericTypeSuffix); + _itemsBuilder.Add(new TypeImportCompletionItemInfo(item, isPublic, isGeneric, isAttribute)); + } + + public void Dispose() + => _itemsBuilder.Free(); + } + } + + [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] + private sealed class CacheServiceFactory : AbstractImportCompletionCacheServiceFactory + { + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs index 95477b4249827d11e5e9504988e37718b3a8046f..c8758842c9c1f8dd790831f7b6d8441089fd12f5 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs @@ -1,23 +1,22 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion { internal abstract partial class AbstractTypeImportCompletionService : ITypeImportCompletionService { - private ITypeImportCompletionCacheService CacheService { get; } + private IImportCompletionCacheService CacheService { get; } protected abstract string GenericTypeSuffix { get; } @@ -25,7 +24,7 @@ internal abstract partial class AbstractTypeImportCompletionService : ITypeImpor internal AbstractTypeImportCompletionService(Workspace workspace) { - CacheService = workspace.Services.GetService(); + CacheService = workspace.Services.GetRequiredService>(); } public async Task> GetTopLevelTypesAsync( @@ -39,7 +38,7 @@ internal AbstractTypeImportCompletionService(Workspace workspace) throw new ArgumentException(nameof(project)); } - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); // Since we only need top level types from source, therefore we only care if source symbol checksum changes. var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false); @@ -97,7 +96,24 @@ static string GetReferenceKey(PortableExecutableReference reference) Checksum checksum, SyntaxContext syntaxContext, bool isInternalsVisible, - IDictionary cache, + IDictionary cache, + CancellationToken cancellationToken) + { + var cacheEntry = GetCacheEntry(key, assembly, checksum, syntaxContext, cache, cancellationToken); + return cacheEntry.GetItemsForContext( + syntaxContext.SemanticModel.Language, + GenericTypeSuffix, + isInternalsVisible, + syntaxContext.IsAttributeNameContext, + IsCaseSensitive); + } + + private CacheEntry GetCacheEntry( + TKey key, + IAssemblySymbol assembly, + Checksum checksum, + SyntaxContext syntaxContext, + IDictionary cache, CancellationToken cancellationToken) { var language = syntaxContext.SemanticModel.Language; @@ -106,23 +122,18 @@ static string GetReferenceKey(PortableExecutableReference reference) if (!cache.TryGetValue(key, out var cacheEntry) || cacheEntry.Checksum != checksum) { - var builder = new ReferenceCacheEntry.Builder(checksum, language, GenericTypeSuffix); + using var builder = new CacheEntry.Builder(checksum, language, GenericTypeSuffix); GetCompletionItemsForTopLevelTypeDeclarations(assembly.GlobalNamespace, builder, cancellationToken); cacheEntry = builder.ToReferenceCacheEntry(); cache[key] = cacheEntry; } - return cacheEntry.GetItemsForContext( - language, - GenericTypeSuffix, - isInternalsVisible, - syntaxContext.IsAttributeNameContext, - IsCaseSensitive); + return cacheEntry; } private static void GetCompletionItemsForTopLevelTypeDeclarations( INamespaceSymbol rootNamespaceSymbol, - ReferenceCacheEntry.Builder builder, + CacheEntry.Builder builder, CancellationToken cancellationToken) { VisitNamespace(rootNamespaceSymbol, containingNamespace: null, builder, cancellationToken); @@ -130,12 +141,12 @@ static string GetReferenceKey(PortableExecutableReference reference) static void VisitNamespace( INamespaceSymbol symbol, - string containingNamespace, - ReferenceCacheEntry.Builder builder, + string? containingNamespace, + CacheEntry.Builder builder, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - containingNamespace = ConcatNamespace(containingNamespace, symbol.Name); + containingNamespace = CompletionHelper.ConcatNamespace(containingNamespace, symbol.Name); foreach (var memberNamespace in symbol.GetNamespaceMembers()) { @@ -186,17 +197,6 @@ static string GetReferenceKey(PortableExecutableReference reference) } } - private static string ConcatNamespace(string containingNamespace, string name) - { - Debug.Assert(name != null); - if (string.IsNullOrEmpty(containingNamespace)) - { - return name; - } - - return containingNamespace + "." + name; - } - private readonly struct TypeOverloadInfo { public TypeOverloadInfo(INamedTypeSymbol nonGenericOverload, INamedTypeSymbol bestGenericOverload, bool containsPublicGenericOverload) @@ -231,121 +231,6 @@ public TypeOverloadInfo Aggregate(INamedTypeSymbol type) } } - private readonly struct ReferenceCacheEntry - { - public class Builder - { - private readonly string _language; - private readonly string _genericTypeSuffix; - private readonly Checksum _checksum; - - private readonly ArrayBuilder _itemsBuilder; - - public Builder(Checksum checksum, string language, string genericTypeSuffix) - { - _checksum = checksum; - _language = language; - _genericTypeSuffix = genericTypeSuffix; - - _itemsBuilder = ArrayBuilder.GetInstance(); - } - - public ReferenceCacheEntry ToReferenceCacheEntry() - { - return new ReferenceCacheEntry( - _checksum, - _language, - _itemsBuilder.ToImmutableAndFree()); - } - - public void AddItem(INamedTypeSymbol symbol, string containingNamespace, bool isPublic) - { - var isGeneric = symbol.Arity > 0; - - // Need to determine if a type is an attribute up front since we want to filter out - // non-attribute types when in attribute context. We can't do this lazily since we don't hold - // on to symbols. However, the cost of calling `IsAttribute` on every top-level type symbols - // is prohibitively high, so we opt for the heuristic that would do the simple textual "Attribute" - // suffix check first, then the more expensive symbolic check. As a result, all unimported - // attribute types that don't have "Attribute" suffix would be filtered out when in attribute context. - var isAttribute = symbol.Name.HasAttributeSuffix(isCaseSensitive: false) && symbol.IsAttribute(); - - var item = TypeImportCompletionItem.Create(symbol, containingNamespace, _genericTypeSuffix); - _itemsBuilder.Add(new TypeImportCompletionItemInfo(item, isPublic, isGeneric, isAttribute)); - } - } - - private ReferenceCacheEntry( - Checksum checksum, - string language, - ImmutableArray items) - { - Checksum = checksum; - Language = language; - - ItemInfos = items; - } - - public string Language { get; } - - public Checksum Checksum { get; } - - private ImmutableArray ItemInfos { get; } - - public ImmutableArray GetItemsForContext( - string language, - string genericTypeSuffix, - bool isInternalsVisible, - bool isAttributeContext, - bool isCaseSensitive) - { - var isSameLanguage = Language == language; - if (isSameLanguage && !isAttributeContext) - { - return ItemInfos.Where(info => info.IsPublic || isInternalsVisible).SelectAsArray(info => info.Item); - } - - var builder = ArrayBuilder.GetInstance(); - foreach (var info in ItemInfos) - { - if (info.IsPublic || isInternalsVisible) - { - var item = info.Item; - if (isAttributeContext) - { - if (!info.IsAttribute) - { - continue; - } - - item = GetAppropriateAttributeItem(info.Item, isCaseSensitive); - } - - if (!isSameLanguage && info.IsGeneric) - { - // We don't want to cache this item. - item = TypeImportCompletionItem.CreateItemWithGenericDisplaySuffix(item, genericTypeSuffix); - } - - builder.Add(item); - } - } - - return builder.ToImmutableAndFree(); - - static CompletionItem GetAppropriateAttributeItem(CompletionItem attributeItem, bool isCaseSensitive) - { - if (attributeItem.DisplayText.TryGetWithoutAttributeSuffix(isCaseSensitive: isCaseSensitive, out var attributeNameWithoutSuffix)) - { - // We don't want to cache this item. - return TypeImportCompletionItem.CreateAttributeItemWithoutSuffix(attributeItem, attributeNameWithoutSuffix); - } - - return attributeItem; - } - } - } - private readonly struct TypeImportCompletionItemInfo { private readonly ItemPropertyKind _properties; diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService_CacheService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService_CacheService.cs deleted file mode 100644 index af29a65777f2e605f80e16cbeec5102d3b4ae6cd..0000000000000000000000000000000000000000 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService_CacheService.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion -{ - internal partial class AbstractTypeImportCompletionService - { - private interface ITypeImportCompletionCacheService : IWorkspaceService - { - // PE references are keyed on assembly path. - IDictionary PEItemsCache { get; } - - IDictionary ProjectItemsCache { get; } - } - - [ExportWorkspaceServiceFactory(typeof(ITypeImportCompletionCacheService), ServiceLayer.Editor), Shared] - private class TypeImportCompletionCacheServiceFactory : IWorkspaceServiceFactory - { - private readonly ConcurrentDictionary _peItemsCache - = new ConcurrentDictionary(); - - private readonly ConcurrentDictionary _projectItemsCache - = new ConcurrentDictionary(); - - [ImportingConstructor] - public TypeImportCompletionCacheServiceFactory() - { - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - { - var workspace = workspaceServices.Workspace; - if (workspace.Kind == WorkspaceKind.Host) - { - var cacheService = workspaceServices.GetService(); - if (cacheService != null) - { - cacheService.CacheFlushRequested += OnCacheFlushRequested; - } - } - - return new TypeImportCompletionCacheService(_peItemsCache, _projectItemsCache); - } - - private void OnCacheFlushRequested(object sender, EventArgs e) - { - _peItemsCache.Clear(); - _projectItemsCache.Clear(); - } - - private class TypeImportCompletionCacheService : ITypeImportCompletionCacheService - { - public IDictionary PEItemsCache { get; } - - public IDictionary ProjectItemsCache { get; } - - public TypeImportCompletionCacheService( - ConcurrentDictionary peCache, - ConcurrentDictionary projectCache) - { - PEItemsCache = peCache; - ProjectItemsCache = projectCache; - } - } - } - - } -} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.CacheEntry.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.CacheEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..eba893a7155591d57ef53f9e241f8e7ac4dcf93c --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.CacheEntry.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Completion.Providers +{ + internal static partial class ExtensionMethodImportCompletionHelper + { + private readonly struct CacheEntry + { + public Checksum Checksum { get; } + public string Language { get; } + + /// + /// Mapping from the name of target type to extension method symbol infos. + /// + public readonly MultiDictionary SimpleExtensionMethodInfo { get; } + + public readonly ImmutableArray ComplexExtensionMethodInfo { get; } + + private CacheEntry( + Checksum checksum, + string language, + MultiDictionary simpleExtensionMethodInfo, + ImmutableArray complexExtensionMethodInfo) + { + Checksum = checksum; + Language = language; + SimpleExtensionMethodInfo = simpleExtensionMethodInfo; + ComplexExtensionMethodInfo = complexExtensionMethodInfo; + } + + public class Builder : IDisposable + { + private readonly Checksum _checksum; + private readonly string _language; + + private readonly MultiDictionary _simpleItemBuilder; + private readonly ArrayBuilder _complexItemBuilder; + + public Builder(Checksum checksum, string langauge, IEqualityComparer comparer) + { + _checksum = checksum; + _language = langauge; + + _simpleItemBuilder = new MultiDictionary(comparer); + _complexItemBuilder = ArrayBuilder.GetInstance(); + } + + public CacheEntry ToCacheEntry() + { + return new CacheEntry( + _checksum, + _language, + _simpleItemBuilder, + _complexItemBuilder.ToImmutable()); + } + + public void AddItem(SyntaxTreeIndex syntaxIndex) + { + foreach (var (targetType, symbolInfoIndices) in syntaxIndex.SimpleExtensionMethodInfo) + { + foreach (var index in symbolInfoIndices) + { + _simpleItemBuilder.Add(targetType, syntaxIndex.DeclaredSymbolInfos[index]); + } + } + + foreach (var index in syntaxIndex.ComplexExtensionMethodInfo) + { + _complexItemBuilder.Add(syntaxIndex.DeclaredSymbolInfos[index]); + } + } + + public void Dispose() + => _complexItemBuilder.Free(); + } + } + + /// + /// We don't use PE cache from the service, so just pass in type `object` for PE entries. + /// + [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] + private sealed class CacheServiceFactory : AbstractImportCompletionCacheServiceFactory + { + } + + private static IImportCompletionCacheService GetCacheService(Workspace workspace) + => workspace.Services.GetRequiredService>(); + + private static async Task GetCacheEntryAsync( + Project project, + bool loadOnly, + IImportCompletionCacheService cacheService, + CancellationToken cancellationToken) + { + // While we are caching data from SyntaxTreeInfo, all the things we cared about here are actually based on sources symbols. + // So using source symbol checksum would suffice. + var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false); + + // Cache miss, create all requested items. + if (!cacheService.ProjectItemsCache.TryGetValue(project.Id, out var cacheEntry) || + cacheEntry.Checksum != checksum || + cacheEntry.Language != project.Language) + { + var syntaxFacts = project.LanguageServices.GetRequiredService(); + using var builder = new CacheEntry.Builder(checksum, project.Language, syntaxFacts.StringComparer); + + foreach (var document in project.Documents) + { + // Don't look for extension methods in generated code. + if (document.State.Attributes.IsGenerated) + { + continue; + } + + var info = await document.GetSyntaxTreeIndexAsync(loadOnly, cancellationToken).ConfigureAwait(false); + if (info == null) + { + return null; + } + + if (info.ContainsExtensionMethod) + { + builder.AddItem(info); + } + } + + cacheEntry = builder.ToCacheEntry(); + cacheService.ProjectItemsCache[project.Id] = cacheEntry; + } + + return cacheEntry; + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..df195a4733f83f686d4d7d7417a4411e1cf091d6 --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs @@ -0,0 +1,491 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion.Log; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Completion.Providers +{ + /// + /// Provides completion items for extension methods from unimported namespace. + /// + /// It runs out-of-proc if it's enabled + internal static partial class ExtensionMethodImportCompletionHelper + { + private static readonly char[] s_dotSeparator = new char[] { '.' }; + + private static readonly object s_gate = new object(); + private static Task s_indexingTask = Task.CompletedTask; + + public static async Task> GetUnimportedExtensionMethodsAsync( + Document document, + int position, + ITypeSymbol receiverTypeSymbol, + ISet namespaceInScope, + bool forceIndexCreation, + CancellationToken cancellationToken) + { + var ticks = Environment.TickCount; + var project = document.Project; + + // This service is only defined for C# and VB, but we'll be a bit paranoid. + var client = RemoteSupportedLanguages.IsSupported(project.Language) + ? await project.Solution.Workspace.TryGetRemoteHostClientAsync(cancellationToken).ConfigureAwait(false) + : null; + + var (serializableItems, counter) = client == null + ? await GetUnimportedExtensionMethodsInCurrentProcessAsync(document, position, receiverTypeSymbol, namespaceInScope, forceIndexCreation, cancellationToken).ConfigureAwait(false) + : await GetUnimportedExtensionMethodsInRemoteProcessAsync(client, document, position, receiverTypeSymbol, namespaceInScope, forceIndexCreation, cancellationToken).ConfigureAwait(false); + + counter.TotalTicks = Environment.TickCount - ticks; + counter.TotalExtensionMethodsProvided = serializableItems.Length; + counter.Report(); + + return serializableItems; + } + + public static async Task<(ImmutableArray, StatisticCounter)> GetUnimportedExtensionMethodsInRemoteProcessAsync( + RemoteHostClient client, + Document document, + int position, + ITypeSymbol receiverTypeSymbol, + ISet namespaceInScope, + bool forceIndexCreation, + CancellationToken cancellationToken) + { + var project = document.Project; + var (serializableItems, counter) = await client.TryRunCodeAnalysisRemoteAsync<(IList, StatisticCounter)>( + project.Solution, + nameof(IRemoteExtensionMethodImportCompletionService.GetUnimportedExtensionMethodsAsync), + new object[] { document.Id, position, SymbolKey.CreateString(receiverTypeSymbol), namespaceInScope.ToArray(), forceIndexCreation }, + cancellationToken).ConfigureAwait(false); + + return (serializableItems.ToImmutableArray(), counter); + } + + public static async Task<(ImmutableArray, StatisticCounter)> GetUnimportedExtensionMethodsInCurrentProcessAsync( + Document document, + int position, + ITypeSymbol receiverTypeSymbol, + ISet namespaceInScope, + bool forceIndexCreation, + CancellationToken cancellationToken) + { + var counter = new StatisticCounter(); + var ticks = Environment.TickCount; + + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + + // Get the metadata name of all the base types and interfaces this type derived from. + using var _ = PooledHashSet.GetInstance(out var allTypeNamesBuilder); + allTypeNamesBuilder.Add(receiverTypeSymbol.MetadataName); + allTypeNamesBuilder.AddRange(receiverTypeSymbol.GetBaseTypes().Select(t => t.MetadataName)); + allTypeNamesBuilder.AddRange(receiverTypeSymbol.GetAllInterfacesIncludingThis().Select(t => t.MetadataName)); + + // interface doesn't inherit from object, but is implicitly convertable to object type. + if (receiverTypeSymbol.IsInterfaceType()) + { + allTypeNamesBuilder.Add(nameof(Object)); + } + + var allTypeNames = allTypeNamesBuilder.ToImmutableArray(); + var indicesResult = await TryGetIndicesAsync( + document.Project, forceIndexCreation, cancellationToken).ConfigureAwait(false); + + // Don't show unimported extension methods if the index isn't ready. + if (!indicesResult.HasResult) + { + // We use a very simple approach to build the cache in the background: + // queue a new task only if the previous task is completed, regardless of what + // that task is. + lock (s_gate) + { + if (s_indexingTask.IsCompleted) + { + s_indexingTask = Task.Run(() => TryGetIndicesAsync(document.Project, forceIndexCreation: true, CancellationToken.None)); + } + } + + return (ImmutableArray.Empty, counter); + } + + var matchedMethods = CreateAggregatedFilter(allTypeNames, indicesResult.SyntaxIndices, indicesResult.SymbolInfos); + + counter.GetFilterTicks = Environment.TickCount - ticks; + counter.NoFilter = !indicesResult.HasResult; + + ticks = Environment.TickCount; + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var items = GetExtensionMethodItems(compilation.GlobalNamespace, receiverTypeSymbol, + semanticModel!, position, namespaceInScope, matchedMethods, counter, cancellationToken); + + counter.GetSymbolTicks = Environment.TickCount - ticks; + + return (items, counter); + } + + private static async Task TryGetIndicesAsync( + Project currentProject, + bool forceIndexCreation, + CancellationToken cancellationToken) + { + var solution = currentProject.Solution; + var cacheService = GetCacheService(solution.Workspace); + var graph = currentProject.Solution.GetProjectDependencyGraph(); + var relevantProjectIds = graph.GetProjectsThatThisProjectTransitivelyDependsOn(currentProject.Id) + .Concat(currentProject.Id); + + using var syntaxDisposer = ArrayBuilder.GetInstance(out var syntaxBuilder); + using var symbolDisposer = ArrayBuilder.GetInstance(out var symbolBuilder); + + foreach (var projectId in relevantProjectIds) + { + var project = solution.GetProject(projectId); + if (project == null || !project.SupportsCompilation) + { + continue; + } + + // By default, don't trigger index creation except for documents in current project. + var loadOnly = !forceIndexCreation && projectId != currentProject.Id; + var cacheEntry = await GetCacheEntryAsync(project, loadOnly, cacheService, cancellationToken).ConfigureAwait(false); + + if (cacheEntry == null) + { + // Don't provide anything if we don't have all the required SyntaxTreeIndex created. + return GetIndicesResult.NoneResult; + } + + syntaxBuilder.Add(cacheEntry.Value); + } + + // Search through all direct PE references. + foreach (var peReference in currentProject.MetadataReferences.OfType()) + { + var info = await SymbolTreeInfo.GetInfoForMetadataReferenceAsync( + solution, peReference, loadOnly: !forceIndexCreation, cancellationToken).ConfigureAwait(false); + + if (info == null) + { + // Don't provide anything if we don't have all the required SymbolTreeInfo created. + return GetIndicesResult.NoneResult; + } + + if (info.ContainsExtensionMethod) + { + symbolBuilder.Add(info); + } + } + + var syntaxIndices = syntaxBuilder.ToImmutable(); + var symbolInfos = symbolBuilder.ToImmutable(); + + return new GetIndicesResult(hasResult: true, syntaxIndices, symbolInfos); + } + + private static MultiDictionary CreateAggregatedFilter(ImmutableArray targetTypeNames, ImmutableArray syntaxIndices, ImmutableArray symbolInfos) + { + var results = new MultiDictionary(); + + // Find matching extension methods from source. + foreach (var index in syntaxIndices) + { + // Add simple extension methods with matching target type name + foreach (var targetTypeName in targetTypeNames) + { + var methodInfos = index.SimpleExtensionMethodInfo[targetTypeName]; + if (methodInfos.Count == 0) + { + continue; + } + + foreach (var methodInfo in methodInfos) + { + results.Add(methodInfo.FullyQualifiedContainerName, methodInfo.Name); + } + } + + // Add all complex extension methods, we will need to completely rely on symbols to match them. + foreach (var methodInfo in index.ComplexExtensionMethodInfo) + { + results.Add(methodInfo.FullyQualifiedContainerName, methodInfo.Name); + } + } + + // Find matching extension methods from metadata + foreach (var info in symbolInfos) + { + var methodInfos = info.GetMatchingExtensionMethodInfo(targetTypeNames); + foreach (var methodInfo in methodInfos) + { + results.Add(methodInfo.FullyQualifiedContainerName, methodInfo.Name); + } + } + + return results; + } + + private static ImmutableArray GetExtensionMethodItems( + INamespaceSymbol rootNamespaceSymbol, + ITypeSymbol receiverTypeSymbol, + SemanticModel semanticModel, + int position, + ISet namespaceFilter, + MultiDictionary methodNameFilter, + StatisticCounter counter, + CancellationToken cancellationToken) + { + var compilation = semanticModel.Compilation; + using var _ = ArrayBuilder.GetInstance(out var builder); + + using var conflictTypeRootNode = new ConflictNameNode(name: string.Empty); + + foreach (var (fullyQualifiedContainerName, methodNames) in methodNameFilter) + { + cancellationToken.ThrowIfCancellationRequested(); + + var indexOfLastDot = fullyQualifiedContainerName.LastIndexOf('.'); + var qualifiedNamespaceName = indexOfLastDot > 0 ? fullyQualifiedContainerName.Substring(0, indexOfLastDot) : string.Empty; + + if (namespaceFilter.Contains(qualifiedNamespaceName)) + { + continue; + } + + // Container of extension method (static class in C# and Module in VB) can't be generic or nested. + // Note that we might incorrectly ignore valid types, because, for example, calling `GetTypeByMetadataName` + // would return null if we have multiple definitions of a type even though only one is accessible from here + // (e.g. an internal type declared inside a shared document). In this case, we have to handle the conflicted + // types explicitly here. + // + // TODO: + // Alternatively, we can try to get symbols out of each assembly, instead of the compilation (which includes all references), + // to avoid such conflict. We should give this approach a try and see if any perf improvement can be archieved. + var containerSymbol = compilation.GetTypeByMetadataName(fullyQualifiedContainerName); + + if (containerSymbol != null) + { + GetItemsFromTypeContainsPotentialMatches(containerSymbol, qualifiedNamespaceName, methodNames, receiverTypeSymbol, semanticModel, position, counter, builder); + } + else + { + conflictTypeRootNode.Add(fullyQualifiedContainerName, (qualifiedNamespaceName, methodNames)); + } + } + + var ticks = Environment.TickCount; + + GetItemsFromConflictingTypes(rootNamespaceSymbol, conflictTypeRootNode, builder, receiverTypeSymbol, semanticModel, position, counter); + + counter.GetSymbolExtraTicks = Environment.TickCount - ticks; + + return builder.ToImmutable(); + } + + private static void GetItemsFromTypeContainsPotentialMatches( + INamedTypeSymbol containerSymbol, + string qualifiedNamespaceName, + MultiDictionary.ValueSet methodNames, + ITypeSymbol receiverTypeSymbol, + SemanticModel semanticModel, + int position, + StatisticCounter counter, + ArrayBuilder builder) + { + counter.TotalTypesChecked++; + + if (containerSymbol == null || + !containerSymbol.MightContainExtensionMethods || + !IsSymbolAccessible(containerSymbol, position, semanticModel)) + { + return; + } + + foreach (var methodName in methodNames) + { + var methodSymbols = containerSymbol.GetMembers(methodName).OfType(); + + foreach (var methodSymbol in methodSymbols) + { + counter.TotalExtensionMethodsChecked++; + IMethodSymbol? reducedMethodSymbol = null; + + if (methodSymbol.IsExtensionMethod && + IsSymbolAccessible(methodSymbol, position, semanticModel)) + { + reducedMethodSymbol = methodSymbol.ReduceExtensionMethod(receiverTypeSymbol); + } + + if (reducedMethodSymbol != null) + { + var symbolKeyData = SymbolKey.CreateString(reducedMethodSymbol); + builder.Add(new SerializableImportCompletionItem( + symbolKeyData, + reducedMethodSymbol.Name, + reducedMethodSymbol.Arity, + reducedMethodSymbol.GetGlyph(), + qualifiedNamespaceName)); + } + } + } + } + + private static void GetItemsFromConflictingTypes( + INamespaceSymbol containingNamespaceSymbol, + ConflictNameNode conflictTypeNodes, + ArrayBuilder builder, + ITypeSymbol receiverTypeSymbol, + SemanticModel semanticModel, + int position, + StatisticCounter counter) + { + Debug.Assert(!conflictTypeNodes.NamespaceAndMethodNames.HasValue); + + foreach (var child in conflictTypeNodes.Children.Values) + { + if (child.NamespaceAndMethodNames == null) + { + var childNamespace = containingNamespaceSymbol.GetMembers(child.Name).OfType().FirstOrDefault(); + if (childNamespace != null) + { + GetItemsFromConflictingTypes(childNamespace, child, builder, receiverTypeSymbol, semanticModel, position, counter); + } + } + else + { + var types = containingNamespaceSymbol.GetMembers(child.Name).OfType(); + foreach (var type in types) + { + var (namespaceName, methodNames) = child.NamespaceAndMethodNames.Value; + GetItemsFromTypeContainsPotentialMatches(type, namespaceName, methodNames, receiverTypeSymbol, semanticModel, position, counter, builder); + } + } + } + } + + // We only call this when the containing symbol is accessible, + // so being declared as public means this symbol is also accessible. + private static bool IsSymbolAccessible(ISymbol symbol, int position, SemanticModel semanticModel) + => symbol.DeclaredAccessibility == Accessibility.Public || semanticModel.IsAccessible(position, symbol); + + /// + /// The purpose of this is to help us keeping track of conflicting types with data required to create + /// corresponding items in a tree, which is easy to use while navigating symbol tree recursively. + /// For example, two internal classes with identical fully qualified name but declared in two different + /// projects would be a conflict, even if only one is accessible from project that triggered the completion. + /// + private class ConflictNameNode : IDisposable + { + /// + /// Holds the name of either a namespace name or a type (which is causing conflict). + /// + public string Name { get; } + + /// + /// Child nodes. Only used when this node is a namespace node. + /// + public PooledDictionary Children { get; } + + /// + /// Data needed to create a completion item based on a symbol. Not null only when this node is a type node. + /// + public (string namespaceName, MultiDictionary.ValueSet methodNames)? NamespaceAndMethodNames { get; private set; } + + public ConflictNameNode(string name) + { + Name = name; + Children = PooledDictionary.GetInstance(); + } + + public void Add(string fullyQualifiedContainerName, (string namespaceName, MultiDictionary.ValueSet methodNames) namespaceAndMethodNames) + { + var parts = fullyQualifiedContainerName.Split(s_dotSeparator); + + var current = this; + foreach (var part in parts) + { + if (!current.Children.TryGetValue(part, out var child)) + { + child = new ConflictNameNode(part); + current.Children.Add(part, child); + } + + current = child; + } + + // Type and Namespace can't have identical name + Debug.Assert(current.Children.Count == 0); + current.NamespaceAndMethodNames = namespaceAndMethodNames; + } + + public void Dispose() + { + foreach (var childNode in Children.Values) + { + childNode.Dispose(); + } + + Children.Free(); + } + } + + private readonly struct GetIndicesResult + { + public bool HasResult { get; } + public ImmutableArray SyntaxIndices { get; } + public ImmutableArray SymbolInfos { get; } + + public GetIndicesResult(bool hasResult, ImmutableArray syntaxIndices = default, ImmutableArray symbolInfos = default) + { + HasResult = hasResult; + SyntaxIndices = syntaxIndices; + SymbolInfos = symbolInfos; + } + + public static GetIndicesResult NoneResult => new GetIndicesResult(hasResult: false); + } + } + + internal sealed class StatisticCounter + { + public bool NoFilter; + public int TotalTicks; + public int TotalExtensionMethodsProvided; + public int GetFilterTicks; + public int GetSymbolTicks; + public int GetSymbolExtraTicks; + public int TotalTypesChecked; + public int TotalExtensionMethodsChecked; + + public void Report() + { + if (NoFilter) + { + CompletionProvidersLogger.LogExtensionMethodCompletionSuccess(); + } + else + { + CompletionProvidersLogger.LogExtensionMethodCompletionTicksDataPoint(TotalTicks); + CompletionProvidersLogger.LogExtensionMethodCompletionMethodsProvidedDataPoint(TotalExtensionMethodsProvided); + CompletionProvidersLogger.LogExtensionMethodCompletionGetFilterTicksDataPoint(GetFilterTicks); + CompletionProvidersLogger.LogExtensionMethodCompletionGetSymbolTicksDataPoint(GetSymbolTicks); + CompletionProvidersLogger.LogExtensionMethodCompletionTypesCheckedDataPoint(TotalTypesChecked); + CompletionProvidersLogger.LogExtensionMethodCompletionMethodsCheckedDataPoint(TotalExtensionMethodsChecked); + } + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs new file mode 100644 index 0000000000000000000000000000000000000000..131b49ba1c8b7174b21115fc8eb63dfb5dbe859c --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion +{ + internal interface IImportCompletionCacheService : IWorkspaceService + { + // PE references are keyed on assembly path. + IDictionary PEItemsCache { get; } + + IDictionary ProjectItemsCache { get; } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IRemoteExtensionMethodImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IRemoteExtensionMethodImportCompletionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..ca69194e51185ead4f0fabb3438d0e43f5c4ea63 --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IRemoteExtensionMethodImportCompletionService.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Completion.Providers +{ + internal interface IRemoteExtensionMethodImportCompletionService + { + Task<(IList, StatisticCounter)> GetUnimportedExtensionMethodsAsync( + DocumentId documentId, + int position, + string receiverTypeSymbolKeyData, + string[] namespaceInScope, + bool forceIndexCreation, + CancellationToken cancellationToken); + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs index 2de348f751c89f5387b8018be869e575ab5befc6..efaeeb65eec09003bce5599c518676a8bef0408f 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ITypeImportCompletionService.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/TypeImportCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs similarity index 53% rename from src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/TypeImportCompletionItem.cs rename to src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs index 4872a52c19158ce4cad950dcf6b2c77b4ede3f61..1c8b9ff731c6d78ba0d24facd1aca80829fbe22d 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/TypeImportCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs @@ -1,31 +1,48 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + +using System.Collections.Immutable; using System.Diagnostics; -using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.Completion.Providers { - internal static class TypeImportCompletionItem + internal static class ImportCompletionItem { private const string SortTextFormat = "~{0} {1}"; - private const string GenericTypeNameManglingString = "`"; - private static readonly string[] s_aritySuffixesOneToNine = { "`1", "`2", "`3", "`4", "`5", "`6", "`7", "`8", "`9" }; private const string TypeAritySuffixName = nameof(TypeAritySuffixName); private const string AttributeFullName = nameof(AttributeFullName); + private const string SymbolKeyData = nameof(SymbolKeyData); public static CompletionItem Create(INamedTypeSymbol typeSymbol, string containingNamespace, string genericTypeSuffix) + => Create(typeSymbol.Name, typeSymbol.Arity, containingNamespace, typeSymbol.GetGlyph(), genericTypeSuffix, CompletionItemFlags.CachedAndExpanded, symbolKeyData: null); + + public static CompletionItem Create(string name, int arity, string containingNamespace, Glyph glyph, string genericTypeSuffix, CompletionItemFlags flags, string? symbolKeyData) { - PooledDictionary propertyBuilder = null; + ImmutableDictionary? properties = null; - if (typeSymbol.Arity > 0) + if (symbolKeyData != null || arity > 0) { - propertyBuilder = PooledDictionary.GetInstance(); - propertyBuilder.Add(TypeAritySuffixName, GetAritySuffix(typeSymbol.Arity)); + var builder = PooledDictionary.GetInstance(); + + if (symbolKeyData != null) + { + builder.Add(SymbolKeyData, symbolKeyData); + } + else + { + // We don't need arity to recover symbol if we already have SymbolKeyData or it's 0. + // (but it still needed below to decide whether to show generic suffix) + builder.Add(TypeAritySuffixName, AbstractDeclaredSymbolInfoFactoryService.GetMetadataAritySuffix(arity)); + } + + properties = builder.ToImmutableDictionaryAndFree(); } // Add tildes (ASCII: 126) to name and namespace as sort text: @@ -33,19 +50,19 @@ public static CompletionItem Create(INamedTypeSymbol typeSymbol, string containi // 2. ' ' before namespace makes types with identical type name but from different namespace all show up in the list, // it also makes sure type with shorter name shows first, e.g. 'SomeType` before 'SomeTypeWithLongerName'. var sortTextBuilder = PooledStringBuilder.GetInstance(); - sortTextBuilder.Builder.AppendFormat(SortTextFormat, typeSymbol.Name, containingNamespace); + sortTextBuilder.Builder.AppendFormat(SortTextFormat, name, containingNamespace); var item = CompletionItem.Create( - displayText: typeSymbol.Name, + displayText: name, sortText: sortTextBuilder.ToStringAndFree(), - properties: propertyBuilder?.ToImmutableDictionaryAndFree(), - tags: GlyphTags.GetTags(typeSymbol.GetGlyph()), + properties: properties, + tags: GlyphTags.GetTags(glyph), rules: CompletionItemRules.Default, displayTextPrefix: null, - displayTextSuffix: typeSymbol.Arity == 0 ? string.Empty : genericTypeSuffix, + displayTextSuffix: arity == 0 ? string.Empty : genericTypeSuffix, inlineDescription: containingNamespace); - item.Flags = CompletionItemFlags.CachedAndExpanded; + item.Flags = flags; return item; } @@ -71,63 +88,58 @@ public static CompletionItem CreateAttributeItemWithoutSuffix(CompletionItem att } public static CompletionItem CreateItemWithGenericDisplaySuffix(CompletionItem item, string genericTypeSuffix) - { - return item.WithDisplayTextSuffix(genericTypeSuffix); - } + => item.WithDisplayTextSuffix(genericTypeSuffix); public static string GetContainingNamespace(CompletionItem item) => item.InlineDescription; public static async Task GetCompletionDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) { - var metadataName = GetMetadataName(item); - if (!string.IsNullOrEmpty(metadataName)) + var compilation = (await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false)); + var symbol = GetSymbol(item, compilation); + + if (symbol != null) { - var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var symbol = compilation.GetTypeByMetadataName(metadataName); - if (symbol != null) - { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - // We choose not to display the number of "type overloads" for simplicity. - // Otherwise, we need additional logic to track internal and public visible - // types separately, and cache both completion items. - return await CommonCompletionUtilities.CreateDescriptionAsync( - document.Project.Solution.Workspace, - semanticModel, - position: 0, - symbol, - overloadCount: 0, - supportedPlatforms: null, - cancellationToken).ConfigureAwait(false); - } + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + // We choose not to display the number of "type overloads" for simplicity. + // Otherwise, we need additional logic to track internal and public visible + // types separately, and cache both completion items. + return await CommonCompletionUtilities.CreateDescriptionAsync( + document.Project.Solution.Workspace, + semanticModel, + position: 0, + symbol, + overloadCount: 0, + supportedPlatforms: null, + cancellationToken).ConfigureAwait(false); } return CompletionDescription.Empty; } - private static string GetAritySuffix(int arity) - { - Debug.Assert(arity > 0); - return (arity <= s_aritySuffixesOneToNine.Length) - ? s_aritySuffixesOneToNine[arity - 1] - : string.Concat(GenericTypeNameManglingString, arity.ToString(CultureInfo.InvariantCulture)); - } - private static string GetFullyQualifiedName(string namespaceName, string typeName) => namespaceName.Length == 0 ? typeName : namespaceName + "." + typeName; - private static string GetMetadataName(CompletionItem item) + private static ISymbol? GetSymbol(CompletionItem item, Compilation compilation) { + // If we have SymbolKey data (i.e. this is an extension method item), use it to recover symbol + if (item.Properties.TryGetValue(SymbolKeyData, out var symbolId)) + { + return SymbolKey.ResolveString(symbolId, compilation).GetAnySymbol(); + } + + // Otherwise, this is a type item, so we don't have SymbolKey data. But we should still have all + // the data to construct its full metadata name var containingNamespace = GetContainingNamespace(item); var typeName = item.Properties.TryGetValue(AttributeFullName, out var attributeFullName) ? attributeFullName : item.DisplayText; var fullyQualifiedName = GetFullyQualifiedName(containingNamespace, typeName); if (item.Properties.TryGetValue(TypeAritySuffixName, out var aritySuffix)) { - return fullyQualifiedName + aritySuffix; + return compilation.GetTypeByMetadataName(fullyQualifiedName + aritySuffix); } - return fullyQualifiedName; + return compilation.GetTypeByMetadataName(fullyQualifiedName); } } } diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableImportCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableImportCompletionItem.cs new file mode 100644 index 0000000000000000000000000000000000000000..f4b25483c8783362ba071ae2e1808f2fbfe395bc --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/SerializableImportCompletionItem.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +namespace Microsoft.CodeAnalysis.Completion.Providers +{ + internal readonly struct SerializableImportCompletionItem + { + public readonly string SymbolKeyData; + public readonly int Arity; + public readonly string Name; + public readonly Glyph Glyph; + public readonly string ContainingNamespace; + + public SerializableImportCompletionItem(string symbolKeyData, string name, int arity, Glyph glyph, string containingNamespace) + { + SymbolKeyData = symbolKeyData; + Arity = arity; + Name = name; + Glyph = glyph; + ContainingNamespace = containingNamespace; + } + } +} diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ExtensionMethodImportCompletionProvider.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ExtensionMethodImportCompletionProvider.vb new file mode 100644 index 0000000000000000000000000000000000000000..9c92c5928b8789723ebdbaf26b9a747387971b3f --- /dev/null +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ExtensionMethodImportCompletionProvider.vb @@ -0,0 +1,32 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Collections.Immutable +Imports System.Threading +Imports Microsoft.CodeAnalysis.Completion.Providers +Imports Microsoft.CodeAnalysis.Options +Imports Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery +Imports Microsoft.CodeAnalysis.Text + +Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers + Friend NotInheritable Class ExtensionMethodImportCompletionProvider + Inherits AbstractExtensionMethodImportCompletionProvider + + Protected Overrides ReadOnly Property GenericSuffix As String + Get + Return "(Of ...)" + End Get + End Property + + Friend Overrides Function IsInsertionTrigger(text As SourceText, characterPosition As Integer, options As OptionSet) As Boolean + Return CompletionUtilities.IsDefaultTriggerCharacterOrParen(text, characterPosition, options) + End Function + + Protected Overrides Function CreateContextAsync(document As Document, position As Integer, cancellationToken As CancellationToken) As Task(Of SyntaxContext) + Return ImportCompletionProviderHelper.CreateContextAsync(document, position, cancellationToken) + End Function + + Protected Overrides Function GetImportedNamespaces(location As SyntaxNode, semanticModel As SemanticModel, cancellationToken As CancellationToken) As ImmutableArray(Of String) + Return ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel, cancellationToken) + End Function + End Class +End Namespace diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ImportCompletionProviderHelper.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ImportCompletionProviderHelper.vb new file mode 100644 index 0000000000000000000000000000000000000000..2bb65c51df00d997512609e6b1fdb0c17f30e698 --- /dev/null +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ImportCompletionProviderHelper.vb @@ -0,0 +1,39 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Collections.Immutable +Imports System.Threading +Imports Microsoft.CodeAnalysis.PooledObjects +Imports Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery +Imports Microsoft.CodeAnalysis.VisualBasic.Extensions.ContextQuery +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers + + Friend NotInheritable Class ImportCompletionProviderHelper + + Public Shared Function GetImportedNamespaces(location As SyntaxNode, semanticModel As SemanticModel, cancellationToken As CancellationToken) As ImmutableArray(Of String) + Dim builder = ArrayBuilder(Of String).GetInstance() + + ' Get namespaces from import directives + Dim importsInScope = semanticModel.GetImportNamespacesInScope(location) + For Each import As INamespaceSymbol In importsInScope + builder.Add(import.ToDisplayString(SymbolDisplayFormats.NameFormat)) + Next + + ' Get global imports from compilation option + Dim vbOptions = DirectCast(semanticModel.Compilation.Options, VisualBasicCompilationOptions) + For Each globalImport As GlobalImport In vbOptions.GlobalImports + builder.Add(globalImport.Name) + Next + + Return builder.ToImmutableAndFree() + End Function + + Public Shared Async Function CreateContextAsync(document As Document, position As Integer, cancellationToken As CancellationToken) As Task(Of SyntaxContext) + ' Need regular semantic model because we will use it to get imported namespace symbols. Otherwise we will try to + ' reach outside of the span And ended up with "node not within syntax tree" error from the speculative model. + Dim semanticModel = Await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(False) + Return Await VisualBasicSyntaxContext.CreateContextAsync(document.Project.Solution.Workspace, semanticModel, position, cancellationToken).ConfigureAwait(False) + End Function + End Class +End Namespace diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb index ce1be116fa5872f8c7eb0c78b26c5515b95dd485..ea148ccc1ef951313997891fab8fe55b6096ba54 100644 --- a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb @@ -4,10 +4,8 @@ Imports System.Collections.Immutable Imports System.Threading Imports Microsoft.CodeAnalysis.Completion.Providers Imports Microsoft.CodeAnalysis.Options -Imports Microsoft.CodeAnalysis.PooledObjects Imports Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery Imports Microsoft.CodeAnalysis.Text -Imports Microsoft.CodeAnalysis.VisualBasic.Extensions.ContextQuery Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers @@ -19,35 +17,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers Return CompletionUtilities.IsDefaultTriggerCharacterOrParen(text, characterPosition, options) End Function - Protected Overrides Async Function CreateContextAsync(document As Document, position As Integer, cancellationToken As CancellationToken) As Task(Of SyntaxContext) - ' Need regular semantic model because we will use it to get imported namespace symbols. Otherwise we will try to - ' reach outside of the span And ended up with "node not within syntax tree" error from the speculative model. - Dim semanticModel = Await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(False) - Return Await VisualBasicSyntaxContext.CreateContextAsync(document.Project.Solution.Workspace, semanticModel, position, cancellationToken).ConfigureAwait(False) + Protected Overrides Function CreateContextAsync(document As Document, position As Integer, cancellationToken As CancellationToken) As Task(Of SyntaxContext) + Return ImportCompletionProviderHelper.CreateContextAsync(document, position, cancellationToken) End Function Protected Overrides Function GetImportedNamespaces(location As SyntaxNode, semanticModel As SemanticModel, cancellationToken As CancellationToken) As ImmutableArray(Of String) - Dim builder = ArrayBuilder(Of String).GetInstance() - - ' Get namespaces from import directives - Dim importsInScope = semanticModel.GetImportNamespacesInScope(location) - For Each import As INamespaceSymbol In importsInScope - builder.Add(import.ToDisplayString(SymbolDisplayFormats.NameFormat)) - Next - - ' Get global imports from compilation option - Dim vbOptions = DirectCast(semanticModel.Compilation.Options, VisualBasicCompilationOptions) - For Each globalImport As GlobalImport In vbOptions.GlobalImports - builder.Add(globalImport.Name) - Next - - Return builder.ToImmutableAndFree() - End Function - - Protected Overrides Async Function IsInImportsDirectiveAsync(document As Document, position As Integer, cancellationToken As CancellationToken) As Task(Of Boolean) - Dim syntaxTree = Await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(False) - Dim leftToken = SyntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDirectives:=True, includeDocumentationComments:=True) - Return leftToken.GetAncestor(Of ImportsStatementSyntax)() IsNot Nothing + Return ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel, cancellationToken) End Function End Class End Namespace diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/BasicTypeImportCompletionServiceFactory.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionServiceFactory.vb similarity index 95% rename from src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/BasicTypeImportCompletionServiceFactory.vb rename to src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionServiceFactory.vb index 8875fa2f4ed2d51a01e56b9b822317a9890b6ae6..4bd0ec302c23ae406baf9c63d24cdfc61c8bfdfc 100644 --- a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/BasicTypeImportCompletionServiceFactory.vb +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionServiceFactory.vb @@ -7,7 +7,7 @@ Imports Microsoft.CodeAnalysis.Host.Mef Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers - Friend NotInheritable Class BasicTypeImportCompletionServiceFactory + Friend NotInheritable Class TypeImportCompletionServiceFactory Implements ILanguageServiceFactory diff --git a/src/Features/VisualBasic/Portable/Completion/VisualBasicCompletionService.vb b/src/Features/VisualBasic/Portable/Completion/VisualBasicCompletionService.vb index dca86291b78af8d6c0cfe9ce6269bb50a7b42e24..abe376e1fc71ff0ddeed8835567622444445d489 100644 --- a/src/Features/VisualBasic/Portable/Completion/VisualBasicCompletionService.vb +++ b/src/Features/VisualBasic/Portable/Completion/VisualBasicCompletionService.vb @@ -63,6 +63,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion End If completionProviders = completionProviders.Add(New TypeImportCompletionProvider()) + completionProviders = completionProviders.Add(New ExtensionMethodImportCompletionProvider()) _completionProviders = completionProviders End Sub diff --git a/src/Workspaces/CSharp/Portable/FindSymbols/CSharpDeclaredSymbolInfoFactoryService.cs b/src/Workspaces/CSharp/Portable/FindSymbols/CSharpDeclaredSymbolInfoFactoryService.cs index ae3d0221bcad3d811463c6e67639938e9ecb0cd3..8b8f04d3d3e61f01f8c0d0923ffecbd47ff7bfe1 100644 --- a/src/Workspaces/CSharp/Portable/FindSymbols/CSharpDeclaredSymbolInfoFactoryService.cs +++ b/src/Workspaces/CSharp/Portable/FindSymbols/CSharpDeclaredSymbolInfoFactoryService.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; +using System.Diagnostics; using System.Linq; using System.Text; -using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.FindSymbols; @@ -140,7 +140,7 @@ private void ProcessUsings(List> aliasMaps, SyntaxLis } } - public override bool TryGetDeclaredSymbolInfo(StringTable stringTable, SyntaxNode node, out DeclaredSymbolInfo declaredSymbolInfo) + public override bool TryGetDeclaredSymbolInfo(StringTable stringTable, SyntaxNode node, string rootNamespace, out DeclaredSymbolInfo declaredSymbolInfo) { switch (node.Kind()) { @@ -492,5 +492,100 @@ private static string GetSimpleTypeName(SimpleNameSyntax name) private bool IsExtensionMethod(MethodDeclarationSyntax method) => method.ParameterList.Parameters.Count > 0 && method.ParameterList.Parameters[0].Modifiers.Any(SyntaxKind.ThisKeyword); + + // Root namespace is a VB only concept, which basically means root namespace is always global in C#. + public override string GetRootNamespace(CompilationOptions compilationOptions) + => string.Empty; + + public override bool TryGetAliasesFromUsingDirective(SyntaxNode node, out ImmutableArray<(string aliasName, string name)> aliases) + { + if (node is UsingDirectiveSyntax usingDirectiveNode && usingDirectiveNode.Alias != null) + { + if (TryGetSimpleTypeName(usingDirectiveNode.Alias.Name, typeParameterNames: null, out var aliasName) && + TryGetSimpleTypeName(usingDirectiveNode.Name, typeParameterNames: null, out var name)) + { + aliases = ImmutableArray.Create<(string, string)>((aliasName, name)); + return true; + } + } + + aliases = default; + return false; + } + + public override string GetTargetTypeName(SyntaxNode node) + { + var methodDeclaration = (MethodDeclarationSyntax)node; + Debug.Assert(IsExtensionMethod(methodDeclaration)); + + var typeParameterNames = methodDeclaration.TypeParameterList?.Parameters.SelectAsArray(p => p.Identifier.Text); + TryGetSimpleTypeName(methodDeclaration.ParameterList.Parameters[0].Type, typeParameterNames, out var targetTypeName); + return targetTypeName; + } + + private static bool TryGetSimpleTypeName(SyntaxNode node, ImmutableArray? typeParameterNames, out string simpleTypeName) + { + if (node is TypeSyntax typeNode) + { + switch (typeNode) + { + case IdentifierNameSyntax identifierNameNode: + // We consider it a complex method if the receiver type is a type parameter. + var text = identifierNameNode.Identifier.Text; + simpleTypeName = typeParameterNames?.Contains(text) == true ? null : text; + return simpleTypeName != null; + + case GenericNameSyntax genericNameNode: + var name = genericNameNode.Identifier.Text; + var arity = genericNameNode.Arity; + simpleTypeName = arity == 0 ? name : name + GetMetadataAritySuffix(arity); + return true; + + case PredefinedTypeSyntax predefinedTypeNode: + simpleTypeName = GetSpecialTypeName(predefinedTypeNode); + return simpleTypeName != null; + + case AliasQualifiedNameSyntax aliasQualifiedNameNode: + return TryGetSimpleTypeName(aliasQualifiedNameNode.Name, typeParameterNames, out simpleTypeName); + + case QualifiedNameSyntax qualifiedNameNode: + // For an identifier to the right of a '.', it can't be a type parameter, + // so we don't need to check for it further. + return TryGetSimpleTypeName(qualifiedNameNode.Right, typeParameterNames: null, out simpleTypeName); + + case NullableTypeSyntax nullableNode: + // Ignore nullability, becase nullable reference type might not be enabled universally. + // In the worst case we just include more methods to check in out filter. + return TryGetSimpleTypeName(nullableNode.ElementType, typeParameterNames, out simpleTypeName); + } + } + + simpleTypeName = null; + return false; + } + + private static string GetSpecialTypeName(PredefinedTypeSyntax predefinedTypeNode) + { + var kind = predefinedTypeNode.Keyword.Kind(); + return kind switch + { + SyntaxKind.BoolKeyword => "Boolean", + SyntaxKind.ByteKeyword => "Byte", + SyntaxKind.SByteKeyword => "SByte", + SyntaxKind.ShortKeyword => "Int16", + SyntaxKind.UShortKeyword => "UInt16", + SyntaxKind.IntKeyword => "Int32", + SyntaxKind.UIntKeyword => "UInt32", + SyntaxKind.LongKeyword => "Int64", + SyntaxKind.ULongKeyword => "UInt64", + SyntaxKind.DoubleKeyword => "Double", + SyntaxKind.FloatKeyword => "Single", + SyntaxKind.DecimalKeyword => "Decimal", + SyntaxKind.StringKeyword => "String", + SyntaxKind.CharKeyword => "Char", + SyntaxKind.ObjectKeyword => "Object", + _ => null, + }; + } } } diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index aa093ecb35117b67fa6f33aaf6066bba5dbfc083..c1cc894f6931026a031a47942b3e842cbf38309c 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -200,6 +200,9 @@ public bool IsUsingDirectiveName(SyntaxNode node) ((UsingDirectiveSyntax)node.Parent).Name == node; } + public bool IsUsingAliasDirective(SyntaxNode node) + => node is UsingDirectiveSyntax usingDirectiveNode && usingDirectiveNode.Alias != null; + public bool IsForEachStatement(SyntaxNode node) => node is ForEachStatementSyntax; @@ -1264,6 +1267,12 @@ public SyntaxNode GetRightSideOfDot(SyntaxNode node) (node as MemberAccessExpressionSyntax)?.Name; } + public SyntaxNode GetLeftSideOfDot(SyntaxNode node, bool allowImplicitTarget) + { + return (node as QualifiedNameSyntax)?.Left ?? + (node as MemberAccessExpressionSyntax)?.Expression; + } + public bool IsLeftSideOfExplicitInterfaceSpecifier(SyntaxNode node) => (node as NameSyntax).IsLeftSideOfExplicitInterfaceSpecifier(); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.Node.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.Node.cs index 57bacec99342050ff82cd3387dfc99446129473b..0e5f335a02a6f4010b54b0a3cec757ee00991659 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.Node.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.Node.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Immutable; using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols { @@ -19,15 +23,17 @@ internal partial class SymbolTreeInfo [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] private struct BuilderNode { - public static readonly BuilderNode RootNode = new BuilderNode("", RootNodeParentIndex); + public static readonly BuilderNode RootNode = new BuilderNode("", RootNodeParentIndex, default); public readonly string Name; public readonly int ParentIndex; + public readonly MultiDictionary.ValueSet ParameterTypeInfos; - public BuilderNode(string name, int parentIndex) + public BuilderNode(string name, int parentIndex, MultiDictionary.ValueSet parameterTypeInfos = default) { Name = name; ParentIndex = parentIndex; + ParameterTypeInfos = parameterTypeInfos; } public bool IsRoot => ParentIndex == RootNodeParentIndex; @@ -72,5 +78,107 @@ private string GetDebuggerDisplay() return NameSpan + ", " + ParentIndex; } } + + private readonly struct ParameterTypeInfo + { + /// + /// This is the type name of the parameter when is false. + /// + public readonly string Name; + + /// + /// Similar to , we divide extension methods into simple + /// and complex categories for filtering purpose. Whether a method is simple is determined based on if we + /// can determine it's target type easily with a pure text matching. For complex methods, we will need to + /// rely on symbol to decide if it's feasible. + /// + /// Simple types include: + /// - Primitive types + /// - Types which is not a generic method parameter + /// - By reference type of any types above + /// + public readonly bool IsComplexType; + + public ParameterTypeInfo(string name, bool isComplex) + { + Name = name; + IsComplexType = isComplex; + } + } + + public readonly struct ExtensionMethodInfo + { + /// + /// Name of the extension method. + /// This can be used to retrive corresponding symbols via + /// + public readonly string Name; + + /// + /// Fully qualified name for the type that contains this extension method. + /// + public readonly string FullyQualifiedContainerName; + + public ExtensionMethodInfo(string fullyQualifiedContainerName, string name) + { + FullyQualifiedContainerName = fullyQualifiedContainerName; + Name = name; + } + } + + private sealed class ParameterTypeInfoProvider : ISignatureTypeProvider + { + public static readonly ParameterTypeInfoProvider Instance = new ParameterTypeInfoProvider(); + + private static ParameterTypeInfo ComplexInfo + => new ParameterTypeInfo(string.Empty, isComplex: true); + + public ParameterTypeInfo GetPrimitiveType(PrimitiveTypeCode typeCode) + => new ParameterTypeInfo(typeCode.ToString(), isComplex: false); + + public ParameterTypeInfo GetGenericInstantiation(ParameterTypeInfo genericType, ImmutableArray typeArguments) + => genericType.IsComplexType + ? ComplexInfo + : new ParameterTypeInfo(genericType.Name, isComplex: false); + + public ParameterTypeInfo GetByReferenceType(ParameterTypeInfo elementType) + => elementType; + + public ParameterTypeInfo GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var type = reader.GetTypeDefinition(handle); + var name = reader.GetString(type.Name); + return new ParameterTypeInfo(name, isComplex: false); + } + + public ParameterTypeInfo GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var type = reader.GetTypeReference(handle); + var name = reader.GetString(type.Name); + return new ParameterTypeInfo(name, isComplex: false); + } + + public ParameterTypeInfo GetTypeFromSpecification(MetadataReader reader, object genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + var sigReader = reader.GetBlobReader(reader.GetTypeSpecification(handle).Signature); + return new SignatureDecoder(Instance, reader, genericContext).DecodeType(ref sigReader); + } + + public ParameterTypeInfo GetArrayType(ParameterTypeInfo elementType, ArrayShape shape) => ComplexInfo; + + public ParameterTypeInfo GetSZArrayType(ParameterTypeInfo elementType) => ComplexInfo; + + public ParameterTypeInfo GetFunctionPointerType(MethodSignature signature) => ComplexInfo; + + public ParameterTypeInfo GetGenericMethodParameter(object genericContext, int index) => ComplexInfo; + + public ParameterTypeInfo GetGenericTypeParameter(object genericContext, int index) => ComplexInfo; + + public ParameterTypeInfo GetModifiedType(ParameterTypeInfo modifier, ParameterTypeInfo unmodifiedType, bool isRequired) => ComplexInfo; + + public ParameterTypeInfo GetPinnedType(ParameterTypeInfo elementType) => ComplexInfo; + + public ParameterTypeInfo GetPointerType(ParameterTypeInfo elementType) => ComplexInfo; + } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs index 0ff7d77a579e33692db3f416c841a44fb1f4cf58..7939c2f38bb5bad0b87f86fbb3e36b926db565ba 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -49,6 +51,39 @@ internal partial class SymbolTreeInfo : IChecksummedObject /// private readonly OrderPreservingMultiDictionary _inheritanceMap; + /// + /// Maps the name of target type name of simple extension methods to its . + /// for the definition of simple/complex methods. + /// + private readonly MultiDictionary? _simpleTypeNameToExtensionMethodMap; + + /// + /// A list of for complex extension methods. + /// for the definition of simple/complex methods. + /// + private readonly ImmutableArray _extensionMethodOfComplexType; + + public bool ContainsExtensionMethod => _simpleTypeNameToExtensionMethodMap?.Count > 0 || _extensionMethodOfComplexType.Length > 0; + + public ImmutableArray GetMatchingExtensionMethodInfo(ImmutableArray parameterTypeNames) + { + if (_simpleTypeNameToExtensionMethodMap == null) + { + return _extensionMethodOfComplexType; + } + + var builder = ArrayBuilder.GetInstance(); + builder.AddRange(_extensionMethodOfComplexType); + + foreach (var parameterTypeName in parameterTypeNames) + { + var simpleMethods = _simpleTypeNameToExtensionMethodMap[parameterTypeName]; + builder.AddRange(simpleMethods); + } + + return builder.ToImmutableAndFree(); + } + /// /// The task that produces the spell checker we use for fuzzy match queries. /// We use a task so that we can generate the @@ -85,9 +120,12 @@ internal partial class SymbolTreeInfo : IChecksummedObject string concatenatedNames, ImmutableArray sortedNodes, Task spellCheckerTask, - OrderPreservingMultiDictionary inheritanceMap) + OrderPreservingMultiDictionary inheritanceMap, + ImmutableArray extensionMethodOfComplexType, + MultiDictionary simpleTypeNameToExtensionMethodMap) : this(checksum, concatenatedNames, sortedNodes, spellCheckerTask, - CreateIndexBasedInheritanceMap(concatenatedNames, sortedNodes, inheritanceMap)) + CreateIndexBasedInheritanceMap(concatenatedNames, sortedNodes, inheritanceMap), + extensionMethodOfComplexType, simpleTypeNameToExtensionMethodMap) { } @@ -96,13 +134,17 @@ internal partial class SymbolTreeInfo : IChecksummedObject string concatenatedNames, ImmutableArray sortedNodes, Task spellCheckerTask, - OrderPreservingMultiDictionary inheritanceMap) + OrderPreservingMultiDictionary inheritanceMap, + ImmutableArray extensionMethodOfComplexType, + MultiDictionary? simpleTypeNameToExtensionMethodMap) { Checksum = checksum; _concatenatedNames = concatenatedNames; _nodes = sortedNodes; _spellCheckerTask = spellCheckerTask; _inheritanceMap = inheritanceMap; + _extensionMethodOfComplexType = extensionMethodOfComplexType; + _simpleTypeNameToExtensionMethodMap = simpleTypeNameToExtensionMethodMap; } public static SymbolTreeInfo CreateEmpty(Checksum checksum) @@ -112,13 +154,15 @@ public static SymbolTreeInfo CreateEmpty(Checksum checksum) return new SymbolTreeInfo(checksum, concatenatedNames, sortedNodes, CreateSpellCheckerAsync(checksum, concatenatedNames, sortedNodes), - new OrderPreservingMultiDictionary()); + new OrderPreservingMultiDictionary(), + ImmutableArray.Empty, + new MultiDictionary()); } public SymbolTreeInfo WithChecksum(Checksum checksum) { return new SymbolTreeInfo( - checksum, _concatenatedNames, _nodes, _spellCheckerTask, _inheritanceMap); + checksum, _concatenatedNames, _nodes, _spellCheckerTask, _inheritanceMap, _extensionMethodOfComplexType, _simpleTypeNameToExtensionMethodMap); } public Task> FindAsync( @@ -206,7 +250,7 @@ public SymbolTreeInfo WithChecksum(Checksum checksum) { var comparer = GetComparer(ignoreCase); var results = ArrayBuilder.GetInstance(); - IAssemblySymbol assemblySymbol = null; + IAssemblySymbol? assemblySymbol = null; foreach (var node in FindNodeIndices(name, comparer)) { @@ -358,7 +402,7 @@ private static int BinarySearch(string concatenatedNames, ImmutableArray n out ImmutableArray sortedNodes) { // Generate index numbers from 0 to Count-1 - var tmp = new int[unsortedNodes.Length]; + int[]? tmp = new int[unsortedNodes.Length]; for (var i = 0; i < tmp.Length; i++) { tmp[i] = i; @@ -383,7 +427,7 @@ private static int BinarySearch(string concatenatedNames, ImmutableArray n result.Count = unsortedNodes.Length; var concatenatedNamesBuilder = new StringBuilder(); - string lastName = null; + string? lastName = null; // Copy nodes into the result array in the appropriate order and fixing // up parent indexes as we go. @@ -528,7 +572,9 @@ internal void AssertEquivalentTo(SymbolTreeInfo other) private static SymbolTreeInfo CreateSymbolTreeInfo( Solution solution, Checksum checksum, string filePath, ImmutableArray unsortedNodes, - OrderPreservingMultiDictionary inheritanceMap) + OrderPreservingMultiDictionary inheritanceMap, + MultiDictionary simpleMethods, + ImmutableArray complexMethods) { SortNodes(unsortedNodes, out var concatenatedNames, out var sortedNodes); var createSpellCheckerTask = GetSpellCheckerTask( @@ -536,7 +582,8 @@ internal void AssertEquivalentTo(SymbolTreeInfo other) return new SymbolTreeInfo( checksum, concatenatedNames, - sortedNodes, createSpellCheckerTask, inheritanceMap); + sortedNodes, createSpellCheckerTask, inheritanceMap, + complexMethods, simpleMethods); } private static OrderPreservingMultiDictionary CreateIndexBasedInheritanceMap( diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs index 85baef248cc054c3d586b9e9c22bad362e3a8467..a787b20e650a320b0766ecdb9eb3280625f98438 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs @@ -5,13 +5,13 @@ using System.Collections.Immutable; using System.Diagnostics; using System.IO; +using System.Linq; using System.Reflection; using System.Reflection.Metadata; -using System.Runtime.CompilerServices; +using System.Reflection.Metadata.Ecma335; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Utilities; @@ -229,6 +229,16 @@ private struct MetadataInfoCreator : IDisposable // The set of type definitions we've read out of the current metadata reader. private readonly List _allTypeDefinitions; + // Map from node represents extension method to list of possible parameter type info. + // We can have more than one if there's multiple methods with same name but different target type. + // e.g. + // + // public static bool AnotherExtensionMethod1(this int x); + // public static bool AnotherExtensionMethod1(this bool x); + // + private MultiDictionary _extensionMethodToParameterTypeInfo; + private bool _containsExtensionsMethod; + public MetadataInfoCreator( Solution solution, Checksum checksum, PortableExecutableReference reference, CancellationToken cancellationToken) { @@ -238,9 +248,11 @@ private struct MetadataInfoCreator : IDisposable _cancellationToken = cancellationToken; _metadataReader = null; _allTypeDefinitions = new List(); + _containsExtensionsMethod = false; _inheritanceMap = OrderPreservingMultiDictionary.GetInstance(); _parentToChildren = OrderPreservingMultiDictionary.GetInstance(); + _extensionMethodToParameterTypeInfo = new MultiDictionary(); _rootNode = MetadataNode.Allocate(name: ""); } @@ -295,9 +307,12 @@ internal SymbolTreeInfo Create() } } - var unsortedNodes = GenerateUnsortedNodes(); + var simpleMap = new MultiDictionary(); + var complexBuilder = ArrayBuilder.GetInstance(); + var unsortedNodes = GenerateUnsortedNodes(complexBuilder, simpleMap); + return CreateSymbolTreeInfo( - _solution, _checksum, _reference.FilePath, unsortedNodes, _inheritanceMap); + _solution, _checksum, _reference.FilePath, unsortedNodes, _inheritanceMap, simpleMap, complexBuilder.ToImmutableAndFree()); } public void Dispose() @@ -356,6 +371,12 @@ private void GenerateMetadataNodes() { foreach (var definition in definitionsWithSameName) { + if (definition.Kind == MetadataDefinitionKind.Member) + { + // We need to support having multiple methods with same name but different target type. + _extensionMethodToParameterTypeInfo.Add(childNode, definition.TargetTypeInfo); + } + LookupMetadataDefinitions(definition, definitionMap); } @@ -407,12 +428,23 @@ private void GenerateMetadataNodes() // we just pull in methods that have attributes on them. if ((method.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public && (method.Attributes & MethodAttributes.Static) != 0 && + method.GetParameters().Count > 0 && method.GetCustomAttributes().Count > 0) { - var definition = new MetadataDefinition( - MetadataDefinitionKind.Member, _metadataReader.GetString(method.Name)); - - definitionMap.Add(definition.Name, definition); + // Decode method signature to get the target type name (i.e. type name for the first parameter) + var blob = _metadataReader.GetBlobReader(method.Signature); + var decoder = new SignatureDecoder(ParameterTypeInfoProvider.Instance, _metadataReader, genericContext: null); + var signature = decoder.DecodeMethodSignature(ref blob); + + // It'd be good if we don't need to go through all parameters and make unnecessary allocations. + // However, this is not possible with meatadata reader API right now (although it's possible by copying code from meatadata reader implementaion) + if (signature.ParameterTypes.Length > 0) + { + _containsExtensionsMethod = true; + var firstParameterTypeInfo = signature.ParameterTypes[0]; + var definition = new MetadataDefinition(MetadataDefinitionKind.Member, _metadataReader.GetString(method.Name), firstParameterTypeInfo); + definitionMap.Add(definition.Name, definition); + } } } } @@ -665,26 +697,59 @@ private void EnsureParentsAndChildren(List simpleNames) return newChildNode; } - private ImmutableArray GenerateUnsortedNodes() + private ImmutableArray GenerateUnsortedNodes(ArrayBuilder complexBuilder, MultiDictionary simpleTypeNameToMethodMap) { var unsortedNodes = ArrayBuilder.GetInstance(); unsortedNodes.Add(BuilderNode.RootNode); - AddUnsortedNodes(unsortedNodes, parentNode: _rootNode, parentIndex: 0); - + AddUnsortedNodes(unsortedNodes, simpleTypeNameToMethodMap, complexBuilder, parentNode: _rootNode, parentIndex: 0, fullyQualifiedContainerName: _containsExtensionsMethod ? "" : null); return unsortedNodes.ToImmutableAndFree(); } - private void AddUnsortedNodes( - ArrayBuilder unsortedNodes, MetadataNode parentNode, int parentIndex) + private void AddUnsortedNodes(ArrayBuilder unsortedNodes, + MultiDictionary simpleBuilder, + ArrayBuilder complexBuilder, + MetadataNode parentNode, + int parentIndex, + string fullyQualifiedContainerName) { foreach (var child in _parentToChildren[parentNode]) { - var childNode = new BuilderNode(child.Name, parentIndex); + var childNode = new BuilderNode(child.Name, parentIndex, _extensionMethodToParameterTypeInfo[child]); var childIndex = unsortedNodes.Count; unsortedNodes.Add(childNode); - AddUnsortedNodes(unsortedNodes, child, childIndex); + if (fullyQualifiedContainerName != null) + { + foreach (var parameterTypeInfo in _extensionMethodToParameterTypeInfo[child]) + { + if (parameterTypeInfo.IsComplexType) + { + complexBuilder.Add(new ExtensionMethodInfo(fullyQualifiedContainerName, child.Name)); + } + else + { + simpleBuilder.Add(parameterTypeInfo.Name, new ExtensionMethodInfo(fullyQualifiedContainerName, child.Name)); + } + } + } + + AddUnsortedNodes(unsortedNodes, simpleBuilder, complexBuilder, child, childIndex, Concat(fullyQualifiedContainerName, child.Name)); + } + + static string Concat(string containerName, string name) + { + if (containerName == null) + { + return null; + } + + if (containerName.Length == 0) + { + return name; + } + + return containerName + "." + name; } } } @@ -727,14 +792,20 @@ private struct MetadataDefinition public string Name { get; } public MetadataDefinitionKind Kind { get; } + /// + /// Only applies to member kind. Represents the type info of the first parameter. + /// + public ParameterTypeInfo TargetTypeInfo { get; } + public NamespaceDefinition Namespace { get; private set; } public TypeDefinition Type { get; private set; } - public MetadataDefinition(MetadataDefinitionKind kind, string name) + public MetadataDefinition(MetadataDefinitionKind kind, string name, ParameterTypeInfo targetTypeInfo = default) : this() { Kind = kind; Name = name; + TargetTypeInfo = targetTypeInfo; } public static MetadataDefinition Create( diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs index b7f777ab0ad3016314b1ab83d715eb6ed3472106..49a1b2f315a2f6d517c6c4f807bc609397e76502 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs @@ -10,7 +10,6 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Utilities; using Roslyn.Utilities; @@ -20,7 +19,7 @@ namespace Microsoft.CodeAnalysis.FindSymbols internal partial class SymbolTreeInfo : IObjectWritable { private const string PrefixMetadataSymbolTreeInfo = ""; - private static readonly Checksum SerializationFormatChecksum = Checksum.Create("18"); + private static readonly Checksum SerializationFormatChecksum = Checksum.Create("19"); /// /// Loads the SpellChecker for a given assembly symbol (metadata or project). If the @@ -153,6 +152,35 @@ public void WriteTo(ObjectWriter writer) writer.WriteInt32(v); } } + + if (_simpleTypeNameToExtensionMethodMap == null) + { + writer.WriteInt32(0); + } + else + { + writer.WriteInt32(_simpleTypeNameToExtensionMethodMap.Count); + foreach (var key in _simpleTypeNameToExtensionMethodMap.Keys) + { + writer.WriteString(key); + + var values = _simpleTypeNameToExtensionMethodMap[key]; + writer.WriteInt32(values.Count); + + foreach (var value in values) + { + writer.WriteString(value.FullyQualifiedContainerName); + writer.WriteString(value.Name); + } + } + } + + writer.WriteInt32(_extensionMethodOfComplexType.Length); + foreach (var methodInfo in _extensionMethodOfComplexType) + { + writer.WriteString(methodInfo.FullyQualifiedContainerName); + writer.WriteString(methodInfo.Name); + } } internal static SymbolTreeInfo ReadSymbolTreeInfo_ForTestingPurposesOnly( @@ -197,9 +225,56 @@ public void WriteTo(ObjectWriter writer) } } + MultiDictionary simpleTypeNameToExtensionMethodMap; + ImmutableArray extensionMethodOfComplexType; + + var keyCount = reader.ReadInt32(); + if (keyCount == 0) + { + simpleTypeNameToExtensionMethodMap = null; + } + else + { + simpleTypeNameToExtensionMethodMap = new MultiDictionary(); + + for (var i = 0; i < keyCount; i++) + { + var typeName = reader.ReadString(); + var valueCount = reader.ReadInt32(); + + for (var j = 0; j < valueCount; j++) + { + var containerName = reader.ReadString(); + var name = reader.ReadString(); + + simpleTypeNameToExtensionMethodMap.Add(typeName, new ExtensionMethodInfo(containerName, name)); + } + } + } + + var arrayLength = reader.ReadInt32(); + if (arrayLength == 0) + { + extensionMethodOfComplexType = ImmutableArray.Empty; + } + else + { + var builder = ArrayBuilder.GetInstance(arrayLength); + for (var i = 0; i < arrayLength; ++i) + { + var containerName = reader.ReadString(); + var name = reader.ReadString(); + builder.Add(new ExtensionMethodInfo(containerName, name)); + } + + extensionMethodOfComplexType = builder.ToImmutableAndFree(); + } + var nodeArray = nodes.ToImmutableAndFree(); var spellCheckerTask = createSpellCheckerTask(concatenatedNames, nodeArray); - return new SymbolTreeInfo(checksum, concatenatedNames, nodeArray, spellCheckerTask, inheritanceMap); + return new SymbolTreeInfo( + checksum, concatenatedNames, nodeArray, spellCheckerTask, inheritanceMap, + extensionMethodOfComplexType, simpleTypeNameToExtensionMethodMap); } catch { diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs index 34af0c4a0c8affbd20b0ef9d4fa0ada0e79b7d0c..efe02ae7dcb203e004fc82d02e23be303d0644f1 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Immutable; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -117,7 +118,9 @@ private static async Task ComputeSourceSymbolsChecksumAsync(ProjectSta return CreateSymbolTreeInfo( project.Solution, checksum, project.FilePath, unsortedNodes.ToImmutableAndFree(), - inheritanceMap: new OrderPreservingMultiDictionary()); + inheritanceMap: new OrderPreservingMultiDictionary(), + simpleMethods: null, + complexMethods: ImmutableArray.Empty); } // generate nodes for the global namespace an all descendants diff --git a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex.ExtensionMethodInfo.cs b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex.ExtensionMethodInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..113a8f4d2a7f06393c66f7507171a8d0748dca5d --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex.ExtensionMethodInfo.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.FindSymbols +{ + internal partial class SyntaxTreeIndex + { + private readonly struct ExtensionMethodInfo + { + // We divide extension methods into two categories, simple and complex, for filtering purpose. + // Whether a method is simple is determined based on if we can determine it's target type easily + // with a pure text matching. For complex methods, we will need to rely on symbol to decide if it's + // feasible. + // + // Complex methods include: + // - Method declared in the document which includes using alias directive + // - Generic method where the target type is a type-paramter (e.g. List would be considered simple, not complex) + // - If the target type name is one of the following (i.e. name of the type for the first parameter) + // 1. Array type + // 2. ValueTuple type + // 3. Pointer type + // + // The rest of methods are considered simple. + + /// + /// Name of the simple method's target type name to the index of its DeclaredSymbolInfo in `_declarationInfo`. + /// All predefined types are converted to its metadata form. e.g. int => Int32. For generic types, type parameters are ignored. + /// + public readonly ImmutableDictionary> SimpleExtensionMethodInfo { get; } + + /// + /// Indices of to all complex methods' DeclaredSymbolInfo in `_declarationInfo`. + /// + public readonly ImmutableArray ComplexExtensionMethodInfo { get; } + + public bool ContainsExtensionMethod => SimpleExtensionMethodInfo.Count > 0 || ComplexExtensionMethodInfo.Length > 0; + + public ExtensionMethodInfo( + ImmutableDictionary> simpleExtensionMethodInfo, + ImmutableArray complexExtensionMethodInfo) + { + SimpleExtensionMethodInfo = simpleExtensionMethodInfo; + ComplexExtensionMethodInfo = complexExtensionMethodInfo; + } + + public void WriteTo(ObjectWriter writer) + { + writer.WriteInt32(SimpleExtensionMethodInfo.Count); + + foreach (var kvp in SimpleExtensionMethodInfo) + { + writer.WriteString(kvp.Key); + writer.WriteInt32(kvp.Value.Length); + + foreach (var declaredSymbolInfoIndex in kvp.Value) + { + writer.WriteInt32(declaredSymbolInfoIndex); + } + } + + writer.WriteInt32(ComplexExtensionMethodInfo.Length); + foreach (var declaredSymbolInfoIndex in ComplexExtensionMethodInfo) + { + writer.WriteInt32(declaredSymbolInfoIndex); + } + } + + public static ExtensionMethodInfo? TryReadFrom(ObjectReader reader) + { + try + { + var simpleExtensionMethodInfo = ImmutableDictionary.CreateBuilder>(); + var count = reader.ReadInt32(); + + for (var i = 0; i < count; ++i) + { + var typeName = reader.ReadString(); + var arrayLength = reader.ReadInt32(); + var arrayBuilder = ArrayBuilder.GetInstance(arrayLength); + + for (var j = 0; j < arrayLength; ++j) + { + var declaredSymbolInfoIndex = reader.ReadInt32(); + arrayBuilder.Add(declaredSymbolInfoIndex); + } + + simpleExtensionMethodInfo[typeName] = arrayBuilder.ToImmutableAndFree(); + } + + count = reader.ReadInt32(); + var complexExtensionMethodInfo = ArrayBuilder.GetInstance(count); + for (var i = 0; i < count; ++i) + { + var declaredSymbolInfoIndex = reader.ReadInt32(); + complexExtensionMethodInfo.Add(declaredSymbolInfoIndex); + } + + return new ExtensionMethodInfo(simpleExtensionMethodInfo.ToImmutable(), complexExtensionMethodInfo.ToImmutableAndFree()); + } + catch (Exception) + { + } + + return null; + } + } + } +} diff --git a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex.cs b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex.cs index 7739de6012f70f0d89363dc3d55b1f14c108814b..39a03a256ca280c9ce8400b6aca6462a840add97 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols { @@ -15,19 +16,22 @@ internal sealed partial class SyntaxTreeIndex private readonly IdentifierInfo _identifierInfo; private readonly ContextInfo _contextInfo; private readonly DeclarationInfo _declarationInfo; + private readonly ExtensionMethodInfo _extensionMethodInfo; private SyntaxTreeIndex( Checksum checksum, LiteralInfo literalInfo, IdentifierInfo identifierInfo, ContextInfo contextInfo, - DeclarationInfo declarationInfo) + DeclarationInfo declarationInfo, + ExtensionMethodInfo extensionMethodInfo) { this.Checksum = checksum; _literalInfo = literalInfo; _identifierInfo = identifierInfo; _contextInfo = contextInfo; _declarationInfo = declarationInfo; + _extensionMethodInfo = extensionMethodInfo; } private static readonly ConditionalWeakTable s_documentToIndex = new ConditionalWeakTable(); @@ -56,18 +60,29 @@ public static async Task PrecalculateAsync(Document document, CancellationToken } } + public static Task GetIndexAsync(Document document, CancellationToken cancellationToken) + => GetIndexAsync(document, loadOnly: false, cancellationToken); + public static async Task GetIndexAsync( Document document, + bool loadOnly, CancellationToken cancellationToken) { // See if we already cached an index with this direct document index. If so we can just // return it with no additional work. if (!s_documentToIndex.TryGetValue(document, out var index)) { - index = await GetIndexWorkerAsync(document, cancellationToken).ConfigureAwait(false); + index = await GetIndexWorkerAsync(document, loadOnly, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfFalse(index != null || loadOnly == true, "Result can only be null if 'loadOnly: true' was passed."); + + if (index == null && loadOnly) + { + return null; + } // Populate our caches with this data. s_documentToIndex.GetValue(document, _ => index); + s_documentIdToIndex.Remove(document.Id); s_documentIdToIndex.GetValue(document.Id, _ => index); } @@ -76,6 +91,7 @@ public static async Task PrecalculateAsync(Document document, CancellationToken private static async Task GetIndexWorkerAsync( Document document, + bool loadOnly, CancellationToken cancellationToken) { var checksum = await GetChecksumAsync(document, cancellationToken).ConfigureAwait(false); @@ -92,7 +108,7 @@ public static async Task PrecalculateAsync(Document document, CancellationToken // What we have in memory isn't valid. Try to load from the persistence service. index = await LoadAsync(document, checksum, cancellationToken).ConfigureAwait(false); - if (index != null) + if (index != null || loadOnly) { return index; } diff --git a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Create.cs b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Create.cs index 66317982b4096bae931f83552d93efcd9e224d56..fb7092b6a48d625299a073988603363723b3060b 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Create.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Create.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; @@ -18,7 +19,17 @@ namespace Microsoft.CodeAnalysis.FindSymbols { internal interface IDeclaredSymbolInfoFactoryService : ILanguageService { - bool TryGetDeclaredSymbolInfo(StringTable stringTable, SyntaxNode node, out DeclaredSymbolInfo declaredSymbolInfo); + // `rootNamespace` is required for VB projects that has non-global namespace as root namespace, + // otherwise we would not be able to get correct data from syntax. + bool TryGetDeclaredSymbolInfo(StringTable stringTable, SyntaxNode node, string rootNamespace, out DeclaredSymbolInfo declaredSymbolInfo); + + // Get the name of the target type of specified extension method declaration node. + // The returned value would be null for complex type. + string GetTargetTypeName(SyntaxNode node); + + bool TryGetAliasesFromUsingDirective(SyntaxNode node, out ImmutableArray<(string aliasName, string name)> aliases); + + string GetRootNamespace(CompilationOptions compilationOptions); } internal sealed partial class SyntaxTreeIndex @@ -60,6 +71,11 @@ internal sealed partial class SyntaxTreeIndex var stringLiterals = StringLiteralHashSetPool.Allocate(); var longLiterals = LongLiteralHashSetPool.Allocate(); + var declaredSymbolInfos = ArrayBuilder.GetInstance(); + var complexExtensionMethodInfoBuilder = ArrayBuilder.GetInstance(); + var simpleExtensionMethodInfoBuilder = PooledDictionary>.GetInstance(); + var usingAliases = PooledDictionary.GetInstance(); + try { var containsForEachStatement = false; @@ -77,11 +93,10 @@ internal sealed partial class SyntaxTreeIndex var predefinedTypes = (int)PredefinedType.None; var predefinedOperators = (int)PredefinedOperator.None; - var declaredSymbolInfos = ArrayBuilder.GetInstance(); - if (syntaxFacts != null) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var rootNamespace = infoFactory.GetRootNamespace(project.CompilationOptions); foreach (var current in root.DescendantNodesAndTokensAndSelf(descendIntoTrivia: true)) { @@ -103,6 +118,33 @@ internal sealed partial class SyntaxTreeIndex containsTupleExpressionOrTupleType = containsTupleExpressionOrTupleType || syntaxFacts.IsTupleExpression(node) || syntaxFacts.IsTupleType(node); + if (syntaxFacts.IsUsingAliasDirective(node) && infoFactory.TryGetAliasesFromUsingDirective(node, out var aliases)) + { + foreach (var (aliasName, name) in aliases) + { + // In C#, it's valid to declare two alias with identical name, + // as long as they are in different containers. + // + // e.g. + // using X = System.String; + // namespace N + // { + // using X = System.Int32; + // } + // + // If we detect this, we will simply treat extension methods whose + // target type is this alias as complex method. + if (usingAliases.ContainsKey(aliasName)) + { + usingAliases[aliasName] = null; + } + else + { + usingAliases[aliasName] = name; + } + } + } + // We've received a number of error reports where DeclaredSymbolInfo.GetSymbolAsync() will // crash because the document's syntax root doesn't contain the span of the node returned // by TryGetDeclaredSymbolInfo(). There are two possibilities for this crash: @@ -113,11 +155,21 @@ internal sealed partial class SyntaxTreeIndex // the future then we know the problem lies in (2). If, however, the problem is really in // TryGetDeclaredSymbolInfo, then this will at least prevent us from returning bad spans // and will prevent the crash from occurring. - if (infoFactory.TryGetDeclaredSymbolInfo(stringTable, node, out var declaredSymbolInfo)) + if (infoFactory.TryGetDeclaredSymbolInfo(stringTable, node, rootNamespace, out var declaredSymbolInfo)) { if (root.FullSpan.Contains(declaredSymbolInfo.Span)) { + var declaredSymbolInfoIndex = declaredSymbolInfos.Count; declaredSymbolInfos.Add(declaredSymbolInfo); + + AddExtensionMethodInfo( + infoFactory, + node, + usingAliases, + declaredSymbolInfoIndex, + declaredSymbolInfo, + simpleExtensionMethodInfoBuilder, + complexExtensionMethodInfoBuilder); } else { @@ -214,14 +266,78 @@ internal sealed partial class SyntaxTreeIndex containsAwait, containsTupleExpressionOrTupleType), new DeclarationInfo( - declaredSymbolInfos.ToImmutableAndFree())); + declaredSymbolInfos.ToImmutable()), + new ExtensionMethodInfo( + simpleExtensionMethodInfoBuilder.ToImmutableDictionary(s_getKey, s_getValuesAsImmutableArray), + complexExtensionMethodInfoBuilder.ToImmutable())); } finally { Free(ignoreCase, identifiers, escapedIdentifiers); StringLiteralHashSetPool.ClearAndFree(stringLiterals); LongLiteralHashSetPool.ClearAndFree(longLiterals); + + foreach (var (_, builder) in simpleExtensionMethodInfoBuilder) + { + builder.Free(); + } + + simpleExtensionMethodInfoBuilder.Free(); + complexExtensionMethodInfoBuilder.Free(); + usingAliases.Free(); + declaredSymbolInfos.Free(); + } + } + + private static readonly Func>, string> s_getKey = kvp => kvp.Key; + private static readonly Func>, ImmutableArray> s_getValuesAsImmutableArray = kvp => kvp.Value.ToImmutable(); + + private static void AddExtensionMethodInfo( + IDeclaredSymbolInfoFactoryService infoFactory, + SyntaxNode node, + PooledDictionary aliases, + int declaredSymbolInfoIndex, + DeclaredSymbolInfo declaredSymbolInfo, + PooledDictionary> simpleInfoBuilder, + ArrayBuilder complexInfoBuilder) + { + if (declaredSymbolInfo.Kind != DeclaredSymbolInfoKind.ExtensionMethod) + { + return; + } + + var targetTypeName = infoFactory.GetTargetTypeName(node); + + // complex method + if (targetTypeName == null) + { + complexInfoBuilder.Add(declaredSymbolInfoIndex); + return; + } + + // Target type is an alias + if (aliases.TryGetValue(targetTypeName, out var originalName)) + { + // it is an alias of multiple with identical name, + // simply treat it as a complex method. + if (originalName == null) + { + complexInfoBuilder.Add(declaredSymbolInfoIndex); + return; + } + + // replace the alias with its original name. + targetTypeName = originalName; } + + // So we've got a simple method. + if (!simpleInfoBuilder.TryGetValue(targetTypeName, out var arrayBuilder)) + { + arrayBuilder = ArrayBuilder.GetInstance(); + simpleInfoBuilder[targetTypeName] = arrayBuilder; + } + + arrayBuilder.Add(declaredSymbolInfoIndex); } private static StringTable GetStringTable(Project project) diff --git a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Forwarders.cs b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Forwarders.cs index 4d8dd8a1388f7a0e629dcb23996dcd895ff6fe80..089772468664f44ebaf7a17686054a1867eaced7 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Forwarders.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Forwarders.cs @@ -2,7 +2,6 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.LanguageServices; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols { @@ -10,6 +9,14 @@ internal sealed partial class SyntaxTreeIndex { public ImmutableArray DeclaredSymbolInfos => _declarationInfo.DeclaredSymbolInfos; + public ImmutableDictionary> SimpleExtensionMethodInfo + => _extensionMethodInfo.SimpleExtensionMethodInfo; + + public ImmutableArray ComplexExtensionMethodInfo + => _extensionMethodInfo.ComplexExtensionMethodInfo; + + public bool ContainsExtensionMethod => _extensionMethodInfo.ContainsExtensionMethod; + public bool ProbablyContainsIdentifier(string identifier) => _identifierInfo.ProbablyContainsIdentifier(identifier); public bool ProbablyContainsEscapedIdentifier(string identifier) => _identifierInfo.ProbablyContainsEscapedIdentifier(identifier); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Persistence.cs b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Persistence.cs index 1292d053f48424f50c03a73f0916267681b5d153..346e96004e0c120fda3ecca9a996e982201b184a 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Persistence.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SyntaxTree/SyntaxTreeIndex_Persistence.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.FindSymbols internal sealed partial class SyntaxTreeIndex : IObjectWritable { private const string PersistenceName = ""; - private static readonly Checksum SerializationFormatChecksum = Checksum.Create("16"); + private static readonly Checksum SerializationFormatChecksum = Checksum.Create("17"); public readonly Checksum Checksum; @@ -123,6 +123,7 @@ public void WriteTo(ObjectWriter writer) _identifierInfo.WriteTo(writer); _contextInfo.WriteTo(writer); _declarationInfo.WriteTo(writer); + _extensionMethodInfo.WriteTo(writer); } private static SyntaxTreeIndex ReadFrom( @@ -132,14 +133,15 @@ public void WriteTo(ObjectWriter writer) var identifierInfo = IdentifierInfo.TryReadFrom(reader); var contextInfo = ContextInfo.TryReadFrom(reader); var declarationInfo = DeclarationInfo.TryReadFrom(stringTable, reader); + var extensionMethodInfo = ExtensionMethodInfo.TryReadFrom(reader); - if (literalInfo == null || identifierInfo == null || contextInfo == null || declarationInfo == null) + if (literalInfo == null || identifierInfo == null || contextInfo == null || declarationInfo == null || extensionMethodInfo == null) { return null; } return new SyntaxTreeIndex( - checksum, literalInfo.Value, identifierInfo.Value, contextInfo.Value, declarationInfo.Value); + checksum, literalInfo.Value, identifierInfo.Value, contextInfo.Value, declarationInfo.Value, extensionMethodInfo.Value); } } } diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs index 9bd903ff8b62b8a03b9fa774804e43e76df7a532..34ae81d8c204f56596b3ea1f214cc54275687634 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Text; using System.Threading; @@ -18,6 +19,9 @@ namespace Microsoft.CodeAnalysis.LanguageServices { internal abstract class AbstractDeclaredSymbolInfoFactoryService : IDeclaredSymbolInfoFactoryService { + private const string GenericTypeNameManglingString = "`"; + private static readonly string[] s_aritySuffixesOneToNine = { "`1", "`2", "`3", "`4", "`5", "`6", "`7", "`8", "`9" }; + private readonly static ObjectPool>> s_aliasMapListPool = new ObjectPool>>(() => new List>()); @@ -79,7 +83,27 @@ protected static void Intern(StringTable stringTable, ArrayBuilder build } } - public abstract bool TryGetDeclaredSymbolInfo(StringTable stringTable, SyntaxNode node, out DeclaredSymbolInfo declaredSymbolInfo); + public static string GetMetadataAritySuffix(int arity) + { + Debug.Assert(arity > 0); + return (arity <= s_aritySuffixesOneToNine.Length) + ? s_aritySuffixesOneToNine[arity - 1] + : string.Concat(GenericTypeNameManglingString, arity.ToString(CultureInfo.InvariantCulture)); + } + + public abstract bool TryGetDeclaredSymbolInfo(StringTable stringTable, SyntaxNode node, string rootNamespace, out DeclaredSymbolInfo declaredSymbolInfo); + + /// + /// Get the name of the target type of specified extension method declaration. + /// The node provided must be an extension method declaration, i.e. calling `TryGetDeclaredSymbolInfo()` + /// on `node` should return a `DeclaredSymbolInfo` of kind `ExtensionMethod`. + /// If the return value is null, then it means this is a "complex" method (as described at ). + /// + public abstract string GetTargetTypeName(SyntaxNode node); + + public abstract bool TryGetAliasesFromUsingDirective(SyntaxNode node, out ImmutableArray<(string aliasName, string name)> aliases); + + public abstract string GetRootNamespace(CompilationOptions compilationOptions); } internal abstract class AbstractSyntaxFactsService diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index d6361ca9480324cd014ea3497f29f54a64005b08..d170facb83a69ddd39ba668a6e507170ebe86ac7 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -89,6 +89,7 @@ internal interface ISyntaxFactsService : ILanguageService bool IsTypeNamedVarInVariableOrFieldDeclaration(SyntaxToken token, SyntaxNode parent); bool IsTypeNamedDynamic(SyntaxToken token, SyntaxNode parent); bool IsUsingOrExternOrImport(SyntaxNode node); + bool IsUsingAliasDirective(SyntaxNode node); bool IsGlobalAttribute(SyntaxNode node); bool IsDeclaration(SyntaxNode node); bool IsTypeDeclaration(SyntaxNode node); @@ -175,6 +176,20 @@ internal interface ISyntaxFactsService : ILanguageService bool IsLeftSideOfDot(SyntaxNode node); SyntaxNode GetRightSideOfDot(SyntaxNode node); + /// + /// Get the node on the left side of the dot if given a dotted expression. + /// + /// + /// In VB, we have a member access expression with a null expression, this may be one of the + /// following forms: + /// 1) new With { .a = 1, .b = .a .a refers to the anonymous type + /// 2) With obj : .m .m refers to the obj type + /// 3) new T() With { .a = 1, .b = .a 'a refers to the T type + /// If `allowImplicitTarget` is set to true, the returned node will be set to approperiate node, otherwise, it will return null. + /// This parameter has no affect on C# node. + /// + SyntaxNode GetLeftSideOfDot(SyntaxNode node, bool allowImplicitTarget = false); + bool IsRightSideOfQualifiedName(SyntaxNode node); bool IsLeftSideOfExplicitInterfaceSpecifier(SyntaxNode node); diff --git a/src/Workspaces/Core/Portable/Log/FunctionId.cs b/src/Workspaces/Core/Portable/Log/FunctionId.cs index 932180d9673842304eda960fc2f41acb20334766..e130cdf554b50b09f0e7155e351f7a30c3aa037e 100644 --- a/src/Workspaces/Core/Portable/Log/FunctionId.cs +++ b/src/Workspaces/Core/Portable/Log/FunctionId.cs @@ -160,6 +160,7 @@ internal enum FunctionId Completion_KeywordCompletionProvider_GetItemsWorker, Completion_SnippetCompletionProvider_GetItemsWorker_CSharp, Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, + Completion_ExtensionMethodImportCompletionProvider_GetCompletionItemsAsync, SignatureHelp_ModelComputation_ComputeModelInBackground, SignatureHelp_ModelComputation_UpdateModelInBackground, diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/DocumentExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/DocumentExtensions.cs index 4e36f76abe83b5f81bccca788fbc9043d848229f..712a310c49de5780ad612f3fb4ae99bee2a88c6d 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/DocumentExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/DocumentExtensions.cs @@ -27,6 +27,20 @@ internal static partial class DocumentExtensions public static TLanguageService? GetLanguageService(this Document? document) where TLanguageService : class, ILanguageService => document?.Project?.LanguageServices?.GetService(); + public static TLanguageService GetRequiredLanguageService(this Document document) where TLanguageService : class, ILanguageService + => document.Project.LanguageServices.GetRequiredService(); + + public static async Task GetRequiredSyntaxTreeAsync(this Document document, CancellationToken cancellationToken) + { + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxTree == null) + { + throw new InvalidOperationException(string.Format(WorkspacesResources.SyntaxTree_is_required_to_accomplish_the_task_but_is_not_supported_by_document_0, document.Name)); + } + + return syntaxTree; + } + public static bool IsOpen(this Document document) { var workspace = document.Project.Solution.Workspace as Workspace; @@ -170,7 +184,10 @@ public static async Task IsForkedDocumentWithSyntaxChangesAsync(this Docum } public static Task GetSyntaxTreeIndexAsync(this Document document, CancellationToken cancellationToken) - => SyntaxTreeIndex.GetIndexAsync(document, cancellationToken); + => SyntaxTreeIndex.GetIndexAsync(document, loadOnly: false, cancellationToken); + + public static Task GetSyntaxTreeIndexAsync(this Document document, bool loadOnly, CancellationToken cancellationToken) + => SyntaxTreeIndex.GetIndexAsync(document, loadOnly, cancellationToken); /// /// Returns the semantic model for this document that may be produced from partial semantics. The semantic model diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs index 33ed5cb2d78e73356e2f285561a9000f799b70cc..a4c68cf4b8ba39d9c6339261b967b1e775e1d66a 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs @@ -2,12 +2,12 @@ #nullable enable +using System; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Shared.Extensions @@ -122,5 +122,16 @@ public static async Task GetVersionAsync(this Project project, Can var newSolution = project.Solution.AddAnalyzerConfigDocuments(ImmutableArray.Create(documentInfo)); return newSolution.GetProject(project.Id)?.GetAnalyzerConfigDocument(id); } + + public static async Task GetRequiredCompilationAsync(this Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation == null) + { + throw new InvalidOperationException(string.Format(WorkspacesResources.Compilation_is_required_to_accomplish_the_task_but_is_not_supported_by_project_0, project.Name)); + } + + return compilation; + } } } diff --git a/src/Workspaces/Core/Portable/WorkspacesResources.Designer.cs b/src/Workspaces/Core/Portable/WorkspacesResources.Designer.cs index e99d66f612959d0e5c99be944ebba2bbf84bc21e..0f97ad707b6eee4eb86db35502f9bedf526f9d8f 100644 --- a/src/Workspaces/Core/Portable/WorkspacesResources.Designer.cs +++ b/src/Workspaces/Core/Portable/WorkspacesResources.Designer.cs @@ -574,6 +574,15 @@ internal class WorkspacesResources { } } + /// + /// Looks up a localized string similar to Compilation is required to accomplish the task but is not supported by project {0}.. + /// + internal static string Compilation_is_required_to_accomplish_the_task_but_is_not_supported_by_project_0 { + get { + return ResourceManager.GetString("Compilation_is_required_to_accomplish_the_task_but_is_not_supported_by_project_0", resourceCulture); + } + } + /// /// Looks up a localized string similar to Core EditorConfig Options. /// @@ -3375,6 +3384,15 @@ internal class WorkspacesResources { } } + /// + /// Looks up a localized string similar to Syntax tree is required to accomplish the task but is not supported by document {0}.. + /// + internal static string SyntaxTree_is_required_to_accomplish_the_task_but_is_not_supported_by_document_0 { + get { + return ResourceManager.GetString("SyntaxTree_is_required_to_accomplish_the_task_but_is_not_supported_by_document_0", resourceCulture); + } + } + /// /// Looks up a localized string similar to Temporary storage cannot be written more than once.. /// diff --git a/src/Workspaces/Core/Portable/WorkspacesResources.resx b/src/Workspaces/Core/Portable/WorkspacesResources.resx index 8ee214bcf8fa331f7d251682e212173d0a52cf1e..da7445724c76e5dd667c44e2cdebdba535ec3c14 100644 --- a/src/Workspaces/Core/Portable/WorkspacesResources.resx +++ b/src/Workspaces/Core/Portable/WorkspacesResources.resx @@ -1479,4 +1479,10 @@ Zero-width positive lookbehind assertions are typically used at the beginning of Document does not support syntax trees + + Compilation is required to accomplish the task but is not supported by project {0}. + + + Syntax tree is required to accomplish the task but is not supported by document {0}. + \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf index 6ba1579e9a917d2b9ff6da02fc887cd5f5ce85ff..f152a274dda3f413daa4af805600c8f2d7758b5b 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf @@ -27,6 +27,11 @@ Změna dokumentu {0} není podporovaná. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Základní možnosti pro EditorConfig @@ -1312,6 +1317,11 @@ Pozitivní kontrolní výrazy zpětného vyhledávání s nulovou šířkou se o Specifikace symbolů + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Soubory jazyka Visual Basic diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf index 931489d3788130c2eff09003eb9a0137718b5ee9..a4f3b03b06c4f88b3157f47a3b27d2db26be9584 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf @@ -27,6 +27,11 @@ Das Ändern des Dokuments "{0}" wird nicht unterstützt. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Wichtige EditorConfig-Optionen @@ -1312,6 +1317,11 @@ Positive Lookbehindassertionen mit Nullbreite werden normalerweise am Anfang reg Symbolspezifikationen + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Visual Basic-Dateien diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf index 6fbcc0cefa328f2436508cdcaf67e08422326a3f..e84cea3b90e0348528d0875f331dc6e0bcf22c07 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf @@ -27,6 +27,11 @@ Documento cambiante '{0}' no es compatible. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Opciones principales de EditorConfig @@ -1312,6 +1317,11 @@ Las aserciones posteriores positivas de ancho cero se usan normalmente al princi Especificaciones de símbolos + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Archivos de Visual Basic diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf index 1dcd5ef71ec79403e3d551b2c86676b3df8d6b9c..fb4c5aa9ae12e161a1694629e248b2b44dc2d304 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf @@ -27,6 +27,11 @@ Le changement du document '{0}' n'est pas pris en charge. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Options EditorConfig principales @@ -1312,6 +1317,11 @@ Les assertions de postanalyse positives de largeur nulle sont généralement uti Spécifications de symboles + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Fichiers Visual Basic diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf index 3a6ef514fae3ee7f16ec5fb1d93922f0e2ffa5fe..f07b13290ffc195ee28abd12c5e51243d60ac645 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf @@ -27,6 +27,11 @@ La modifica del documento '{0}' non è supportata. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Opzioni EditorConfig di base @@ -1312,6 +1317,11 @@ Le asserzioni lookbehind positive di larghezza zero vengono usate in genere all' Specifiche dei simboli + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files File Visual Basic diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf index 67ec924b44d6f48349f61924b0d50ad4184ee02f..fbfee4086551998cb17904078afc2ff38bad8423 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf @@ -27,6 +27,11 @@ ドキュメント '{0}' の変更はサポートされていません。 + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options コア EditorConfig オプション @@ -1312,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of 記号の仕様 + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Visual Basic ファイル diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf index be123c16fb8c6004491365bd7a25c9aadbbaa35c..78ec92e21e5b625bcef1a0fe29b8405c5c754b81 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf @@ -27,6 +27,11 @@ '{0}' 문서 변경은 지원되지 않습니다. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options 코어 EditorConfig 옵션 @@ -1312,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of 기호 사양 + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Visual Basic 파일 diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf index b3e158c6f729823bec310ff47cf3e9ff740c9709..31f5e96c87d65aba420051004409469f6609dc70 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf @@ -27,6 +27,11 @@ Zmiana dokumentu „{0}” nie jest obsługiwana. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Podstawowe opcje EditorConfig @@ -1312,6 +1317,11 @@ Pozytywne asercje wsteczne o zerowej szerokości są zwykle używane na początk Specyfikacje symboli + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Pliki języka Visual Basic diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf index a3af8c0b5964635acfd78d25ce58186a7b32800b..a0bdf450e41aa5dd62fe935ebcadd69d731d0353 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf @@ -27,6 +27,11 @@ Não há suporte para alterar o documento '{0}'. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Opções Principais do EditorConfig @@ -1312,6 +1317,11 @@ As declarações de lookbehind positivas de largura zero normalmente são usadas Especificações de símbolo + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Arquivos do Visual Basic diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf index d92acc1215395ea07f27d58bb1a45833ec782bbe..070a24d73d7b715c7113330f77b3501f097f8a73 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf @@ -27,6 +27,11 @@ Изменение документа "{0}" не поддерживается. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Основные параметры EditorConfig @@ -1312,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of Спецификации символов + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Файлы Visual Basic diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf index 10ecc114167289a99a0e37722b5638ddaa2c45b9..94c497291a8dce21f6652970210d6927cc1b13a5 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf @@ -27,6 +27,11 @@ Değişen belge '{0}' desteklenmiyor. + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Çekirdek EditorConfig seçenekleri @@ -1312,6 +1317,11 @@ Sıfır genişlikli pozitif geri yönlü onaylamalar genellikle normal ifadeleri Sembol belirtimleri + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Visual Basic dosyaları diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf index b2c818ce3ba68516f4942330cf470b2137670b04..492e07a8d58e22851f7d4ad2a4073ecca06229cc 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf @@ -27,6 +27,11 @@ 不支持更改文档“{0}”。 + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options Core EditorConfig 选项 @@ -1312,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of 符号规范 + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files visual basic 文件 diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf index 5b84849c8fc1cac714612895b41a2c43220d35e0..5f3848f42e03ad847749a3a2a09dd3edd15d0e05 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf @@ -27,6 +27,11 @@ 不支援變更文件 '{0}'。 + + Compilation is required to accomplish the task but is not supported by project {0}. + Compilation is required to accomplish the task but is not supported by project {0}. + + Core EditorConfig Options 核心 EditorConfig 選項 @@ -1312,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of 符號規格 + + Syntax tree is required to accomplish the task but is not supported by document {0}. + Syntax tree is required to accomplish the task but is not supported by document {0}. + + Visual Basic files Visual Basic 檔案 diff --git a/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_ExtensionMethodFiltering.cs b/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_ExtensionMethodFiltering.cs new file mode 100644 index 0000000000000000000000000000000000000000..ea52541c4c53c6613295bdd290043e8867429669 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_ExtensionMethodFiltering.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion.Providers; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.Remote +{ + internal partial class CodeAnalysisService : IRemoteExtensionMethodImportCompletionService + { + public Task<(IList, StatisticCounter)> GetUnimportedExtensionMethodsAsync( + DocumentId documentId, + int position, + string receiverTypeSymbolKeyData, + string[] namespaceInScope, + bool forceIndexCreation, + CancellationToken cancellationToken) + { + return RunServiceAsync(async () => + { + using (UserOperationBooster.Boost()) + { + var solution = await GetSolutionAsync(cancellationToken).ConfigureAwait(false); + var document = solution.GetDocument(documentId)!; + var compilation = (await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false)); + var symbol = SymbolKey.ResolveString(receiverTypeSymbolKeyData, compilation, cancellationToken: cancellationToken).GetAnySymbol(); + + if (symbol is ITypeSymbol receiverTypeSymbol) + { + var syntaxFacts = document.GetRequiredLanguageService(); + var namespaceInScopeSet = new HashSet(namespaceInScope, syntaxFacts.StringComparer); + + var (items, counter) = await ExtensionMethodImportCompletionHelper.GetUnimportedExtensionMethodsInCurrentProcessAsync( + document, position, receiverTypeSymbol, namespaceInScopeSet, forceIndexCreation, cancellationToken).ConfigureAwait(false); + return ((IList)items, counter); + } + + return (Array.Empty(), new StatisticCounter()); + } + }, cancellationToken); + } + } +} diff --git a/src/Workspaces/VisualBasic/Portable/FindSymbols/VisualBasicDeclaredSymbolInfoFactoryService.vb b/src/Workspaces/VisualBasic/Portable/FindSymbols/VisualBasicDeclaredSymbolInfoFactoryService.vb index 3cd546ba7b35969125c2a449846203ebe39cf106..4edb2b041c1e58f5c568c744ed4becb7f3929c91 100644 --- a/src/Workspaces/VisualBasic/Portable/FindSymbols/VisualBasicDeclaredSymbolInfoFactoryService.vb +++ b/src/Workspaces/VisualBasic/Portable/FindSymbols/VisualBasicDeclaredSymbolInfoFactoryService.vb @@ -2,9 +2,9 @@ Imports System.Collections.Immutable Imports System.Composition +Imports System.Runtime.CompilerServices Imports System.Text Imports Microsoft.CodeAnalysis -Imports Microsoft.CodeAnalysis.Collections Imports Microsoft.CodeAnalysis.FindSymbols Imports Microsoft.CodeAnalysis.Host.Mef Imports Microsoft.CodeAnalysis.LanguageServices @@ -16,6 +16,9 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols Friend Class VisualBasicDeclaredSymbolInfoFactoryService Inherits AbstractDeclaredSymbolInfoFactoryService + Private Const ExtensionName As String = "Extension" + Private Const ExtensionAttributeName As String = "ExtensionAttribute" + Public Sub New() End Sub @@ -106,11 +109,11 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols Return VisualBasicSyntaxFactsService.Instance.GetDisplayName(node, DisplayNameOptions.IncludeTypeParameters) End Function - Private Function GetFullyQualifiedContainerName(node As SyntaxNode) As String - Return VisualBasicSyntaxFactsService.Instance.GetDisplayName(node, DisplayNameOptions.IncludeNamespaces) + Private Function GetFullyQualifiedContainerName(node As SyntaxNode, rootNamespace As String) As String + Return VisualBasicSyntaxFactsService.Instance.GetDisplayName(node, DisplayNameOptions.IncludeNamespaces, rootNamespace) End Function - Public Overrides Function TryGetDeclaredSymbolInfo(stringTable As StringTable, node As SyntaxNode, ByRef declaredSymbolInfo As DeclaredSymbolInfo) As Boolean + Public Overrides Function TryGetDeclaredSymbolInfo(stringTable As StringTable, node As SyntaxNode, rootNamespace As String, ByRef declaredSymbolInfo As DeclaredSymbolInfo) As Boolean Select Case node.Kind() Case SyntaxKind.ClassBlock Dim classDecl = CType(node, ClassBlockSyntax) @@ -119,7 +122,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols classDecl.ClassStatement.Identifier.ValueText, GetTypeParameterSuffix(classDecl.ClassStatement.TypeParameterList), GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.Class, GetAccessibility(classDecl, classDecl.ClassStatement.Modifiers), classDecl.ClassStatement.Identifier.Span, @@ -132,7 +135,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols stringTable, enumDecl.EnumStatement.Identifier.ValueText, Nothing, GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.Enum, GetAccessibility(enumDecl, enumDecl.EnumStatement.Modifiers), enumDecl.EnumStatement.Identifier.Span, @@ -146,7 +149,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols interfaceDecl.InterfaceStatement.Identifier.ValueText, GetTypeParameterSuffix(interfaceDecl.InterfaceStatement.TypeParameterList), GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.Interface, GetAccessibility(interfaceDecl, interfaceDecl.InterfaceStatement.Modifiers), interfaceDecl.InterfaceStatement.Identifier.Span, @@ -160,7 +163,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols moduleDecl.ModuleStatement.Identifier.ValueText, GetTypeParameterSuffix(moduleDecl.ModuleStatement.TypeParameterList), GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.Module, GetAccessibility(moduleDecl, moduleDecl.ModuleStatement.Modifiers), moduleDecl.ModuleStatement.Identifier.Span, @@ -174,7 +177,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols structDecl.StructureStatement.Identifier.ValueText, GetTypeParameterSuffix(structDecl.StructureStatement.TypeParameterList), GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.Struct, GetAccessibility(structDecl, structDecl.StructureStatement.Modifiers), structDecl.StructureStatement.Identifier.Span, @@ -190,7 +193,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols typeBlock.BlockStatement.Identifier.ValueText, GetConstructorSuffix(constructor), GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.Constructor, GetAccessibility(constructor, constructor.SubNewStatement.Modifiers), constructor.SubNewStatement.NewKeyword.Span, @@ -206,7 +209,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols delegateDecl.Identifier.ValueText, GetTypeParameterSuffix(delegateDecl.TypeParameterList), GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.Delegate, GetAccessibility(delegateDecl, delegateDecl.Modifiers), delegateDecl.Identifier.Span, @@ -218,7 +221,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols stringTable, enumMember.Identifier.ValueText, Nothing, GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), + GetFullyQualifiedContainerName(node.Parent, rootNamespace), DeclaredSymbolInfoKind.EnumMember, Accessibility.Public, enumMember.Identifier.Span, @@ -232,7 +235,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols stringTable, eventDecl.Identifier.ValueText, Nothing, GetContainerDisplayName(eventParent), - GetFullyQualifiedContainerName(eventParent), + GetFullyQualifiedContainerName(eventParent, rootNamespace), DeclaredSymbolInfoKind.Event, GetAccessibility(statementOrBlock, eventDecl.Modifiers), eventDecl.Identifier.Span, @@ -245,8 +248,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols funcDecl.SubOrFunctionStatement.Identifier.ValueText, GetMethodSuffix(funcDecl), GetContainerDisplayName(node.Parent), - GetFullyQualifiedContainerName(node.Parent), - DeclaredSymbolInfoKind.Method, + GetFullyQualifiedContainerName(node.Parent, rootNamespace), + If(IsExtensionMethod(funcDecl), DeclaredSymbolInfoKind.ExtensionMethod, DeclaredSymbolInfoKind.Method), GetAccessibility(node, funcDecl.SubOrFunctionStatement.Modifiers), funcDecl.SubOrFunctionStatement.Identifier.Span, ImmutableArray(Of String).Empty, @@ -265,7 +268,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols stringTable, modifiedIdentifier.Identifier.ValueText, Nothing, GetContainerDisplayName(fieldDecl.Parent), - GetFullyQualifiedContainerName(fieldDecl.Parent), + GetFullyQualifiedContainerName(fieldDecl.Parent, rootNamespace), kind, GetAccessibility(fieldDecl, fieldDecl.Modifiers), modifiedIdentifier.Identifier.Span, ImmutableArray(Of String).Empty) @@ -279,7 +282,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols stringTable, propertyDecl.Identifier.ValueText, GetPropertySuffix(propertyDecl), GetContainerDisplayName(propertyParent), - GetFullyQualifiedContainerName(propertyParent), + GetFullyQualifiedContainerName(propertyParent, rootNamespace), DeclaredSymbolInfoKind.Property, GetAccessibility(statementOrBlock, propertyDecl.Modifiers), propertyDecl.Identifier.Span, @@ -291,6 +294,31 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols Return False End Function + Private Function IsExtensionMethod(node As MethodBlockSyntax) As Boolean + Dim parameterCount = node.SubOrFunctionStatement.ParameterList?.Parameters.Count + + ' Extension method must have at least one parameter and declared inside a module + If Not parameterCount.HasValue OrElse parameterCount.Value = 0 OrElse TypeOf node.Parent IsNot ModuleBlockSyntax Then + Return False + End If + + For Each attributeList In node.BlockStatement.AttributeLists + For Each attribute In attributeList.Attributes + ' ExtensionAttribute takes no argument. + If attribute.ArgumentList?.Arguments.Count > 0 Then + Continue For + End If + + Dim name = attribute.Name.GetRightmostName()?.ToString() + If String.Equals(name, ExtensionName, StringComparison.OrdinalIgnoreCase) OrElse String.Equals(name, ExtensionAttributeName, StringComparison.OrdinalIgnoreCase) Then + Return True + End If + Next + Next + + Return False + End Function + Private Function IsNestedType(node As DeclarationStatementSyntax) As Boolean Return TypeOf node.Parent Is TypeBlockSyntax End Function @@ -431,5 +459,122 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.FindSymbols First = False Next End Sub + + Public Overrides Function GetTargetTypeName(node As SyntaxNode) As String + Dim funcDecl = CType(node, MethodBlockSyntax) + Debug.Assert(IsExtensionMethod(funcDecl)) + + Dim typeParameterNames = funcDecl.SubOrFunctionStatement.TypeParameterList?.Parameters.SelectAsArray(Function(p) p.Identifier.Text) + Dim targetTypeName As String = Nothing + TryGetSimpleTypeNameWorker(funcDecl.BlockStatement.ParameterList.Parameters(0).AsClause?.Type, typeParameterNames, targetTypeName) + + Return targetTypeName + End Function + + Public Overrides Function TryGetAliasesFromUsingDirective(node As SyntaxNode, ByRef aliases As ImmutableArray(Of (aliasName As String, name As String))) As Boolean + + Dim importStatement = TryCast(node, ImportsStatementSyntax) + Dim builder = ArrayBuilder(Of (String, String)).GetInstance() + + If (importStatement IsNot Nothing) Then + For Each importsClause In importStatement.ImportsClauses + + If importsClause.Kind = SyntaxKind.SimpleImportsClause Then + Dim simpleImportsClause = DirectCast(importsClause, SimpleImportsClauseSyntax) + Dim aliasName, name As String + +#Disable Warning BC42030 ' Variable is passed by reference before it has been assigned a value + If simpleImportsClause.Alias IsNot Nothing AndAlso + TryGetSimpleTypeNameWorker(simpleImportsClause.Alias, Nothing, aliasName) AndAlso + TryGetSimpleTypeNameWorker(simpleImportsClause, Nothing, name) Then +#Enable Warning BC42030 ' Variable is passed by reference before it has been assigned a value + + builder.Add((aliasName, name)) + End If + End If + Next + + aliases = builder.ToImmutableAndFree() + Return True + End If + + aliases = Nothing + Return False + End Function + + Private Shared Function TryGetSimpleTypeNameWorker(node As SyntaxNode, typeParameterNames As ImmutableArray(Of String)?, ByRef simpleTypeName As String) As Boolean + If TypeOf node Is IdentifierNameSyntax Then + Dim identifierName = DirectCast(node, IdentifierNameSyntax) + Dim text = identifierName.Identifier.Text + simpleTypeName = If(typeParameterNames?.Contains(text), Nothing, text) + Return simpleTypeName IsNot Nothing + + ElseIf TypeOf node Is GenericNameSyntax Then + Dim genericName = DirectCast(node, GenericNameSyntax) + Dim name = genericName.Identifier.Text + Dim arity = genericName.Arity + simpleTypeName = If(arity = 0, name, name + GetMetadataAritySuffix(arity)) + Return True + + ElseIf TypeOf node Is QualifiedNameSyntax Then + ' For an identifier to the right of a '.', it can't be a type parameter, + ' so we don't need to check for it further. + Dim qualifiedName = DirectCast(node, QualifiedNameSyntax) + Return TryGetSimpleTypeNameWorker(qualifiedName.Right, Nothing, simpleTypeName) + + ElseIf TypeOf node Is NullableTypeSyntax Then + Return TryGetSimpleTypeNameWorker(DirectCast(node, NullableTypeSyntax).ElementType, typeParameterNames, simpleTypeName) + + ElseIf TypeOf node Is PredefinedTypeSyntax Then + simpleTypeName = GetSpecialTypeName(DirectCast(node, PredefinedTypeSyntax)) + Return simpleTypeName IsNot Nothing + End If + + simpleTypeName = Nothing + Return False + End Function + + Private Shared Function GetSpecialTypeName(predefinedTypeNode As PredefinedTypeSyntax) As String + Select Case predefinedTypeNode.Keyword.Kind() + Case SyntaxKind.BooleanKeyword + Return "Boolean" + Case SyntaxKind.ByteKeyword + Return "Byte" + Case SyntaxKind.CharKeyword + Return "Char" + Case SyntaxKind.DateKeyword + Return "DateTime" + Case SyntaxKind.DecimalKeyword + Return "Decimal" + Case SyntaxKind.DoubleKeyword + Return "Double" + Case SyntaxKind.IntegerKeyword + Return "Int32" + Case SyntaxKind.LongKeyword + Return "Int64" + Case SyntaxKind.ObjectKeyword + Return "Object" + Case SyntaxKind.SByteKeyword + Return "SByte" + Case SyntaxKind.ShortKeyword + Return "Int16" + Case SyntaxKind.SingleKeyword + Return "Single" + Case SyntaxKind.StringKeyword + Return "String" + Case SyntaxKind.UIntegerKeyword + Return "UInt32" + Case SyntaxKind.ULongKeyword + Return "UInt64" + Case SyntaxKind.UShortKeyword + Return "UInt16" + Case Else + Return Nothing + End Select + End Function + + Public Overrides Function GetRootNamespace(compilationOptions As CompilationOptions) As String + Return DirectCast(compilationOptions, VisualBasicCompilationOptions).RootNamespace + End Function End Class End Namespace diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index 3ff061f4d4b02174d39c188e848fa7f652de5d2b..27cbc1c11abf3f6d0ff111b9822870432d702b9b 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -1312,6 +1312,11 @@ Namespace Microsoft.CodeAnalysis.VisualBasic TryCast(node, MemberAccessExpressionSyntax)?.Name) End Function + Public Function GetLeftSideOfDot(node As SyntaxNode, Optional allowImplicitTarget As Boolean = False) As SyntaxNode Implements ISyntaxFactsService.GetLeftSideOfDot + Return If(TryCast(node, QualifiedNameSyntax)?.Left, + TryCast(node, MemberAccessExpressionSyntax)?.GetExpressionOfMemberAccessExpression(allowImplicitTarget)) + End Function + Public Function IsLeftSideOfExplicitInterfaceSpecifier(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsLeftSideOfExplicitInterfaceSpecifier Return IsLeftSideOfDot(node) AndAlso TryCast(node.Parent.Parent, ImplementsClauseSyntax) IsNot Nothing End Function @@ -2062,5 +2067,24 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Private Function ISyntaxFactsService_IsExpressionStatement(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsExpressionStatement Return MyBase.IsExpressionStatement(node) End Function + + Public Function IsUsingAliasDirective(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsUsingAliasDirective + Dim importStatement = TryCast(node, ImportsStatementSyntax) + + If (importStatement IsNot Nothing) Then + For Each importsClause In importStatement.ImportsClauses + + If importsClause.Kind = SyntaxKind.SimpleImportsClause Then + Dim simpleImportsClause = DirectCast(importsClause, SimpleImportsClauseSyntax) + + If simpleImportsClause.Alias IsNot Nothing Then + Return True + End If + End If + Next + End If + + Return False + End Function End Class End Namespace