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

Json serializer type discovery #18

Merged
merged 15 commits into from
Jul 23, 2020
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 @@ -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>();
Copy link
Member

Choose a reason for hiding this comment

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

The trouble with this approach is that you can't guarantee that the attribute name will actually be called "JsonSerializable"

For example, here are a bunch of ways I can rename the attribute https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzhgHwAEAmARgFgAoIgBgAIBBAOwgwAsYoA5bAWxi4ADtjAx6AXnpEyAbmp16AZQgCOAS2YBzAKIAbXDB3MM62HoCek6WQB0Sruux71AL2zA9MRhgxR1wACuGDDyVNQA2g7+zm4eXgC6CgDM0iRM9ADeAL7UkTL2jrHunt6+/kEhSTSppPQAQlm54VQRLGycPPyCImKFMS4liSlp9ADCTXmtKmrsmroGRiZmMJbVRLXpACJNQA===

Basically when dealing with syntax, you can only make guesses at things, not full decisions. You won't be able to actually know if the same attribute without doing binding. That's what the compiler does when you ask for a semantic model (it's not a trivial operation ;))

The way the other 'attribute based' generators solve this is by just selecting candidates for inclusion (basically anything that has an attribute):
https://github.com/dotnet/roslyn-sdk/blob/94087f6f1d5414ca187d8836caf7c8a114f22978/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs#L176

Then doing the actual selection during generation. https://github.com/dotnet/roslyn-sdk/blob/94087f6f1d5414ca187d8836caf7c8a114f22978/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs#L50

As your attribute is provided via references (not injected directly via the generator) you won't need to do the 'create a new compilation' step you'll see above.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

@jkotas Not really. I handle all those cases. I ensure the name ends with GeneratedDllImportAttribute or GeneratedDllImport. This handles all the cases in that example see

private static bool IsGeneratedDllImportAttribute(AttributeSyntax attrSyntaxMaybe)
{
var attrName = attrSyntaxMaybe.Name.ToString();
return attrName.EndsWith(GeneratedDllImport)
|| attrName.EndsWith(GeneratedDllImportAttribute);
}
.

This doesn't handle the other case of TrickGeneratedDllImportAttribute though. Still on the fence whether it is worth addressing based on the cost of building up the semantic model where it is being done.

}

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