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 enumDeclarations = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (s, _) => IsSyntaxTargetForGeneration(s), transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) .Where(static m => m is not null)!; IncrementalValueProvider<(Compilation, ImmutableArray)> 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 enums, SourceProductionContext context) { if(enums.IsDefaultOrEmpty) { // nothing to do yet return; } IEnumerable distinctEnums = enums.Distinct(); List 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 GetTypesToGenerate( Compilation compilation, IEnumerable enums, CancellationToken ct) { var enumsToProcess = new List(); 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>(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(); } }