Skip to content

Commit

Permalink
Json serializer type discovery (dotnet#18)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kevinwkt authored Jul 23, 2020
1 parent 8508a99 commit 816409c
Show file tree
Hide file tree
Showing 22 changed files with 1,428 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,90 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Text.Json.Serialization;
using Xunit;

namespace System.Text.Json.SourceGeneration.Tests
{
public class JsonSerializerSouceGeneratorTests
public class JsonSerializerSourceGeneratorTests
{
[JsonSerializable]
public class SampleInternalTest
{
public char PublicCharField;
private string PrivateStringField;
public int PublicIntPropertyPublic { get; set; }
public int PublicIntPropertyPrivateSet { get; private set; }
public int PublicIntPropertyPrivateGet { private get; set; }

public SampleInternalTest()
{
PublicCharField = 'a';
PrivateStringField = "privateStringField";
}

public SampleInternalTest(char c, string s)
{
PublicCharField = c;
PrivateStringField = s;
}

private SampleInternalTest(int i)
{
PublicIntPropertyPublic = i;
}

private void UseFields()
{
string use = PublicCharField.ToString() + PrivateStringField;
}
}

[JsonSerializable(typeof(JsonConverterAttribute))]
public class SampleExternalTest { }

[Fact]
public static void TestGeneratedCode()
public void TestGeneratedCode()
{
Assert.Equal("Hello", HelloWorldGenerated.HelloWorld.SayHello());
var internalTypeTest = new HelloWorldGenerated.SampleInternalTestClassInfo();
var externalTypeTest = new HelloWorldGenerated.SampleExternalTestClassInfo();

// Check base class names.
Assert.Equal("SampleInternalTestClassInfo", internalTypeTest.GetClassName());
Assert.Equal("SampleExternalTestClassInfo", externalTypeTest.GetClassName());

// Public and private Ctors are visible.
Assert.Equal(3, internalTypeTest.Ctors.Count);
Assert.Equal(2, externalTypeTest.Ctors.Count);

// Ctor params along with its types are visible.
Dictionary<string, string> expectedCtorParamsInternal = new Dictionary<string, string> { { "c", "Char"}, { "s", "String" }, { "i", "Int32" } };
Assert.Equal(expectedCtorParamsInternal, internalTypeTest.CtorParams);

Dictionary<string, string> expectedCtorParamsExternal = new Dictionary<string, string> { { "converterType", "Type"} };
Assert.Equal(expectedCtorParamsExternal, externalTypeTest.CtorParams);

// Public and private methods are visible.
List<string> expectedMethodsInternal = new List<string> { "get_PublicIntPropertyPublic", "set_PublicIntPropertyPublic", "get_PublicIntPropertyPrivateSet", "set_PublicIntPropertyPrivateSet", "get_PublicIntPropertyPrivateGet", "set_PublicIntPropertyPrivateGet", "UseFields" };
Assert.Equal(expectedMethodsInternal, internalTypeTest.Methods);

List<string> expectedMethodsExternal = new List<string> { "get_ConverterType", "CreateConverter" };
Assert.Equal(expectedMethodsExternal, externalTypeTest.Methods);

// Public and private fields are visible.
Dictionary<string, string> expectedFieldsInternal = new Dictionary<string, string> { { "PublicCharField", "Char" }, { "PrivateStringField", "String" } };
Assert.Equal(expectedFieldsInternal, internalTypeTest.Fields);

Dictionary<string, string> expectedFieldsExternal = new Dictionary<string, string> { };
Assert.Equal(expectedFieldsExternal, externalTypeTest.Fields);

// Public properties are visible.
Dictionary<string, string> expectedPropertiesInternal = new Dictionary<string, string> { { "PublicIntPropertyPublic", "Int32" }, { "PublicIntPropertyPrivateSet", "Int32" }, { "PublicIntPropertyPrivateGet", "Int32" } };
Assert.Equal(expectedPropertiesInternal, internalTypeTest.Properties);

Dictionary<string, string> expectedPropertiesExternal = new Dictionary<string, string> { { "ConverterType", "Type"} };
Assert.Equal(expectedPropertiesExternal, externalTypeTest.Properties);
}
}
}
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>$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,181 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;

namespace System.Text.Json.SourceGeneration.UnitTests
{
public static class GeneratorTests
public class GeneratorTests
{
[Fact]
public static void SourceGeneratorInitializationPass()
public void TypeDiscoveryPrimitivePOCO()
{
string source = @"
using System;
using System.Text.Json.Serialization;
namespace HelloWorld
{
[JsonSerializable]
public class MyType {
public int PublicPropertyInt { get; set; }
public string PublicPropertyString { get; set; }
private int PrivatePropertyInt { get; set; }
private string PrivatePropertyString { get; set; }
public double PublicDouble;
public char PublicChar;
private double PrivateDouble;
private char PrivateChar;
public void MyMethod() { }
public void MySecondMethod() { }
}
}";

Compilation compilation = CreateCompilation(source);

JsonSerializerSourceGenerator generator = new JsonSerializerSourceGenerator();

Compilation outCompilation = RunGenerators(compilation, out var generatorDiags, generator);

// Check base functionality of found types.
Assert.Equal(1, generator.foundTypes.Count);
Assert.Equal("HelloWorld.MyType", generator.foundTypes["MyType"].FullName);

// Check for received properties in created type.
string[] expectedPropertyNames = { "PublicPropertyInt", "PublicPropertyString", "PrivatePropertyInt", "PrivatePropertyString" };
string[] receivedPropertyNames = generator.foundTypes["MyType"].GetProperties().Select(property => property.Name).ToArray();
Assert.Equal(expectedPropertyNames, receivedPropertyNames);

// Check for fields in created type.
string[] expectedFieldNames = { "PublicDouble", "PublicChar", "PrivateDouble", "PrivateChar" };
string[] receivedFieldNames = generator.foundTypes["MyType"].GetFields().Select(field => field.Name).ToArray();
Assert.Equal(expectedFieldNames, receivedFieldNames);

// Check for methods in created type.
string[] expectedMethodNames = { "get_PublicPropertyInt", "set_PublicPropertyInt", "get_PublicPropertyString", "set_PublicPropertyString", "get_PrivatePropertyInt", "set_PrivatePropertyInt", "get_PrivatePropertyString", "set_PrivatePropertyString", "MyMethod", "MySecondMethod" };
string[] receivedMethodNames = generator.foundTypes["MyType"].GetMethods().Select(method => method.Name).ToArray();
Assert.Equal(expectedMethodNames, receivedMethodNames);
}

[Fact]
public static void SourceGeneratorInitializationFail()
public void TypeDiscoveryPrimitiveTemporaryPOCO()
{
string source = @"
using System;
using System.Text.Json.Serialization;
namespace HelloWorld
{
[JsonSerializable]
public class MyType {
public int PublicPropertyInt { get; set; }
public string PublicPropertyString { get; set; }
private int PrivatePropertyInt { get; set; }
private string PrivatePropertyString { get; set; }
public double PublicDouble;
public char PublicChar;
private double PrivateDouble;
private char PrivateChar;
public void MyMethod() { }
public void MySecondMethod() { }
}
[JsonSerializable(typeof(JsonConverterAttribute))]
public class NotMyType { }
}";

Compilation compilation = CreateCompilation(source);

JsonSerializerSourceGenerator generator = new JsonSerializerSourceGenerator();

Compilation outCompilation = RunGenerators(compilation, out var generatorDiags, generator);

// Check base functionality of found types.
Assert.Equal(2, generator.foundTypes.Count);

// Check for MyType.
Assert.Equal("HelloWorld.MyType", generator.foundTypes["MyType"].FullName);

// Check for received properties in created type.
string[] expectedPropertyNamesMyType = { "PublicPropertyInt", "PublicPropertyString", "PrivatePropertyInt", "PrivatePropertyString" };
string[] receivedPropertyNamesMyType = generator.foundTypes["MyType"].GetProperties().Select(property => property.Name).ToArray();
Assert.Equal(expectedPropertyNamesMyType, receivedPropertyNamesMyType);

// Check for fields in created type.
string[] expectedFieldNamesMyType = { "PublicDouble", "PublicChar", "PrivateDouble", "PrivateChar" };
string[] receivedFieldNamesMyType = generator.foundTypes["MyType"].GetFields().Select(field => field.Name).ToArray();
Assert.Equal(expectedFieldNamesMyType, receivedFieldNamesMyType);

// Check for methods in created type.
string[] expectedMethodNamesMyType = { "get_PublicPropertyInt", "set_PublicPropertyInt", "get_PublicPropertyString", "set_PublicPropertyString", "get_PrivatePropertyInt", "set_PrivatePropertyInt", "get_PrivatePropertyString", "set_PrivatePropertyString", "MyMethod", "MySecondMethod" };
string[] receivedMethodNamesMyType = generator.foundTypes["MyType"].GetMethods().Select(method => method.Name).ToArray();
Assert.Equal(expectedMethodNamesMyType, receivedMethodNamesMyType);

// Check for NotMyType.
Assert.Equal("System.Text.Json.Serialization.JsonConverterAttribute", generator.foundTypes["NotMyType"].FullName);

// Check for received properties in created type.
string[] expectedPropertyNamesNotMyType = { "ConverterType" };
string[] receivedPropertyNamesNotMyType = generator.foundTypes["NotMyType"].GetProperties().Select(property => property.Name).ToArray();
Assert.Equal(expectedPropertyNamesNotMyType, receivedPropertyNamesNotMyType);

// Check for fields in created type.
string[] expectedFieldNamesNotMyType = { };
string[] receivedFieldNamesNotMyType = generator.foundTypes["NotMyType"].GetFields().Select(field => field.Name).ToArray();
Assert.Equal(expectedFieldNamesNotMyType, receivedFieldNamesNotMyType);

// Check for methods in created type.
string[] expectedMethodNamesNotMyType = { "get_ConverterType", "CreateConverter" };
string[] receivedMethodNamesNotMyType = generator.foundTypes["NotMyType"].GetMethods().Select(method => method.Name).ToArray();
Assert.Equal(expectedMethodNamesNotMyType, receivedMethodNamesNotMyType);
}

[Fact]
public static void SourceGeneratorExecutionPass()
private Compilation CreateCompilation(string source)
{
// Bypass System.Runtime error.
Assembly systemRuntimeAssembly = Assembly.Load("System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
string systemRuntimeAssemblyPath = systemRuntimeAssembly.Location;

MetadataReference[] references = new MetadataReference[] {
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location),
MetadataReference.CreateFromFile(typeof(JsonSerializableAttribute).Assembly.Location),
MetadataReference.CreateFromFile(typeof(JsonSerializerOptions).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Type).Assembly.Location),
MetadataReference.CreateFromFile(typeof(KeyValuePair).Assembly.Location),
MetadataReference.CreateFromFile(systemRuntimeAssemblyPath),
};

return CSharpCompilation.Create(
"TestAssembly",
syntaxTrees: new[] { CSharpSyntaxTree.ParseText(source) },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
}

[Fact]
public static void SourceGeneratorExecutionFail()
private GeneratorDriver CreateDriver(Compilation compilation, params ISourceGenerator[] generators)
=> new CSharpGeneratorDriver(
new CSharpParseOptions(kind: SourceCodeKind.Regular, documentationMode: DocumentationMode.Parse),
ImmutableArray.Create(generators),
ImmutableArray<AdditionalText>.Empty);

private Compilation RunGenerators(Compilation compilation, out ImmutableArray<Diagnostic> diagnostics, params ISourceGenerator[] generators)
{
CreateDriver(compilation, generators).RunFullGeneration(compilation, out Compilation outCompilation, out diagnostics);
return outCompilation;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" Version="$(MicrosoftCodeAnalysisVersion)" PrivateAssets="all" />

<ProjectReference Include="..\src\System.Text.Json.csproj" />
<ProjectReference Include="..\System.Text.Json.SourceGeneration\System.Text.Json.SourceGeneration.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

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

namespace System.Text.Json.SourceGeneration
{
public class JsonSerializableSyntaxReceiver : ISyntaxReceiver
{
public List<KeyValuePair<string, IdentifierNameSyntax>> ExternalClassTypes = new List<KeyValuePair<string, IdentifierNameSyntax>>();
public List<KeyValuePair<string, TypeDeclarationSyntax>> InternalClassTypes = new List<KeyValuePair<string, TypeDeclarationSyntax>>();

public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// Look for classes or structs for JsonSerializable Attribute.
if (syntaxNode is ClassDeclarationSyntax || syntaxNode is StructDeclarationSyntax)
{
// Find JsonSerializable Attributes.
IEnumerable<AttributeSyntax>? serializableAttributes = null;
AttributeListSyntax attributeList = ((TypeDeclarationSyntax)syntaxNode).AttributeLists.SingleOrDefault();
if (attributeList != null)
{
serializableAttributes = attributeList.Attributes.Where(node => (node is AttributeSyntax attr && attr.Name.ToString() == "JsonSerializable")).Cast<AttributeSyntax>();
}

if (serializableAttributes?.Any() == true)
{
// JsonSerializableAttribute has AllowMultiple as False, should only have 1 attribute.
Debug.Assert(serializableAttributes.Count() == 1);
AttributeSyntax attributeNode = serializableAttributes.First();

// Check if the attribute is being passed a type.
if (attributeNode.DescendantNodes().Where(node => node is TypeOfExpressionSyntax).Any())
{
// Get JsonSerializable attribute arguments.
AttributeArgumentSyntax attributeArgumentNode = (AttributeArgumentSyntax)attributeNode.DescendantNodes().Where(node => node is AttributeArgumentSyntax).SingleOrDefault();
// Get external class token from arguments.
IdentifierNameSyntax externalTypeNode = (IdentifierNameSyntax)attributeArgumentNode?.DescendantNodes().Where(node => node is IdentifierNameSyntax).SingleOrDefault();
ExternalClassTypes.Add(new KeyValuePair<string, IdentifierNameSyntax>(((TypeDeclarationSyntax)syntaxNode).Identifier.Text, externalTypeNode));
}
else
{
InternalClassTypes.Add(new KeyValuePair<string, TypeDeclarationSyntax>(((TypeDeclarationSyntax)syntaxNode).Identifier.Text, (TypeDeclarationSyntax)syntaxNode));
}
}
}
}
}
}
Loading

0 comments on commit 816409c

Please sign in to comment.