first commit
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Scriban;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace EnumSerializer.Generator {
|
||||
|
||||
|
||||
[Generator]
|
||||
public class EnumConverterGenerator : IIncrementalGenerator {
|
||||
const string JsonConverterAttribute = "Newtonsoft.Json.JsonConverterAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context) {
|
||||
IncrementalValuesProvider<EnumDeclarationSyntax> enumDeclarations = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: static (s, _) => IsSyntaxTargetForGeneration(s),
|
||||
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
|
||||
.Where(static m => m is not null)!;
|
||||
|
||||
IncrementalValueProvider<(Compilation, ImmutableArray<EnumDeclarationSyntax>)> compilationAndEnums
|
||||
= context.CompilationProvider.Combine(enumDeclarations.Collect());
|
||||
|
||||
context.RegisterSourceOutput(compilationAndEnums,
|
||||
static (spc, source) => Execute(source.Item1, source.Item2, spc));
|
||||
}
|
||||
|
||||
static bool IsSyntaxTargetForGeneration(SyntaxNode node) {
|
||||
return node is EnumDeclarationSyntax e && e.AttributeLists.Count > 0;
|
||||
}
|
||||
|
||||
static EnumDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context) {
|
||||
// we know the node is a EnumDeclarationSyntax thanks to IsSyntaxTargetForGeneration
|
||||
var enumDeclarationSyntax = (EnumDeclarationSyntax)context.Node;
|
||||
|
||||
// loop through all the attributes on the method
|
||||
foreach(AttributeListSyntax attributeListSyntax in enumDeclarationSyntax.AttributeLists) {
|
||||
foreach(AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) {
|
||||
if(context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol
|
||||
is not IMethodSymbol attributeSymbol) {
|
||||
// weird, we couldn't get the symbol, ignore it
|
||||
continue;
|
||||
}
|
||||
|
||||
INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;
|
||||
string fullName = attributeContainingTypeSymbol.ToDisplayString();
|
||||
|
||||
// Is the attribute the [JsonConverterAttribute] attribute?
|
||||
if(fullName == JsonConverterAttribute) {
|
||||
// return the enum
|
||||
return enumDeclarationSyntax;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we didn't find the attribute we were looking for
|
||||
return null;
|
||||
}
|
||||
|
||||
static void Execute(
|
||||
Compilation compilation,
|
||||
ImmutableArray<EnumDeclarationSyntax> enums,
|
||||
SourceProductionContext context) {
|
||||
if(enums.IsDefaultOrEmpty) {
|
||||
// nothing to do yet
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<EnumDeclarationSyntax> distinctEnums = enums.Distinct();
|
||||
|
||||
List<EnumInfo> enumsToProcess = GetTypesToGenerate(compilation, distinctEnums, context.CancellationToken);
|
||||
if(enumsToProcess.Count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Template template = Template.Parse(SourceGenerationHelper.ConverterTemplate);
|
||||
foreach(var enumToProcess in enumsToProcess) {
|
||||
var result = SourceGenerationHelper.GenerateConverterClass(template, enumToProcess);
|
||||
context.AddSource(
|
||||
hintName: $"{enumToProcess.Name}Converter.g.cs",
|
||||
sourceText: SourceText.From(result, Encoding.UTF8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static List<EnumInfo> GetTypesToGenerate(
|
||||
Compilation compilation,
|
||||
IEnumerable<EnumDeclarationSyntax> enums, CancellationToken ct) {
|
||||
var enumsToProcess = new List<EnumInfo>();
|
||||
INamedTypeSymbol? enumAttribute = compilation.GetTypeByMetadataName(JsonConverterAttribute);
|
||||
if(enumAttribute is null) {
|
||||
// nothing to do if this type isn't available
|
||||
return enumsToProcess;
|
||||
}
|
||||
|
||||
foreach(var enumDeclarationSyntax in enums) {
|
||||
// stop if we're asked to
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
SemanticModel semanticModel = compilation.GetSemanticModel(enumDeclarationSyntax.SyntaxTree);
|
||||
if(semanticModel.GetDeclaredSymbol(enumDeclarationSyntax, cancellationToken: ct)
|
||||
is not INamedTypeSymbol enumSymbol) {
|
||||
// report diagnostic, something went wrong
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = enumSymbol.Name;
|
||||
string nameSpace = enumSymbol.ContainingNamespace.IsGlobalNamespace
|
||||
? string.Empty
|
||||
: enumSymbol.ContainingNamespace.ToString();
|
||||
|
||||
string fullyQualifiedName = enumSymbol.ToString();
|
||||
|
||||
var enumMembers = enumSymbol.GetMembers();
|
||||
var members = new List<KeyValuePair<string, string>>(enumMembers.Length);
|
||||
|
||||
foreach(var member in enumMembers) {
|
||||
if(member is not IFieldSymbol field
|
||||
|| field.ConstantValue is null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
string? displayName = null;
|
||||
foreach(var attribute in member.GetAttributes()) {
|
||||
if(attribute.AttributeClass is null
|
||||
|| attribute.AttributeClass.Name != "DisplayAttribute") {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach(var namedArgument in attribute.NamedArguments) {
|
||||
if(namedArgument.Key == "Name" && namedArgument.Value.Value?.ToString() is { } dn) {
|
||||
displayName = dn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
members.Add(new(
|
||||
member.Name,
|
||||
displayName ?? ToSnakeCase(member.Name)
|
||||
));
|
||||
}
|
||||
|
||||
enumsToProcess.Add(new(
|
||||
name: name,
|
||||
ns: nameSpace,
|
||||
fullyQualifiedName: fullyQualifiedName,
|
||||
members: members
|
||||
));
|
||||
}
|
||||
return enumsToProcess;
|
||||
}
|
||||
|
||||
static string ToSnakeCase(string name) =>
|
||||
string.Concat(name.Select((x, i) => i > 0 && char.IsUpper(x)
|
||||
? $"_{x}"
|
||||
: x.ToString())
|
||||
).ToLower();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace EnumSerializer.Generator {
|
||||
|
||||
|
||||
public readonly struct EnumInfo {
|
||||
public readonly string Name;
|
||||
public readonly string FullyQualifiedName;
|
||||
public readonly string Namespace;
|
||||
|
||||
/// <summary>
|
||||
/// Key is the enum name.
|
||||
/// </summary>
|
||||
public readonly List<KeyValuePair<string, string>> Members;
|
||||
|
||||
public EnumInfo(
|
||||
string name,
|
||||
string ns,
|
||||
string fullyQualifiedName,
|
||||
List<KeyValuePair<string, string>> members) {
|
||||
Name = name;
|
||||
Namespace = ns;
|
||||
Members = members;
|
||||
FullyQualifiedName = fullyQualifiedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>9</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<EnableNETAnalyzers>True</EnableNETAnalyzers>
|
||||
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- The following libraries include the source generator interfaces and types we need -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Scriban" Version="5.4.4" GeneratePathProperty="true" PrivateAssets="all" />
|
||||
|
||||
<!-- This ensures the library will be packaged as a source generator when we use `dotnet pack` -->
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
|
||||
<TargetPathWithTargetPlatformMoniker Include="$(PKGScriban)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
# Enum Converter Generator
|
||||
|
||||
## Background
|
||||
|
||||
Telegram.Bot library relies on [Json.NET converters](https://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm)
|
||||
to map JSON input to various enums and vice versa.
|
||||
|
||||
It's rather tedious and repeating task. So that's where C# source generators come to help.
|
||||
|
||||
`EnumSerializer.Generator` looks for enums
|
||||
annotated with `[JsonConverter(typeof(TEnumConverter))]` attribute and generates a converter that handles all possible enum values for us.
|
||||
|
||||
## Credits
|
||||
|
||||
This project is heavily inspired by the series of posts by Andrew Lock [Creating an incremental generator](https://andrewlock.net/creating-a-source-generator-part-1-creating-an-incremental-source-generator/) and [NetEscapades.EnumGenerators
|
||||
](https://github.com/andrewlock/NetEscapades.EnumGenerators) project.
|
||||
|
||||
We use Alexandre Mutel's [Scriban](https://github.com/scriban/scriban) templating engine to generate
|
||||
converter class output.
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Scriban;
|
||||
|
||||
namespace EnumSerializer.Generator {
|
||||
|
||||
|
||||
public static class SourceGenerationHelper {
|
||||
internal const string ConverterTemplate = @"//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by the EnumSerializer.Generator source generator
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
{{~ if enum_namespace ~}}
|
||||
namespace {{ enum_namespace }}{
|
||||
{{~ end ~}}
|
||||
|
||||
internal partial class {{ enum_name }}Converter : JsonConverter<{{ enum_name }}>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, {{ enum_name }} value, JsonSerializer serializer) =>
|
||||
writer.WriteValue(value switch
|
||||
{
|
||||
{{~ for enum_member in enum_members ~}}
|
||||
{{ enum_name }}.{{enum_member.key}} => ""{{ enum_member.value }}"",
|
||||
{{~ end ~}}
|
||||
{{~ if has_unknown_member ~}}
|
||||
_ => throw new NotSupportedException(),
|
||||
{{~ else ~}}
|
||||
({{ enum_name }})0 => ""unknown"",
|
||||
_ => throw new NotSupportedException(),
|
||||
{{~ end ~}}
|
||||
});
|
||||
|
||||
public override {{ enum_name }} ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
{{ enum_name }} existingValue,
|
||||
bool hasExistingValue,
|
||||
JsonSerializer serializer
|
||||
) =>
|
||||
JToken.ReadFrom(reader).Value<string>() switch
|
||||
{
|
||||
{{~ for enum_member in enum_members ~}}
|
||||
""{{ enum_member.value }}"" => {{ enum_name }}.{{ enum_member.key }},
|
||||
{{~ end ~}}
|
||||
{{~ if has_unknown_member ~}}
|
||||
_ => {{ enum_name }}.Unknown,
|
||||
{{~ else ~}}
|
||||
_ => 0,
|
||||
{{~ end ~}}
|
||||
};
|
||||
}}";
|
||||
|
||||
public static string GenerateConverterClass(Template template, EnumInfo enumToGenerate) {
|
||||
var hasUnknownMember = enumToGenerate.Members.Any(
|
||||
e => string.Equals(e.Value, "Unknown", StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
var result = template.Render(new {
|
||||
EnumNamespace = enumToGenerate.Namespace,
|
||||
EnumName = enumToGenerate.Name,
|
||||
EnumMembers = enumToGenerate.Members,
|
||||
HasUnknownMember = hasUnknownMember
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user