Skip to content

Commit 816409c

Browse files
authored
Json serializer type discovery (dotnet#18)
* Create JsonSerializableAttribute attribute for type discovery * Include reflection utils and implement basic type discovery * Update unit tests for TypeDiscovery * Use linq for cleaner JsonSerializationSyntaxReceiver * Update license for reflection utils * Update tests and generator to latest model * Add external class unit tests for Type Discovery * Update summary, variable names and other nits * Update csharp coding style applied to vars * Separated Unit Tests into helper classes and minor changes for nullables in Syntax Receiver * Create test cases checking full type wrapper in end to end and change typewrapper to show private methods * Update dicitonary to list for syntax receiver * Separate syntax receiver to new file
1 parent 8508a99 commit 816409c

22 files changed

Lines changed: 1428 additions & 44 deletions

src/libraries/System.Text.Json/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,90 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Generic;
6+
using System.Text.Json.Serialization;
57
using Xunit;
68

79
namespace System.Text.Json.SourceGeneration.Tests
810
{
9-
public class JsonSerializerSouceGeneratorTests
11+
public class JsonSerializerSourceGeneratorTests
1012
{
13+
[JsonSerializable]
14+
public class SampleInternalTest
15+
{
16+
public char PublicCharField;
17+
private string PrivateStringField;
18+
public int PublicIntPropertyPublic { get; set; }
19+
public int PublicIntPropertyPrivateSet { get; private set; }
20+
public int PublicIntPropertyPrivateGet { private get; set; }
21+
22+
public SampleInternalTest()
23+
{
24+
PublicCharField = 'a';
25+
PrivateStringField = "privateStringField";
26+
}
27+
28+
public SampleInternalTest(char c, string s)
29+
{
30+
PublicCharField = c;
31+
PrivateStringField = s;
32+
}
33+
34+
private SampleInternalTest(int i)
35+
{
36+
PublicIntPropertyPublic = i;
37+
}
38+
39+
private void UseFields()
40+
{
41+
string use = PublicCharField.ToString() + PrivateStringField;
42+
}
43+
}
44+
45+
[JsonSerializable(typeof(JsonConverterAttribute))]
46+
public class SampleExternalTest { }
47+
1148
[Fact]
12-
public static void TestGeneratedCode()
49+
public void TestGeneratedCode()
1350
{
14-
Assert.Equal("Hello", HelloWorldGenerated.HelloWorld.SayHello());
51+
var internalTypeTest = new HelloWorldGenerated.SampleInternalTestClassInfo();
52+
var externalTypeTest = new HelloWorldGenerated.SampleExternalTestClassInfo();
53+
54+
// Check base class names.
55+
Assert.Equal("SampleInternalTestClassInfo", internalTypeTest.GetClassName());
56+
Assert.Equal("SampleExternalTestClassInfo", externalTypeTest.GetClassName());
57+
58+
// Public and private Ctors are visible.
59+
Assert.Equal(3, internalTypeTest.Ctors.Count);
60+
Assert.Equal(2, externalTypeTest.Ctors.Count);
61+
62+
// Ctor params along with its types are visible.
63+
Dictionary<string, string> expectedCtorParamsInternal = new Dictionary<string, string> { { "c", "Char"}, { "s", "String" }, { "i", "Int32" } };
64+
Assert.Equal(expectedCtorParamsInternal, internalTypeTest.CtorParams);
65+
66+
Dictionary<string, string> expectedCtorParamsExternal = new Dictionary<string, string> { { "converterType", "Type"} };
67+
Assert.Equal(expectedCtorParamsExternal, externalTypeTest.CtorParams);
68+
69+
// Public and private methods are visible.
70+
List<string> expectedMethodsInternal = new List<string> { "get_PublicIntPropertyPublic", "set_PublicIntPropertyPublic", "get_PublicIntPropertyPrivateSet", "set_PublicIntPropertyPrivateSet", "get_PublicIntPropertyPrivateGet", "set_PublicIntPropertyPrivateGet", "UseFields" };
71+
Assert.Equal(expectedMethodsInternal, internalTypeTest.Methods);
72+
73+
List<string> expectedMethodsExternal = new List<string> { "get_ConverterType", "CreateConverter" };
74+
Assert.Equal(expectedMethodsExternal, externalTypeTest.Methods);
75+
76+
// Public and private fields are visible.
77+
Dictionary<string, string> expectedFieldsInternal = new Dictionary<string, string> { { "PublicCharField", "Char" }, { "PrivateStringField", "String" } };
78+
Assert.Equal(expectedFieldsInternal, internalTypeTest.Fields);
79+
80+
Dictionary<string, string> expectedFieldsExternal = new Dictionary<string, string> { };
81+
Assert.Equal(expectedFieldsExternal, externalTypeTest.Fields);
82+
83+
// Public properties are visible.
84+
Dictionary<string, string> expectedPropertiesInternal = new Dictionary<string, string> { { "PublicIntPropertyPublic", "Int32" }, { "PublicIntPropertyPrivateSet", "Int32" }, { "PublicIntPropertyPrivateGet", "Int32" } };
85+
Assert.Equal(expectedPropertiesInternal, internalTypeTest.Properties);
86+
87+
Dictionary<string, string> expectedPropertiesExternal = new Dictionary<string, string> { { "ConverterType", "Type"} };
88+
Assert.Equal(expectedPropertiesExternal, externalTypeTest.Properties);
1589
}
1690
}
1791
}

src/libraries/System.Text.Json/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
44
</PropertyGroup>

src/libraries/System.Text.Json/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs

Lines changed: 157 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,181 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using System.Reflection;
9+
using System.Text.Json.Serialization;
510
using Microsoft.CodeAnalysis;
6-
using Microsoft.CodeAnalysis.Diagnostics;
11+
using Microsoft.CodeAnalysis.CSharp;
712
using Xunit;
813

914
namespace System.Text.Json.SourceGeneration.UnitTests
1015
{
11-
public static class GeneratorTests
16+
public class GeneratorTests
1217
{
1318
[Fact]
14-
public static void SourceGeneratorInitializationPass()
19+
public void TypeDiscoveryPrimitivePOCO()
1520
{
21+
string source = @"
22+
using System;
23+
using System.Text.Json.Serialization;
24+
25+
namespace HelloWorld
26+
{
27+
[JsonSerializable]
28+
public class MyType {
29+
public int PublicPropertyInt { get; set; }
30+
public string PublicPropertyString { get; set; }
31+
private int PrivatePropertyInt { get; set; }
32+
private string PrivatePropertyString { get; set; }
33+
34+
public double PublicDouble;
35+
public char PublicChar;
36+
private double PrivateDouble;
37+
private char PrivateChar;
38+
39+
public void MyMethod() { }
40+
public void MySecondMethod() { }
41+
}
42+
}";
43+
44+
Compilation compilation = CreateCompilation(source);
45+
46+
JsonSerializerSourceGenerator generator = new JsonSerializerSourceGenerator();
47+
48+
Compilation outCompilation = RunGenerators(compilation, out var generatorDiags, generator);
49+
50+
// Check base functionality of found types.
51+
Assert.Equal(1, generator.foundTypes.Count);
52+
Assert.Equal("HelloWorld.MyType", generator.foundTypes["MyType"].FullName);
53+
54+
// Check for received properties in created type.
55+
string[] expectedPropertyNames = { "PublicPropertyInt", "PublicPropertyString", "PrivatePropertyInt", "PrivatePropertyString" };
56+
string[] receivedPropertyNames = generator.foundTypes["MyType"].GetProperties().Select(property => property.Name).ToArray();
57+
Assert.Equal(expectedPropertyNames, receivedPropertyNames);
58+
59+
// Check for fields in created type.
60+
string[] expectedFieldNames = { "PublicDouble", "PublicChar", "PrivateDouble", "PrivateChar" };
61+
string[] receivedFieldNames = generator.foundTypes["MyType"].GetFields().Select(field => field.Name).ToArray();
62+
Assert.Equal(expectedFieldNames, receivedFieldNames);
63+
64+
// Check for methods in created type.
65+
string[] expectedMethodNames = { "get_PublicPropertyInt", "set_PublicPropertyInt", "get_PublicPropertyString", "set_PublicPropertyString", "get_PrivatePropertyInt", "set_PrivatePropertyInt", "get_PrivatePropertyString", "set_PrivatePropertyString", "MyMethod", "MySecondMethod" };
66+
string[] receivedMethodNames = generator.foundTypes["MyType"].GetMethods().Select(method => method.Name).ToArray();
67+
Assert.Equal(expectedMethodNames, receivedMethodNames);
1668
}
1769

1870
[Fact]
19-
public static void SourceGeneratorInitializationFail()
71+
public void TypeDiscoveryPrimitiveTemporaryPOCO()
2072
{
73+
string source = @"
74+
using System;
75+
using System.Text.Json.Serialization;
76+
77+
namespace HelloWorld
78+
{
79+
[JsonSerializable]
80+
public class MyType {
81+
public int PublicPropertyInt { get; set; }
82+
public string PublicPropertyString { get; set; }
83+
private int PrivatePropertyInt { get; set; }
84+
private string PrivatePropertyString { get; set; }
85+
86+
public double PublicDouble;
87+
public char PublicChar;
88+
private double PrivateDouble;
89+
private char PrivateChar;
90+
91+
public void MyMethod() { }
92+
public void MySecondMethod() { }
93+
}
94+
95+
[JsonSerializable(typeof(JsonConverterAttribute))]
96+
public class NotMyType { }
97+
98+
}";
99+
100+
Compilation compilation = CreateCompilation(source);
101+
102+
JsonSerializerSourceGenerator generator = new JsonSerializerSourceGenerator();
103+
104+
Compilation outCompilation = RunGenerators(compilation, out var generatorDiags, generator);
105+
106+
// Check base functionality of found types.
107+
Assert.Equal(2, generator.foundTypes.Count);
108+
109+
// Check for MyType.
110+
Assert.Equal("HelloWorld.MyType", generator.foundTypes["MyType"].FullName);
111+
112+
// Check for received properties in created type.
113+
string[] expectedPropertyNamesMyType = { "PublicPropertyInt", "PublicPropertyString", "PrivatePropertyInt", "PrivatePropertyString" };
114+
string[] receivedPropertyNamesMyType = generator.foundTypes["MyType"].GetProperties().Select(property => property.Name).ToArray();
115+
Assert.Equal(expectedPropertyNamesMyType, receivedPropertyNamesMyType);
116+
117+
// Check for fields in created type.
118+
string[] expectedFieldNamesMyType = { "PublicDouble", "PublicChar", "PrivateDouble", "PrivateChar" };
119+
string[] receivedFieldNamesMyType = generator.foundTypes["MyType"].GetFields().Select(field => field.Name).ToArray();
120+
Assert.Equal(expectedFieldNamesMyType, receivedFieldNamesMyType);
121+
122+
// Check for methods in created type.
123+
string[] expectedMethodNamesMyType = { "get_PublicPropertyInt", "set_PublicPropertyInt", "get_PublicPropertyString", "set_PublicPropertyString", "get_PrivatePropertyInt", "set_PrivatePropertyInt", "get_PrivatePropertyString", "set_PrivatePropertyString", "MyMethod", "MySecondMethod" };
124+
string[] receivedMethodNamesMyType = generator.foundTypes["MyType"].GetMethods().Select(method => method.Name).ToArray();
125+
Assert.Equal(expectedMethodNamesMyType, receivedMethodNamesMyType);
126+
127+
// Check for NotMyType.
128+
Assert.Equal("System.Text.Json.Serialization.JsonConverterAttribute", generator.foundTypes["NotMyType"].FullName);
129+
130+
// Check for received properties in created type.
131+
string[] expectedPropertyNamesNotMyType = { "ConverterType" };
132+
string[] receivedPropertyNamesNotMyType = generator.foundTypes["NotMyType"].GetProperties().Select(property => property.Name).ToArray();
133+
Assert.Equal(expectedPropertyNamesNotMyType, receivedPropertyNamesNotMyType);
134+
135+
// Check for fields in created type.
136+
string[] expectedFieldNamesNotMyType = { };
137+
string[] receivedFieldNamesNotMyType = generator.foundTypes["NotMyType"].GetFields().Select(field => field.Name).ToArray();
138+
Assert.Equal(expectedFieldNamesNotMyType, receivedFieldNamesNotMyType);
139+
140+
// Check for methods in created type.
141+
string[] expectedMethodNamesNotMyType = { "get_ConverterType", "CreateConverter" };
142+
string[] receivedMethodNamesNotMyType = generator.foundTypes["NotMyType"].GetMethods().Select(method => method.Name).ToArray();
143+
Assert.Equal(expectedMethodNamesNotMyType, receivedMethodNamesNotMyType);
21144
}
22145

23-
[Fact]
24-
public static void SourceGeneratorExecutionPass()
146+
private Compilation CreateCompilation(string source)
25147
{
148+
// Bypass System.Runtime error.
149+
Assembly systemRuntimeAssembly = Assembly.Load("System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
150+
string systemRuntimeAssemblyPath = systemRuntimeAssembly.Location;
151+
152+
MetadataReference[] references = new MetadataReference[] {
153+
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
154+
MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location),
155+
MetadataReference.CreateFromFile(typeof(JsonSerializableAttribute).Assembly.Location),
156+
MetadataReference.CreateFromFile(typeof(JsonSerializerOptions).Assembly.Location),
157+
MetadataReference.CreateFromFile(typeof(Type).Assembly.Location),
158+
MetadataReference.CreateFromFile(typeof(KeyValuePair).Assembly.Location),
159+
MetadataReference.CreateFromFile(systemRuntimeAssemblyPath),
160+
};
161+
162+
return CSharpCompilation.Create(
163+
"TestAssembly",
164+
syntaxTrees: new[] { CSharpSyntaxTree.ParseText(source) },
165+
references: references,
166+
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
167+
);
26168
}
27169

28-
[Fact]
29-
public static void SourceGeneratorExecutionFail()
170+
private GeneratorDriver CreateDriver(Compilation compilation, params ISourceGenerator[] generators)
171+
=> new CSharpGeneratorDriver(
172+
new CSharpParseOptions(kind: SourceCodeKind.Regular, documentationMode: DocumentationMode.Parse),
173+
ImmutableArray.Create(generators),
174+
ImmutableArray<AdditionalText>.Empty);
175+
176+
private Compilation RunGenerators(Compilation compilation, out ImmutableArray<Diagnostic> diagnostics, params ISourceGenerator[] generators)
30177
{
178+
CreateDriver(compilation, generators).RunFullGeneration(compilation, out Compilation outCompilation, out diagnostics);
179+
return outCompilation;
31180
}
32181
}
33182
}

src/libraries/System.Text.Json/System.Text.Json.SourceGeneration.UnitTests/System.Text.Json.SourceGeneration.UnitTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
<ItemGroup>
77
<PackageReference Include="Microsoft.CodeAnalysis" Version="$(MicrosoftCodeAnalysisVersion)" PrivateAssets="all" />
8+
9+
<ProjectReference Include="..\src\System.Text.Json.csproj" />
810
<ProjectReference Include="..\System.Text.Json.SourceGeneration\System.Text.Json.SourceGeneration.csproj" />
911
</ItemGroup>
1012

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
11+
namespace System.Text.Json.SourceGeneration
12+
{
13+
public class JsonSerializableSyntaxReceiver : ISyntaxReceiver
14+
{
15+
public List<KeyValuePair<string, IdentifierNameSyntax>> ExternalClassTypes = new List<KeyValuePair<string, IdentifierNameSyntax>>();
16+
public List<KeyValuePair<string, TypeDeclarationSyntax>> InternalClassTypes = new List<KeyValuePair<string, TypeDeclarationSyntax>>();
17+
18+
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
19+
{
20+
// Look for classes or structs for JsonSerializable Attribute.
21+
if (syntaxNode is ClassDeclarationSyntax || syntaxNode is StructDeclarationSyntax)
22+
{
23+
// Find JsonSerializable Attributes.
24+
IEnumerable<AttributeSyntax>? serializableAttributes = null;
25+
AttributeListSyntax attributeList = ((TypeDeclarationSyntax)syntaxNode).AttributeLists.SingleOrDefault();
26+
if (attributeList != null)
27+
{
28+
serializableAttributes = attributeList.Attributes.Where(node => (node is AttributeSyntax attr && attr.Name.ToString() == "JsonSerializable")).Cast<AttributeSyntax>();
29+
}
30+
31+
if (serializableAttributes?.Any() == true)
32+
{
33+
// JsonSerializableAttribute has AllowMultiple as False, should only have 1 attribute.
34+
Debug.Assert(serializableAttributes.Count() == 1);
35+
AttributeSyntax attributeNode = serializableAttributes.First();
36+
37+
// Check if the attribute is being passed a type.
38+
if (attributeNode.DescendantNodes().Where(node => node is TypeOfExpressionSyntax).Any())
39+
{
40+
// Get JsonSerializable attribute arguments.
41+
AttributeArgumentSyntax attributeArgumentNode = (AttributeArgumentSyntax)attributeNode.DescendantNodes().Where(node => node is AttributeArgumentSyntax).SingleOrDefault();
42+
// Get external class token from arguments.
43+
IdentifierNameSyntax externalTypeNode = (IdentifierNameSyntax)attributeArgumentNode?.DescendantNodes().Where(node => node is IdentifierNameSyntax).SingleOrDefault();
44+
ExternalClassTypes.Add(new KeyValuePair<string, IdentifierNameSyntax>(((TypeDeclarationSyntax)syntaxNode).Identifier.Text, externalTypeNode));
45+
}
46+
else
47+
{
48+
InternalClassTypes.Add(new KeyValuePair<string, TypeDeclarationSyntax>(((TypeDeclarationSyntax)syntaxNode).Identifier.Text, (TypeDeclarationSyntax)syntaxNode));
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)