[release/6.0] Support duplicated type names with src gen (#58766)
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Wed, 8 Sep 2021 21:18:53 +0000 (15:18 -0600)
committerGitHub <noreply@github.com>
Wed, 8 Sep 2021 21:18:53 +0000 (15:18 -0600)
* Support duplicated type names with src gen

* Move helpers to common location

Co-authored-by: Steve Harter <steveharter@users.noreply.github.com>
src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs [new file with mode: 0644]
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs
src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs
src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs

diff --git a/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs b/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs
new file mode 100644 (file)
index 0000000..6a64948
--- /dev/null
@@ -0,0 +1,138 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+
+namespace System.Text.Json.Reflection
+{
+    internal static partial class RoslynExtensions
+    {
+        // Copied from: https://github.com/dotnet/roslyn/blob/main/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/CompilationExtensions.cs
+        /// <summary>
+        /// Gets a type by its metadata name to use for code analysis within a <see cref="Compilation"/>. This method
+        /// attempts to find the "best" symbol to use for code analysis, which is the symbol matching the first of the
+        /// following rules.
+        ///
+        /// <list type="number">
+        ///   <item><description>
+        ///     If only one type with the given name is found within the compilation and its referenced assemblies, that
+        ///     type is returned regardless of accessibility.
+        ///   </description></item>
+        ///   <item><description>
+        ///     If the current <paramref name="compilation"/> defines the symbol, that symbol is returned.
+        ///   </description></item>
+        ///   <item><description>
+        ///     If exactly one referenced assembly defines the symbol in a manner that makes it visible to the current
+        ///     <paramref name="compilation"/>, that symbol is returned.
+        ///   </description></item>
+        ///   <item><description>
+        ///     Otherwise, this method returns <see langword="null"/>.
+        ///   </description></item>
+        /// </list>
+        /// </summary>
+        /// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
+        /// <param name="fullyQualifiedMetadataName">The fully-qualified metadata type name to find.</param>
+        /// <returns>The symbol to use for code analysis; otherwise, <see langword="null"/>.</returns>
+        public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string fullyQualifiedMetadataName)
+        {
+            // Try to get the unique type with this name, ignoring accessibility
+            var type = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName);
+
+            // Otherwise, try to get the unique type with this name originally defined in 'compilation'
+            type ??= compilation.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName);
+
+            // Otherwise, try to get the unique accessible type with this name from a reference
+            if (type is null)
+            {
+                foreach (var module in compilation.Assembly.Modules)
+                {
+                    foreach (var referencedAssembly in module.ReferencedAssemblySymbols)
+                    {
+                        var currentType = referencedAssembly.GetTypeByMetadataName(fullyQualifiedMetadataName);
+                        if (currentType is null)
+                            continue;
+
+                        switch (currentType.GetResultantVisibility())
+                        {
+                            case SymbolVisibility.Public:
+                            case SymbolVisibility.Internal when referencedAssembly.GivesAccessTo(compilation.Assembly):
+                                break;
+
+                            default:
+                                continue;
+                        }
+
+                        if (type is object)
+                        {
+                            // Multiple visible types with the same metadata name are present
+                            return null;
+                        }
+
+                        type = currentType;
+                    }
+                }
+            }
+
+            return type;
+        }
+
+        // copied from https://github.com/dotnet/roslyn/blob/main/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs
+        private static SymbolVisibility GetResultantVisibility(this ISymbol symbol)
+        {
+            // Start by assuming it's visible.
+            SymbolVisibility visibility = SymbolVisibility.Public;
+
+            switch (symbol.Kind)
+            {
+                case SymbolKind.Alias:
+                    // Aliases are uber private.  They're only visible in the same file that they
+                    // were declared in.
+                    return SymbolVisibility.Private;
+
+                case SymbolKind.Parameter:
+                    // Parameters are only as visible as their containing symbol
+                    return GetResultantVisibility(symbol.ContainingSymbol);
+
+                case SymbolKind.TypeParameter:
+                    // Type Parameters are private.
+                    return SymbolVisibility.Private;
+            }
+
+            while (symbol != null && symbol.Kind != SymbolKind.Namespace)
+            {
+                switch (symbol.DeclaredAccessibility)
+                {
+                    // If we see anything private, then the symbol is private.
+                    case Accessibility.NotApplicable:
+                    case Accessibility.Private:
+                        return SymbolVisibility.Private;
+
+                    // If we see anything internal, then knock it down from public to
+                    // internal.
+                    case Accessibility.Internal:
+                    case Accessibility.ProtectedAndInternal:
+                        visibility = SymbolVisibility.Internal;
+                        break;
+
+                        // For anything else (Public, Protected, ProtectedOrInternal), the
+                        // symbol stays at the level we've gotten so far.
+                }
+
+                symbol = symbol.ContainingSymbol;
+            }
+
+            return visibility;
+        }
+
+        // Copied from: https://github.com/dotnet/roslyn/blob/main/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/SymbolVisibility.cs
+#pragma warning disable CA1027 // Mark enums with FlagsAttribute
+        private enum SymbolVisibility
+#pragma warning restore CA1027 // Mark enums with FlagsAttribute
+        {
+            Public = 0,
+            Internal = 1,
+            Private = 2,
+            Friend = Internal,
+        }
+    }
+}
index fecdb37..2c5f3eb 100644 (file)
@@ -32,6 +32,9 @@ namespace System.Text.Json.SourceGeneration
             private const string JsonNumberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonNumberHandlingAttribute";
             private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute";
             private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute";
+            private const string JsonSerializerContextFullName = "System.Text.Json.Serialization.JsonSerializerContext";
+            private const string JsonSerializerAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute";
+            private const string JsonSourceGenerationOptionsAttributeFullName = "System.Text.Json.Serialization.JsonSourceGenerationOptionsAttribute";
 
             private readonly Compilation _compilation;
             private readonly SourceProductionContext _sourceProductionContext;
@@ -145,9 +148,9 @@ namespace System.Text.Json.SourceGeneration
             public SourceGenerationSpec? GetGenerationSpec(ImmutableArray<ClassDeclarationSyntax> classDeclarationSyntaxList)
             {
                 Compilation compilation = _compilation;
-                INamedTypeSymbol jsonSerializerContextSymbol = compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializerContext");
-                INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializableAttribute");
-                INamedTypeSymbol jsonSourceGenerationOptionsAttributeSymbol = compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSourceGenerationOptionsAttribute");
+                INamedTypeSymbol jsonSerializerContextSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerContextFullName);
+                INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerAttributeFullName);
+                INamedTypeSymbol jsonSourceGenerationOptionsAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSourceGenerationOptionsAttributeFullName);
 
                 if (jsonSerializerContextSymbol == null || jsonSerializableAttributeSymbol == null || jsonSourceGenerationOptionsAttributeSymbol == null)
                 {
index c088cc6..189a842 100644 (file)
@@ -23,7 +23,7 @@ namespace System.Text.Json.Reflection
 
         public Type? Resolve(string fullyQualifiedMetadataName)
         {
-            INamedTypeSymbol? typeSymbol = _compilation.GetTypeByMetadataName(fullyQualifiedMetadataName);
+            INamedTypeSymbol? typeSymbol = _compilation.GetBestTypeByMetadataName(fullyQualifiedMetadataName);
             return typeSymbol.AsType(this);
         }
 
index 4e24797..aa3f431 100644 (file)
@@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis;
 
 namespace System.Text.Json.Reflection
 {
-    internal static class RoslynExtensions
+    internal static partial class RoslynExtensions
     {
         public static Type AsType(this ITypeSymbol typeSymbol, MetadataLoadContextInternal metadataLoadContext)
         {
@@ -67,3 +67,4 @@ namespace System.Text.Json.Reflection
         }
     }
 }
+
index de7b135..e92c127 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFrameworks>netstandard2.0</TargetFrameworks>
     <CLSCompliant>false</CLSCompliant>
@@ -37,6 +37,7 @@
     <Compile Include="..\Common\JsonSourceGenerationMode.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationMode.cs" />
     <Compile Include="..\Common\JsonSourceGenerationOptionsAttribute.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationOptionsAttribute.cs" />
     <Compile Include="..\Common\ReflectionExtensions.cs" Link="Common\System\Text\Json\Serialization\ReflectionExtensions.cs" />
+    <Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
     <Compile Include="ClassType.cs" />
     <Compile Include="CollectionType.cs" />
     <Compile Include="JsonConstants.cs" />
index f12e5b0..0aa811d 100644 (file)
@@ -3,6 +3,7 @@
 
 using System.Collections.Generic;
 using System.Collections.Immutable;
+using System.IO;
 using System.Linq;
 using System.Reflection;
 using Microsoft.CodeAnalysis;
@@ -467,5 +468,68 @@ namespace System.Text.Json.Serialization
         }
 
         // TODO: add test guarding against (de)serializing static classes.
+
+        [Fact]
+        public void TestMultipleDefinitions()
+        {
+            // Adding a dependency to an assembly that has internal definitions of public types
+            // should not result in a collision and break generation.
+            // This verifies the usage of GetBestTypeByMetadataName() instead of GetTypeByMetadataName().
+            var referencedSource = @"
+                namespace System.Text.Json.Serialization
+                {
+                    internal class JsonSerializerContext { }
+                    internal class JsonSerializableAttribute { }
+                    internal class JsonSourceGenerationOptionsAttribute { }
+                }";
+
+            // Compile the referenced assembly first.
+            Compilation referencedCompilation = CompilationHelper.CreateCompilation(referencedSource);
+
+            // Obtain the image of the referenced assembly.
+            byte[] referencedImage;
+            using (MemoryStream ms = new MemoryStream())
+            {
+                var emitResult = referencedCompilation.Emit(ms);
+                if (!emitResult.Success)
+                {
+                    throw new InvalidOperationException();
+                }
+                referencedImage = ms.ToArray();
+            }
+
+            // Generate the code
+            string source = @"
+                using System.Text.Json.Serialization;
+                namespace HelloWorld
+                {
+                    [JsonSerializable(typeof(HelloWorld.MyType))]
+                    internal partial class JsonContext : JsonSerializerContext
+                    {
+                    }
+
+                    public class MyType
+                    {
+                        public int MyInt { get; set; }
+                    }
+                }";
+
+            MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) };
+            Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences);
+            JsonSourceGenerator generator = new JsonSourceGenerator();
+
+            Compilation newCompilation = CompilationHelper.RunGenerators(
+                compilation,
+                out ImmutableArray<Diagnostic> generatorDiags, generator);
+
+            // Make sure compilation was successful.
+            Assert.Empty(generatorDiags.Where(diag => diag.Severity.Equals(DiagnosticSeverity.Error)));
+            Assert.Empty(newCompilation.GetDiagnostics().Where(diag => diag.Severity.Equals(DiagnosticSeverity.Error)));
+
+            // Should find the generated type.
+            Dictionary<string, Type> types = generator.GetSerializableTypes();
+            Assert.Equal(1, types.Count);
+            Assert.Equal("HelloWorld.MyType", types.Keys.First());
+        }
     }
 }