first commit

This commit is contained in:
2026-08-03 22:52:44 +02:00
commit 17393a47d3
292 changed files with 23678 additions and 0 deletions
@@ -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();
}
}
+27
View File
@@ -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>
+19
View File
@@ -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;
}
}
}