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

[release/6.0-preview4] Add JSON source generator #51510

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ namespace System.Text.Json.Serialization
/// When specified on <see cref="JsonSerializerOptions.DefaultIgnoreCondition"/>,
/// determines when properties and fields across the type graph are ignored.
/// When specified on <see cref="JsonIgnoreAttribute.Condition"/>, controls whether
/// a property is ignored during serialization and deserialization. This option
/// a property or field is ignored during serialization and deserialization. This option
/// overrides the setting on <see cref="JsonSerializerOptions.DefaultIgnoreCondition"/>.
/// </summary>
public enum JsonIgnoreCondition
#if BUILDING_SOURCE_GENERATOR
internal
#else
public
#endif
enum JsonIgnoreCondition
{
/// <summary>
/// Property is never ignored during serialization or deserialization.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,31 @@ namespace System.Text.Json.Serialization
/// Determines how <see cref="JsonSerializer"/> handles numbers when serializing and deserializing.
/// </summary>
[Flags]
public enum JsonNumberHandling
#if BUILDING_SOURCE_GENERATOR
internal
#else
public
#endif
enum JsonNumberHandling
{
/// <summary>
/// Numbers will only be read from <see cref="JsonTokenType.Number"/> tokens and will only be written as JSON numbers (without quotes).
/// </summary>
Strict = 0x0,

/// <summary>
/// Numbers can be read from <see cref="JsonTokenType.String"/> tokens.
/// Does not prevent numbers from being read from <see cref="JsonTokenType.Number"/> token.
/// Strings that have escaped characters will be unescaped before reading.
/// Leading or trailing trivia within the string token, including whitespace, is not allowed.
/// </summary>
AllowReadingFromString = 0x1,

/// <summary>
/// Numbers will be written as JSON strings (with quotes), not as JSON numbers.
/// </summary>
WriteAsString = 0x2,

/// <summary>
/// The "NaN", "Infinity", and "-Infinity" <see cref="JsonTokenType.String"/> tokens can be read as
/// floating-point constants, and the <see cref="float"/> and <see cref="double"/> values for these
Expand Down
37 changes: 30 additions & 7 deletions src/libraries/System.Text.Json/System.Text.Json.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\Sys
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests.csproj", "{607D1960-1428-40D5-8AC4-D98E3C9BCF47}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3C544454-BD8B-44F4-A174-B61F18957613}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{EC8CE194-261A-4115-9582-E2DB1A25CAFB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{676B6044-FA47-4B7D-AEC2-FA94DB23A423}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{74017ACD-3AC1-4BB5-804B-D57E305FFBD9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}"
EndProject
Global
GlobalSection(NestedProjects) = preSolution
{102945CA-3736-4B2C-8E68-242A0B247F2B} = {3C544454-BD8B-44F4-A174-B61F18957613}
{607D1960-1428-40D5-8AC4-D98E3C9BCF47} = {3C544454-BD8B-44F4-A174-B61F18957613}
{73D5739C-E382-4E22-A7D3-B82705C58C74} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{25C42754-B384-4842-8FA7-75D7A79ADF0D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{4774F56D-16A8-4ABB-8C73-5F57609F1773} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
Expand All @@ -52,6 +59,10 @@ Global
{7909EB27-0D6E-46E6-B9F9-8A1EFD557018} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{6485EED4-C313-4551-9865-8ADCED603629} = {74017ACD-3AC1-4BB5-804B-D57E305FFBD9}
{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {3C544454-BD8B-44F4-A174-B61F18957613}
{33599A6C-F340-4E1B-9B4D-CB8946C22140} = {3C544454-BD8B-44F4-A174-B61F18957613}
{18173CEC-895F-4F62-B7BB-B724457FEDCD} = {3C544454-BD8B-44F4-A174-B61F18957613}
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -114,10 +125,22 @@ Global
{1285FF43-F491-4BE0-B92C-37DA689CBD4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1285FF43-F491-4BE0-B92C-37DA689CBD4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1285FF43-F491-4BE0-B92C-37DA689CBD4B}.Release|Any CPU.Build.0 = Release|Any CPU
{607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{607D1960-1428-40D5-8AC4-D98E3C9BCF47}.Release|Any CPU.Build.0 = Release|Any CPU
{6485EED4-C313-4551-9865-8ADCED603629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6485EED4-C313-4551-9865-8ADCED603629}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6485EED4-C313-4551-9865-8ADCED603629}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6485EED4-C313-4551-9865-8ADCED603629}.Release|Any CPU.Build.0 = Release|Any CPU
{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}.Release|Any CPU.Build.0 = Release|Any CPU
{33599A6C-F340-4E1B-9B4D-CB8946C22140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33599A6C-F340-4E1B-9B4D-CB8946C22140}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33599A6C-F340-4E1B-9B4D-CB8946C22140}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33599A6C-F340-4E1B-9B4D-CB8946C22140}.Release|Any CPU.Build.0 = Release|Any CPU
{18173CEC-895F-4F62-B7BB-B724457FEDCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18173CEC-895F-4F62-B7BB-B724457FEDCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
21 changes: 21 additions & 0 deletions src/libraries/System.Text.Json/gen/ClassType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;

namespace System.Text.Json.SourceGeneration
{
internal enum ClassType
{
TypeUnsupportedBySourceGen = 0,
Object = 1,
KnownType = 2,
TypeWithDesignTimeProvidedCustomConverter = 3,
Enumerable = 4,
Dictionary = 5,
Nullable = 6,
Enum = 7,
}
}
19 changes: 19 additions & 0 deletions src/libraries/System.Text.Json/gen/CollectionType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;

namespace System.Text.Json.SourceGeneration
{
internal enum CollectionType
{
NotApplicable = 0,
Array = 1,
List = 2,
IEnumerable = 3,
IList = 4,
Dictionary = 5
}
}
10 changes: 10 additions & 0 deletions src/libraries/System.Text.Json/gen/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices
{
/// <summary>
/// Dummy class so C# init-only properties can compile on NetStandard.
/// </summary>
internal sealed class IsExternalInit { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace System.Text.Json.SourceGeneration
{
internal sealed class JsonSerializableSyntaxReceiver : ISyntaxReceiver
{
public List<CompilationUnitSyntax> CompilationUnits { get; } = new();

public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is CompilationUnitSyntax compilationUnit)
{
CompilationUnits.Add(compilationUnit);
}
}
}
}
128 changes: 128 additions & 0 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json.SourceGeneration.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace System.Text.Json.SourceGeneration
{
/// <summary>
/// Generates source code to optimize serialization and deserialization with JsonSerializer.
/// </summary>
[Generator]
public sealed class JsonSourceGenerator : ISourceGenerator
{
private JsonSourceGeneratorHelper? _helper;

/// <summary>
/// Helper for unit tests.
/// </summary>
public Dictionary<string, Type>? SerializableTypes => _helper.GetSerializableTypes();

/// <summary>
/// Registers a syntax resolver to receive compilation units.
/// </summary>
/// <param name="context"></param>
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new JsonSerializableSyntaxReceiver());
}

/// <summary>
/// Generates source code to optimize serialization and deserialization with JsonSerializer.
/// </summary>
/// <param name="executionContext"></param>
public void Execute(GeneratorExecutionContext executionContext)
{
Compilation compilation = executionContext.Compilation;

const string JsonSerializableAttributeName = "System.Text.Json.Serialization.JsonSerializableAttribute";
INamedTypeSymbol jsonSerializableAttribute = compilation.GetTypeByMetadataName(JsonSerializableAttributeName);
if (jsonSerializableAttribute == null)
{
return;
}

JsonSerializableSyntaxReceiver receiver = (JsonSerializableSyntaxReceiver)executionContext.SyntaxReceiver;
MetadataLoadContextInternal metadataLoadContext = new(compilation);

TypeExtensions.NullableOfTType = metadataLoadContext.Resolve(typeof(Nullable<>));

JsonSourceGeneratorHelper helper = new(executionContext, metadataLoadContext);
_helper = helper;

// Discover serializable types indicated by JsonSerializableAttribute.
foreach (CompilationUnitSyntax compilationUnit in receiver.CompilationUnits)
{
SemanticModel compilationSemanticModel = executionContext.Compilation.GetSemanticModel(compilationUnit.SyntaxTree);

foreach (AttributeListSyntax attributeListSyntax in compilationUnit.AttributeLists)
{
AttributeSyntax attributeSyntax = attributeListSyntax.Attributes.First();
IMethodSymbol attributeSymbol = compilationSemanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol;

if (attributeSymbol == null || !jsonSerializableAttribute.Equals(attributeSymbol.ContainingType, SymbolEqualityComparer.Default))
{
// Not the right attribute.
continue;
}

// Get JsonSerializableAttribute arguments.
IEnumerable<SyntaxNode> attributeArguments = attributeSyntax.DescendantNodes().Where(node => node is AttributeArgumentSyntax);

ITypeSymbol? typeSymbol = null;
string? typeInfoPropertyName = null;

int i = 0;
foreach (AttributeArgumentSyntax node in attributeArguments)
{
if (i == 0)
{
TypeOfExpressionSyntax? typeNode = node.ChildNodes().Single() as TypeOfExpressionSyntax;
if (typeNode != null)
{
ExpressionSyntax typeNameSyntax = (ExpressionSyntax)typeNode.ChildNodes().Single();
typeSymbol = compilationSemanticModel.GetTypeInfo(typeNameSyntax).ConvertedType;
}
}
else if (i == 1)
{
// Obtain the optional TypeInfoPropertyName string property on the attribute, if present.
SyntaxNode? typeInfoPropertyNameNode = node.ChildNodes().ElementAtOrDefault(1);
if (typeInfoPropertyNameNode != null)
{
typeInfoPropertyName = typeInfoPropertyNameNode.GetFirstToken().ValueText;
}
}

i++;
}

if (typeSymbol == null)
{
continue;
}


Type type = new TypeWrapper(typeSymbol, metadataLoadContext);
if (type.Namespace == "<global namespace>")
{
// typeof() reference where the type's name isn't fully qualified.
// The compilation is not valid and the user needs to fix their code.
// The compiler will notify the user so we don't have to.
return;
}

helper.RegisterRootSerializableType(type, typeInfoPropertyName);
}
}

helper.GenerateSerializationMetadata();
}
}
}
Loading