diff --git a/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs b/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs new file mode 100644 index 00000000000000..6a64948f3828a2 --- /dev/null +++ b/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs @@ -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 + /// + /// Gets a type by its metadata name to use for code analysis within a . This method + /// attempts to find the "best" symbol to use for code analysis, which is the symbol matching the first of the + /// following rules. + /// + /// + /// + /// If only one type with the given name is found within the compilation and its referenced assemblies, that + /// type is returned regardless of accessibility. + /// + /// + /// If the current defines the symbol, that symbol is returned. + /// + /// + /// If exactly one referenced assembly defines the symbol in a manner that makes it visible to the current + /// , that symbol is returned. + /// + /// + /// Otherwise, this method returns . + /// + /// + /// + /// The to consider for analysis. + /// The fully-qualified metadata type name to find. + /// The symbol to use for code analysis; otherwise, . + 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, + } + } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index cd47bcae0c0755..5dec808c207fc1 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -31,6 +31,9 @@ private sealed class Parser 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; @@ -144,9 +147,9 @@ public Parser(Compilation compilation, in SourceProductionContext sourceProducti public SourceGenerationSpec? GetGenerationSpec(ImmutableArray 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) { diff --git a/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs b/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs index c088cc6d7e1cbf..189a842f44f5c6 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs @@ -23,7 +23,7 @@ public MetadataLoadContextInternal(Compilation compilation) public Type? Resolve(string fullyQualifiedMetadataName) { - INamedTypeSymbol? typeSymbol = _compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); + INamedTypeSymbol? typeSymbol = _compilation.GetBestTypeByMetadataName(fullyQualifiedMetadataName); return typeSymbol.AsType(this); } diff --git a/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs index 4e2479784de13e..aa3f431fced8d0 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs @@ -7,7 +7,7 @@ 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 @@ public static MethodAttributes GetMethodAttributes(this IMethodSymbol methodSymb } } } + diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj index de7b135a86f188..e92c127bed8095 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 false @@ -37,6 +37,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index f12e5b07c2057b..0aa811d64fcce4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -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 @@ private void CheckFieldsPropertiesMethods(Type type, string[] expectedFields, st } // 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 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 types = generator.GetSerializableTypes(); + Assert.Equal(1, types.Count); + Assert.Equal("HelloWorld.MyType", types.Keys.First()); + } } }