Support formatter generation for open generic types.

This commit is contained in:
Mayuki Sawatari 2020-08-14 17:38:04 +09:00
Родитель 7d06c13e72
Коммит 4d19fae633
10 изменённых файлов: 388 добавлений и 11 удалений

Просмотреть файл

@ -78,7 +78,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagePack.Generator", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagePack.MSBuild.Tasks", "src\MessagePack.MSBuild.Tasks\MessagePack.MSBuild.Tasks.csproj", "{8DB135F5-A6FE-44E4-9853-7B48ED21F21B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagePackAnalyzer.Tests", "tests\MessagePackAnalyzer.Tests\MessagePackAnalyzer.Tests.csproj", "{7E5FB4B9-A0F5-4B10-A1F3-03AC0BC8265A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagePackAnalyzer.Tests", "tests\MessagePackAnalyzer.Tests\MessagePackAnalyzer.Tests.csproj", "{7E5FB4B9-A0F5-4B10-A1F3-03AC0BC8265A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagePack.Generator.Tests", "tests\MessagePack.Generator.Tests\MessagePack.Generator.Tests.csproj", "{6AC51E68-4681-463A-B4B6-BD53517244B2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -270,6 +272,14 @@ Global
{7E5FB4B9-A0F5-4B10-A1F3-03AC0BC8265A}.Release|Any CPU.Build.0 = Release|Any CPU
{7E5FB4B9-A0F5-4B10-A1F3-03AC0BC8265A}.Release|NoVSIX.ActiveCfg = Release|Any CPU
{7E5FB4B9-A0F5-4B10-A1F3-03AC0BC8265A}.Release|NoVSIX.Build.0 = Release|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Debug|NoVSIX.ActiveCfg = Debug|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Debug|NoVSIX.Build.0 = Debug|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Release|Any CPU.Build.0 = Release|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Release|NoVSIX.ActiveCfg = Release|Any CPU
{6AC51E68-4681-463A-B4B6-BD53517244B2}.Release|NoVSIX.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -298,6 +308,7 @@ Global
{32C91908-5CAD-4C95-B240-ACBBACAC9476} = {86309CF6-0054-4CE3-BFD3-CA0AA7DB17BC}
{8DB135F5-A6FE-44E4-9853-7B48ED21F21B} = {86309CF6-0054-4CE3-BFD3-CA0AA7DB17BC}
{7E5FB4B9-A0F5-4B10-A1F3-03AC0BC8265A} = {19FE674A-AC94-4E7E-B24C-2285D1D04CDE}
{6AC51E68-4681-463A-B4B6-BD53517244B2} = {19FE674A-AC94-4E7E-B24C-2285D1D04CDE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B3911209-2DBF-47F8-98F6-BBC0EDFE63DE}

Просмотреть файл

@ -24,10 +24,12 @@ namespace MessagePackCompiler.CodeAnalysis
public string FullName { get; set; }
public string TemplateParametersString { get; set; }
public string Namespace { get; set; }
public string[] GenericTypeParameters { get; set; }
public bool IsOpenGenericType { get; set; }
public bool IsIntKey { get; set; }
public bool IsStringKey
@ -52,7 +54,7 @@ namespace MessagePackCompiler.CodeAnalysis
public bool NeedsCastOnAfter { get; set; }
public string FormatterName => (this.Namespace == null ? this.Name : this.Namespace + "." + this.Name) + "Formatter";
public string FormatterName => (this.Namespace == null ? this.Name : this.Namespace + "." + this.Name) + "Formatter" + (this.IsOpenGenericType ? $"<{string.Join(",", this.GenericTypeParameters)}>" : string.Empty);
public int WriteCount
{

Просмотреть файл

@ -272,6 +272,7 @@ namespace MessagePackCompiler.CodeAnalysis
private List<GenericSerializationInfo> collectedGenericInfo;
private List<UnionSerializationInfo> collectedUnionInfo;
private List<ObjectSerializationInfo> collectedClosedTypeGenericInfo;
private List<ObjectSerializationInfo> collectedOpenedTypeGenericInfo;
public TypeCollector(Compilation compilation, bool disallowInternal, bool isForceUseMap, Action<string> logger)
{
@ -311,10 +312,11 @@ namespace MessagePackCompiler.CodeAnalysis
this.collectedGenericInfo = new List<GenericSerializationInfo>();
this.collectedUnionInfo = new List<UnionSerializationInfo>();
this.collectedClosedTypeGenericInfo = new List<ObjectSerializationInfo>();
this.collectedOpenedTypeGenericInfo = new List<ObjectSerializationInfo>();
}
// EntryPoint
public (ObjectSerializationInfo[] ObjectInfo, EnumSerializationInfo[] EnumInfo, GenericSerializationInfo[] GenericInfo, UnionSerializationInfo[] UnionInfo, ObjectSerializationInfo[] ClosedTypeGenericInfo) Collect()
public (ObjectSerializationInfo[] ObjectInfo, EnumSerializationInfo[] EnumInfo, GenericSerializationInfo[] GenericInfo, UnionSerializationInfo[] UnionInfo, ObjectSerializationInfo[] ClosedTypeGenericInfo, ObjectSerializationInfo[] OpenedTypeGenericInfo) Collect()
{
this.ResetWorkspace();
@ -328,7 +330,8 @@ namespace MessagePackCompiler.CodeAnalysis
this.collectedEnumInfo.OrderBy(x => x.FullName).ToArray(),
this.collectedGenericInfo.Distinct().OrderBy(x => x.FullName).ToArray(),
this.collectedUnionInfo.OrderBy(x => x.FullName).ToArray(),
this.collectedClosedTypeGenericInfo.OrderBy(x => x.FullName).ToArray());
this.collectedClosedTypeGenericInfo.OrderBy(x => x.FullName).ToArray(),
this.collectedOpenedTypeGenericInfo.OrderBy(x => x.FullName).ToArray());
}
// Gate of recursive collect
@ -564,10 +567,11 @@ namespace MessagePackCompiler.CodeAnalysis
return;
}
// Skip generic symbol declaration itself (open type, e.g. Foo<T>) because we can get nothing useful from it anyways.
// Generic types
if (type.IsDefinition)
{
this.CollectGenericUnion(type);
this.CollectObject(type);
return;
}
@ -608,6 +612,7 @@ namespace MessagePackCompiler.CodeAnalysis
private ObjectSerializationInfo GetObjectInfo(INamedTypeSymbol type)
{
var isClass = !type.IsValueType;
var isOpenGenericType = type.IsGenericType;
AttributeData contractAttr = type.GetAttributes().FirstOrDefault(x => x.AttributeClass.ApproximatelyEqual(this.typeReferences.MessagePackObjectAttribute));
if (contractAttr == null)
@ -1001,10 +1006,12 @@ namespace MessagePackCompiler.CodeAnalysis
var info = new ObjectSerializationInfo
{
IsClass = isClass,
IsOpenGenericType = isOpenGenericType,
GenericTypeParameters = isOpenGenericType ? type.TypeParameters.Select(x => x.ToDisplayString()).ToArray() : Array.Empty<string>(),
ConstructorParameters = constructorParameters.ToArray(),
IsIntKey = isIntKey,
Members = isIntKey ? intMembers.Values.ToArray() : stringMembers.Values.ToArray(),
Name = GetMinimallyQualifiedClassName(type),
Name = isOpenGenericType ? GetGenericFormatterClassName(type) : GetMinimallyQualifiedClassName(type),
FullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
Namespace = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToDisplayString(),
HasIMessagePackSerializationCallbackReceiver = hasSerializationConstructor,
@ -1015,6 +1022,11 @@ namespace MessagePackCompiler.CodeAnalysis
return info;
}
private static string GetGenericFormatterClassName(INamedTypeSymbol type)
{
return type.Name;
}
private static string GetMinimallyQualifiedClassName(INamedTypeSymbol type)
{
var name = type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);

Просмотреть файл

@ -55,7 +55,7 @@ namespace MessagePackCompiler
sw.Restart();
logger("Method Collect Start");
var (objectInfo, enumInfo, genericInfo, unionInfo, closedTypeGenericInfo) = collector.Collect();
var (objectInfo, enumInfo, genericInfo, unionInfo, closedTypeGenericInfo, openedTypeGenericInfo) = collector.Collect();
logger("Method Collect Complete:" + sw.Elapsed.ToString());

Просмотреть файл

@ -48,7 +48,7 @@ namespace ");
this.Write("\r\n public sealed class ");
this.Write(this.ToStringHelper.ToStringWithCulture(objInfo.Name));
this.Write("Formatter");
this.Write(this.ToStringHelper.ToStringWithCulture(objInfo.TemplateParametersString != null? objInfo.TemplateParametersString : ""));
this.Write(this.ToStringHelper.ToStringWithCulture((objInfo.IsOpenGenericType ? $"<{string.Join(",", objInfo.GenericTypeParameters)}>" : "")));
this.Write(" : global::MessagePack.Formatters.IMessagePackFormatter<");
this.Write(this.ToStringHelper.ToStringWithCulture(objInfo.FullName));
this.Write(">\r\n {\r\n");

Просмотреть файл

@ -26,7 +26,7 @@ namespace <#= Namespace #>
using MessagePack;
<# foreach(var objInfo in ObjectSerializationInfos) { #>
public sealed class <#= objInfo.Name #>Formatter<#= objInfo.TemplateParametersString != null? objInfo.TemplateParametersString : "" #> : global::MessagePack.Formatters.IMessagePackFormatter<<#= objInfo.FullName #>>
public sealed class <#= objInfo.Name #>Formatter<#= (objInfo.IsOpenGenericType ? $"<{string.Join(",", objInfo.GenericTypeParameters)}>" : "") #> : global::MessagePack.Formatters.IMessagePackFormatter<<#= objInfo.FullName #>>
{
<# foreach(var item in objInfo.Members) { #>
<# if(item.CustomFormatterTypeName != null) { #>

Просмотреть файл

@ -0,0 +1,62 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace MessagePack.Generator.Tests
{
public class GenerateEnumFormatterTest
{
private readonly ITestOutputHelper testOutputHelper;
public GenerateEnumFormatterTest(ITestOutputHelper testOutputHelper)
{
this.testOutputHelper = testOutputHelper;
}
[Fact]
public async Task EnumFormatter()
{
using var tempWorkarea = TemporaryProjectWorkarea.Create();
var contents = @"
using System;
using MessagePack;
namespace TempProject
{
[MessagePackObject]
public class MyMessagePackObject
{
[Key(0)]
public MyEnum EnumValue { get; set; }
}
public enum MyEnum
{
A, B, C
}
}
";
tempWorkarea.AddFileToProject("MyMessagePackObject.cs", contents);
var compiler = new MessagePackCompiler.CodeGenerator(testOutputHelper.WriteLine, CancellationToken.None);
await compiler.GenerateFileAsync(
tempWorkarea.CsProjectPath,
tempWorkarea.OutputDirectory,
string.Empty,
"TempProjectResolver",
"TempProject.Generated",
false,
string.Empty);
var compilation = tempWorkarea.GetOutputCompilation();
var symbols = compilation.GetNamedTypeSymbolsFromGenerated();
symbols.Should().Contain(x => x.Name == "MyEnumFormatter");
}
}
}

Просмотреть файл

@ -0,0 +1,146 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace MessagePack.Generator.Tests
{
public class GenerateGenericsFormatterTest
{
private readonly ITestOutputHelper testOutputHelper;
public GenerateGenericsFormatterTest(ITestOutputHelper testOutputHelper)
{
this.testOutputHelper = testOutputHelper;
}
[Fact]
public async Task GenericsUnionFormatter()
{
using var tempWorkarea = TemporaryProjectWorkarea.Create();
var contents = @"
using System;
using MessagePack;
namespace TempProject
{
[MessagePackObject]
[Union(0, typeof(Wrapper<string>))]
[Union(1, typeof(Wrapper<int[]>))]
[Union(2, typeof(Wrapper<IEnumerable<Guid>>))]
public class Wrapper<T>
{
[Key(0)]
public T Content { get; set; }
}
}
";
tempWorkarea.AddFileToProject("MyMessagePackObject.cs", contents);
var compiler = new MessagePackCompiler.CodeGenerator(testOutputHelper.WriteLine, CancellationToken.None);
await compiler.GenerateFileAsync(
tempWorkarea.CsProjectPath,
tempWorkarea.OutputDirectory,
string.Empty,
"TempProjectResolver",
"TempProject.Generated",
false,
string.Empty);
var compilation = tempWorkarea.GetOutputCompilation();
var symbols = compilation.GetNamedTypeSymbolsFromGenerated();
var formatters = symbols.SelectMany(x => x.Interfaces).Select(x => x.ToDisplayString()).ToArray();
formatters.Should().Contain(x => x == "MessagePack.Formatters.IMessagePackFormatter<TempProject.Wrapper<IEnumerable<System.Guid>>>");
formatters.Should().Contain(x => x == "MessagePack.Formatters.IMessagePackFormatter<TempProject.Wrapper<int[]>>");
formatters.Should().Contain(x => x == "MessagePack.Formatters.IMessagePackFormatter<TempProject.Wrapper<string>>");
}
[Fact]
public async Task GenericsOfTFormatter()
{
using var tempWorkarea = TemporaryProjectWorkarea.Create(false);
var contents = @"
using System;
using MessagePack;
namespace TempProject
{
[MessagePackObject]
public class MyGenericObject<T>
{
[Key(0)]
public T Content { get; set; }
}
}
";
tempWorkarea.AddFileToProject("MyMessagePackObject.cs", contents);
var compiler = new MessagePackCompiler.CodeGenerator(testOutputHelper.WriteLine, CancellationToken.None);
await compiler.GenerateFileAsync(
tempWorkarea.CsProjectPath,
tempWorkarea.OutputDirectory,
string.Empty,
"TempProjectResolver",
"TempProject.Generated",
false,
string.Empty);
var compilation = tempWorkarea.GetOutputCompilation();
var symbols = compilation.GetNamedTypeSymbolsFromGenerated();
var types = symbols.Select(x => x.ToDisplayString()).ToArray();
types.Should().Contain("TempProject.Generated.Formatters.TempProject.MyGenericObjectFormatter<T>");
var formatters = symbols.SelectMany(x => x.Interfaces).Select(x => x.ToDisplayString()).ToArray();
formatters.Should().Contain(x => x == "MessagePack.Formatters.IMessagePackFormatter<TempProject.MyGenericObject<T>>");
}
[Fact]
public async Task GenericsOfT1T2Formatter()
{
using var tempWorkarea = TemporaryProjectWorkarea.Create();
var contents = @"
using System;
using MessagePack;
namespace TempProject
{
[MessagePackObject]
public class MyGenericObject<T1, T2>
{
[Key(0)]
public T1 ValueA { get; set; }
[Key(1)]
public T2 ValueB { get; set; }
}
}
";
tempWorkarea.AddFileToProject("MyMessagePackObject.cs", contents);
var compiler = new MessagePackCompiler.CodeGenerator(testOutputHelper.WriteLine, CancellationToken.None);
await compiler.GenerateFileAsync(
tempWorkarea.CsProjectPath,
tempWorkarea.OutputDirectory,
string.Empty,
"TempProjectResolver",
"TempProject.Generated",
false,
string.Empty);
var compilation = tempWorkarea.GetOutputCompilation();
var symbols = compilation.GetNamedTypeSymbolsFromGenerated();
var types = symbols.Select(x => x.ToDisplayString()).ToArray();
types.Should().Contain("TempProject.Generated.Formatters.TempProject.MyGenericObjectFormatter<T1, T2>");
var formatters = symbols.SelectMany(x => x.Interfaces).Select(x => x.ToDisplayString()).ToArray();
formatters.Should().Contain(x => x == "MessagePack.Formatters.IMessagePackFormatter<TempProject.MyGenericObject<T1, T2>>");
}
}
}

Просмотреть файл

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>..\MessagePack.Tests\MessagePack.Tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MessagePack\MessagePack.csproj" />
<ProjectReference Include="..\..\src\MessagePack.Annotations\MessagePack.Annotations.csproj" />
<ProjectReference Include="..\..\src\MessagePack.GeneratorCore\MessagePack.GeneratorCore.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,117 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using MessagePack.Formatters;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MessagePack.Generator.Tests
{
/// <summary>
/// Provides a temporary work area for unit testing.
/// </summary>
public class TemporaryProjectWorkarea : IDisposable
{
private readonly string tempDirPath;
private readonly string csprojFileName = "TempProject.csproj";
private readonly bool cleanOnDisposing;
public string CsProjectPath { get; }
public string ProjectDirectory { get; }
public string OutputDirectory { get; }
public static TemporaryProjectWorkarea Create(bool cleanOnDisposing = true)
{
return new TemporaryProjectWorkarea(cleanOnDisposing);
}
private TemporaryProjectWorkarea(bool cleanOnDisposing)
{
this.cleanOnDisposing = cleanOnDisposing;
this.tempDirPath = Path.Combine(Path.GetTempPath(), $"MessagePack.Generator.Tests-{Guid.NewGuid()}");
ProjectDirectory = Path.Combine(tempDirPath, "Project");
OutputDirectory = Path.Combine(tempDirPath, "Output");
Directory.CreateDirectory(ProjectDirectory);
Directory.CreateDirectory(OutputDirectory);
var solutionRootDir = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "../../../.."));
var messagePackProjectDir = Path.Combine(solutionRootDir, "src/MessagePack/MessagePack.csproj");
var annotationsProjectDir = Path.Combine(solutionRootDir, "src/MessagePack.Annotations/MessagePack.Annotations.csproj");
CsProjectPath = Path.Combine(ProjectDirectory, csprojFileName);
var csprojContents = @"
<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include=""" + messagePackProjectDir + @""" />
<ProjectReference Include=""" + annotationsProjectDir + @""" />
</ItemGroup>
</Project>
";
AddFileToProject(csprojFileName, csprojContents);
}
public void AddFileToProject(string fileName, string contents)
{
File.WriteAllText(Path.Combine(ProjectDirectory, fileName), contents.Trim());
}
public CompilationContainer GetOutputCompilation()
{
var compilation = CSharpCompilation.Create(Guid.NewGuid().ToString())
.AddSyntaxTrees(
Directory.EnumerateFiles(ProjectDirectory, "*.cs", SearchOption.AllDirectories)
.Concat(Directory.EnumerateFiles(OutputDirectory, "*.cs", SearchOption.AllDirectories))
.Select(x => CSharpSyntaxTree.ParseText(File.ReadAllText(x), CSharpParseOptions.Default, x)))
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(MessagePack.MessagePackObjectAttribute).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(IMessagePackFormatter<>).Assembly.Location));
return new CompilationContainer(compilation);
}
public void Dispose()
{
if (cleanOnDisposing)
{
Directory.Delete(tempDirPath, true);
}
}
}
public class CompilationContainer
{
public Compilation Compilation { get; }
public CompilationContainer(Compilation compilation)
{
this.Compilation = compilation ?? throw new ArgumentNullException(nameof(compilation));
}
public INamedTypeSymbol[] GetNamedTypeSymbolsFromGenerated()
{
return Compilation.SyntaxTrees
.Select(x => Compilation.GetSemanticModel(x))
.SelectMany(semanticModel =>
{
return semanticModel.SyntaxTree.GetRoot()
.DescendantNodes()
.Select(x => semanticModel.GetDeclaredSymbol(x))
.OfType<INamedTypeSymbol>();
})
.ToArray();
}
}
}