Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix support for JsonSerializerContext contained in arbitrary types. #87829

Merged
merged 4 commits into from
Jun 21, 2023
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
28 changes: 28 additions & 0 deletions src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;

namespace System.Text.Json.SourceGeneration
Expand Down Expand Up @@ -256,5 +257,32 @@ public static INamedTypeSymbol[] GetSortedTypeHierarchy(this ITypeSymbol type)
return JsonHelpers.TraverseGraphWithTopologicalSort<INamedTypeSymbol>(namedType, static t => t.AllInterfaces, SymbolEqualityComparer.Default);
}
}

/// <summary>
/// Returns the kind keyword corresponding to the specified declaration syntax node.
/// </summary>
public static string GetTypeKindKeyword(this TypeDeclarationSyntax typeDeclaration)
{
switch (typeDeclaration.Kind())
{
case SyntaxKind.ClassDeclaration:
return "class";
case SyntaxKind.InterfaceDeclaration:
return "interface";
case SyntaxKind.StructDeclaration:
return "struct";
case SyntaxKind.RecordDeclaration:
return "record";
case SyntaxKind.RecordStructDeclaration:
return "record struct";
case SyntaxKind.EnumDeclaration:
return "enum";
case SyntaxKind.DelegateDeclaration:
return "delegate";
default:
Debug.Fail("unexpected syntax kind");
return null;
}
}
}
}
2 changes: 0 additions & 2 deletions src/libraries/System.Text.Json/gen/JsonConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ namespace System.Text.Json
{
internal static partial class JsonConstants
{
public const string GlobalNamespaceValue = "<global namespace>";

public const string SystemTextJsonSourceGenerationName = "System.Text.Json.SourceGeneration";

public const string IJsonOnSerializedFullName = "System.Text.Json.Serialization.IJsonOnSerialized";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private static SourceWriter CreateSourceWriterWithContextHeader(ContextGeneratio

""");

if (contextSpec.Namespace != JsonConstants.GlobalNamespaceValue)
if (contextSpec.Namespace != null)
{
writer.WriteLine($"namespace {contextSpec.Namespace}");
writer.WriteLine('{');
Expand Down
126 changes: 28 additions & 98 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ public Parser(KnownTypeSymbols knownSymbols)
Debug.Assert(_typesToGenerate.Count == 0);
Debug.Assert(_generatedTypes.Count == 0);

if (!DerivesFromJsonSerializerContext(contextClassDeclaration, _knownSymbols.JsonSerializerContextType, semanticModel, cancellationToken))
INamedTypeSymbol? contextTypeSymbol = semanticModel.GetDeclaredSymbol(contextClassDeclaration, cancellationToken);
Debug.Assert(contextTypeSymbol != null);

if (!_knownSymbols.JsonSerializerContextType.IsAssignableFrom(contextTypeSymbol))
{
return null;
}

INamedTypeSymbol? contextTypeSymbol = semanticModel.GetDeclaredSymbol(contextClassDeclaration, cancellationToken);
Debug.Assert(contextTypeSymbol != null);

if (!TryParseJsonSerializerContextAttributes(
contextTypeSymbol,
out List<TypeToGenerate>? rootSerializableTypes,
Expand All @@ -105,7 +105,7 @@ public Parser(KnownTypeSymbols knownSymbols)
}

Location contextLocation = contextClassDeclaration.GetLocation();
if (!TryGetClassDeclarationList(contextTypeSymbol, out List<string>? classDeclarationList))
if (!TryGetNestedTypeDeclarations(contextClassDeclaration, semanticModel, cancellationToken, out List<string>? classDeclarationList))
{
// Class or one of its containing types is not partial so we can't add to it.
ReportDiagnostic(DiagnosticDescriptors.ContextClassesMustBePartial, contextLocation, contextTypeSymbol.Name);
Expand Down Expand Up @@ -138,7 +138,7 @@ public Parser(KnownTypeSymbols knownSymbols)
{
ContextType = new(contextTypeSymbol),
GeneratedTypes = _generatedTypes.Values.OrderBy(t => t.TypeRef.FullyQualifiedName).ToImmutableEquatableArray(),
Namespace = contextTypeSymbol.ContainingNamespace.ToDisplayString(),
Namespace = contextTypeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null,
ContextClassDeclarations = classDeclarationList.ToImmutableEquatableArray(),
DefaultIgnoreCondition = options.DefaultIgnoreCondition,
IgnoreReadOnlyFields = options.IgnoreReadOnlyFields,
Expand All @@ -154,112 +154,42 @@ public Parser(KnownTypeSymbols knownSymbols)
return contextGenSpec;
}

// Returns true if a given type derives directly from JsonSerializerContext.
private static bool DerivesFromJsonSerializerContext(
ClassDeclarationSyntax classDeclarationSyntax,
INamedTypeSymbol jsonSerializerContextSymbol,
SemanticModel compilationSemanticModel,
CancellationToken cancellationToken)
private static bool TryGetNestedTypeDeclarations(ClassDeclarationSyntax contextClassSyntax, SemanticModel semanticModel, CancellationToken cancellationToken, [NotNullWhen(true)] out List<string>? typeDeclarations)
{
SeparatedSyntaxList<BaseTypeSyntax>? baseTypeSyntaxList = classDeclarationSyntax.BaseList?.Types;
if (baseTypeSyntaxList == null)
{
return false;
}

INamedTypeSymbol? match = null;
typeDeclarations = null;

foreach (BaseTypeSyntax baseTypeSyntax in baseTypeSyntaxList)
for (TypeDeclarationSyntax? currentType = contextClassSyntax; currentType != null; currentType = currentType.Parent as TypeDeclarationSyntax)
{
INamedTypeSymbol? candidate = compilationSemanticModel.GetSymbolInfo(baseTypeSyntax.Type, cancellationToken).Symbol as INamedTypeSymbol;
if (candidate != null && jsonSerializerContextSymbol.Equals(candidate, SymbolEqualityComparer.Default))
StringBuilder stringBuilder = new();
bool isPartialType = false;

foreach (SyntaxToken modifier in currentType.Modifiers)
{
match = candidate;
break;
stringBuilder.Append(modifier.Text);
stringBuilder.Append(' ');
isPartialType |= modifier.IsKind(SyntaxKind.PartialKeyword);
}
}

return match != null;
}

private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out List<string>? classDeclarationList)
{
INamedTypeSymbol currentSymbol = typeSymbol;
classDeclarationList = null;

while (currentSymbol != null)
{
ClassDeclarationSyntax? classDeclarationSyntax = currentSymbol.DeclaringSyntaxReferences.First().GetSyntax() as ClassDeclarationSyntax;

if (classDeclarationSyntax != null)
if (!isPartialType)
{
SyntaxTokenList tokenList = classDeclarationSyntax.Modifiers;
int tokenCount = tokenList.Count;

bool isPartial = false;

string[] declarationElements = new string[tokenCount + 2];

for (int i = 0; i < tokenCount; i++)
{
SyntaxToken token = tokenList[i];
declarationElements[i] = token.Text;

if (token.IsKind(SyntaxKind.PartialKeyword))
{
isPartial = true;
}
}

if (!isPartial)
{
classDeclarationList = null;
return false;
}

declarationElements[tokenCount] = "class";
declarationElements[tokenCount + 1] = GetClassDeclarationName(currentSymbol);

(classDeclarationList ??= new List<string>()).Add(string.Join(" ", declarationElements));
typeDeclarations = null;
return false;
}

currentSymbol = currentSymbol.ContainingType;
}
stringBuilder.Append(currentType.GetTypeKindKeyword());
stringBuilder.Append(' ');

Debug.Assert(classDeclarationList?.Count > 0);
return true;
}
INamedTypeSymbol? typeSymbol = semanticModel.GetDeclaredSymbol(currentType, cancellationToken);
Debug.Assert(typeSymbol != null);

private static string GetClassDeclarationName(INamedTypeSymbol typeSymbol)
{
if (typeSymbol.TypeArguments.Length == 0)
{
return typeSymbol.Name;
}
string typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
stringBuilder.Append(typeName);

StringBuilder sb = new StringBuilder();

sb.Append(typeSymbol.Name);
sb.Append('<');

bool first = true;
foreach (ITypeSymbol typeArg in typeSymbol.TypeArguments)
{
if (!first)
{
sb.Append(", ");
}
else
{
first = false;
}

sb.Append(typeArg.Name);
(typeDeclarations ??= new()).Add(stringBuilder.ToString());
}

sb.Append('>');

return sb.ToString();
Debug.Assert(typeDeclarations?.Count > 0);
return true;
}

private TypeRef EnqueueType(ITypeSymbol type, JsonSourceGenerationMode? generationMode, string? typeInfoPropertyName = null, Location? attributeLocation = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,5 +800,50 @@ public class NestedGenericClass<T2>
result.AssertContainsType("global::HelloWorld.MyGenericClass<string>.NestedGenericClass<int>");
result.AssertContainsType("string");
}

[Theory]
[InlineData("public sealed partial class MySealedClass")]
[InlineData("public partial class MyGenericClass<T>")]
[InlineData("public partial interface IMyInterface")]
[InlineData("public partial interface IMyGenericInterface<T, U>")]
[InlineData("public partial struct MyStruct")]
[InlineData("public partial struct MyGenericStruct<T>")]
[InlineData("public ref partial struct MyRefStruct")]
[InlineData("public ref partial struct MyGenericRefStruct<T>")]
[InlineData("public readonly partial struct MyReadOnlyStruct")]
[InlineData("public readonly ref partial struct MyReadOnlyRefStruct")]
#if ROSLYN4_0_OR_GREATER && NETCOREAPP
[InlineData("public partial record MyRecord(int x)")]
[InlineData("public partial record struct MyRecordStruct(int x)")]
#endif
public void NestedContextsAreSupported(string containingTypeDeclarationHeader)
{
string source = $$"""
using System.Text.Json.Serialization;

namespace HelloWorld
{
{{containingTypeDeclarationHeader}}
{
[JsonSerializable(typeof(MyClass))]
internal partial class JsonContext : JsonSerializerContext
{
}
}

public class MyClass
{
}
}
""";

Compilation compilation = CompilationHelper.CreateCompilation(source);

JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);

// Make sure compilation was successful.
Assert.Empty(result.NewCompilation.GetDiagnostics());
Assert.Empty(result.Diagnostics);
}
}
}