Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -144,9 +147,9 @@ public Parser(Compilation compilation, in SourceProductionContext sourceProducti
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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -67,3 +67,4 @@ public static MethodAttributes GetMethodAttributes(this IMethodSymbol methodSymb
}
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<CLSCompliant>false</CLSCompliant>
Expand Down Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -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();
Comment on lines +490 to +498
Copy link
Member

Choose a reason for hiding this comment

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

Very minor nit: this could have been a helper method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks; left as-is. Note there are other tests that have the same pattern.

}

// 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());
}
}
}