Skip to content

Commit

Permalink
Add JSON source generator (#51300)
Browse files Browse the repository at this point in the history
* Add JSON source generator

* Address review feedback

* Address review feedback

* Align project structure with src-gen conventions

* Fix enum (de)serialization
  • Loading branch information
layomia authored Apr 19, 2021
1 parent ccc25a9 commit e98d043
Show file tree
Hide file tree
Showing 276 changed files with 5,107 additions and 129 deletions.
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

1 comment on commit e98d043

@Xyncgas
Copy link

Choose a reason for hiding this comment

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

so this is the root of all evil
just kidding

Please sign in to comment.