From d6aea2c0128155cf460a0184d22c692769f0bb4a Mon Sep 17 00:00:00 2001 From: Boris Proshin Date: Tue, 19 Mar 2024 18:32:20 +0300 Subject: [PATCH] Add EnumTypeFor attribute --- .../EnumTypeFor/EnumMemberToProcess.cs | 6 + UnityAttributes/EnumTypeFor/EnumToProcess.cs | 20 +++ .../EnumTypeFor/EnumTypeForAttribute.cs | 25 +++ .../EnumTypeFor/EnumTypeForGenerator.cs | 156 ++++++++++++++++++ UnityAttributes/UnityAttributes.csproj | 2 +- 5 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 UnityAttributes/EnumTypeFor/EnumMemberToProcess.cs create mode 100644 UnityAttributes/EnumTypeFor/EnumToProcess.cs create mode 100644 UnityAttributes/EnumTypeFor/EnumTypeForAttribute.cs create mode 100644 UnityAttributes/EnumTypeFor/EnumTypeForGenerator.cs diff --git a/UnityAttributes/EnumTypeFor/EnumMemberToProcess.cs b/UnityAttributes/EnumTypeFor/EnumMemberToProcess.cs new file mode 100644 index 0000000..52c6308 --- /dev/null +++ b/UnityAttributes/EnumTypeFor/EnumMemberToProcess.cs @@ -0,0 +1,6 @@ +namespace UnityAttributes.EnumTypeFor; + +internal sealed record EnumMemberToProcess(string Name) +{ + public string Name { get; } = Name; +} \ No newline at end of file diff --git a/UnityAttributes/EnumTypeFor/EnumToProcess.cs b/UnityAttributes/EnumTypeFor/EnumToProcess.cs new file mode 100644 index 0000000..ca3a1fe --- /dev/null +++ b/UnityAttributes/EnumTypeFor/EnumToProcess.cs @@ -0,0 +1,20 @@ +#nullable enable +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace UnityAttributes.EnumTypeFor; + +internal sealed record EnumToProcess(ITypeSymbol EnumSymbol, ISymbol ForTypeSymbol, List Members, + string? FullNamespace, bool ShortName) +{ + public string FullCsharpName { get; } = EnumSymbol.ToDisplayString(); + public string DocumentationId { get; } = DocumentationCommentId.CreateDeclarationId(EnumSymbol); + public bool ShortName { get; } = ShortName; + + public ITypeSymbol EnumSymbol { get; } = EnumSymbol; + public ISymbol ForTypeSymbol { get; } = ForTypeSymbol; + public List Members { get; } = Members; + public string? FullNamespace { get; } = FullNamespace; + public string ClassName { get; } = + $"{EnumSymbol.Name}For{(ShortName ? ForTypeSymbol.Name : ForTypeSymbol.ToDisplayString().Replace(".", "_"))}"; +} \ No newline at end of file diff --git a/UnityAttributes/EnumTypeFor/EnumTypeForAttribute.cs b/UnityAttributes/EnumTypeFor/EnumTypeForAttribute.cs new file mode 100644 index 0000000..e8b641f --- /dev/null +++ b/UnityAttributes/EnumTypeFor/EnumTypeForAttribute.cs @@ -0,0 +1,25 @@ +namespace UnityAttributes.EnumTypeFor; + +public static class EnumTypeForAttribute +{ + public const string ATTRIBUTE_SHORT_NAME = "EnumTypeFor"; + public const string ATTRIBUTE_FULL_NAME = ATTRIBUTE_SHORT_NAME + "Attribute"; + + public const string ATTRIBUTE_TEXT = + $$""" + /// + + [global::System.AttributeUsage(global::System.AttributeTargets.Enum, Inherited = true, AllowMultiple = true)] + internal sealed class {{ATTRIBUTE_FULL_NAME}} : global::System.Attribute + { + private System.Type _type; + private bool _shortName; + + public {{ATTRIBUTE_FULL_NAME}}(System.Type type, bool shortName = true) + { + _type = type; + _shortName = shortName; + } + } + """; +} \ No newline at end of file diff --git a/UnityAttributes/EnumTypeFor/EnumTypeForGenerator.cs b/UnityAttributes/EnumTypeFor/EnumTypeForGenerator.cs new file mode 100644 index 0000000..b454ab3 --- /dev/null +++ b/UnityAttributes/EnumTypeFor/EnumTypeForGenerator.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using UnityAttributes.Common; + +namespace UnityAttributes.EnumTypeFor; + +[Generator] +public sealed class EnumTypeForGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var enums = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (node, _) => IsSyntaxTargetForGeneration(node), + transform: static (syntaxContext, token) => GetSemanticTargetForGeneration(syntaxContext, token)) + .SelectMany(static (array, _) => array) + .Collect() + .SelectMany(static (array, _) => array); + + context.RegisterPostInitializationOutput(i => i.AddSource( + $"{EnumTypeForAttribute.ATTRIBUTE_FULL_NAME}.g", EnumTypeForAttribute.ATTRIBUTE_TEXT)); + + context.RegisterSourceOutput(enums, GenerateCode); + } + + private static bool IsSyntaxTargetForGeneration(SyntaxNode node) + { + return node is EnumDeclarationSyntax; + } + + private static List GetSemanticTargetForGeneration(GeneratorSyntaxContext ctx, + CancellationToken token) + { + var enumDeclarationSyntax = (EnumDeclarationSyntax) ctx.Node; + + var enumDeclarationSymbol = ctx.SemanticModel.GetDeclaredSymbol(enumDeclarationSyntax, token); + if (enumDeclarationSymbol is not ITypeSymbol enumDeclarationTypeSymbol) + { + return []; + } + + var enumNamespace = enumDeclarationTypeSymbol.GetNamespace(); + + var membersToProcess = new List(); + foreach (var enumMemberDeclarationSyntax in enumDeclarationSyntax.Members) + { + membersToProcess.Add(new EnumMemberToProcess(enumMemberDeclarationSyntax.Identifier.Text)); + } + + var list = new List(); + foreach (var attributeSyntax in enumDeclarationSyntax.AllAttributesWhere + (syntax => syntax.Name.IsEqualByName(EnumTypeForAttribute.ATTRIBUTE_SHORT_NAME))) + { + if (attributeSyntax.ArgumentList is null) + { + continue; + } + + var arguments = attributeSyntax.ArgumentList.Arguments; + if (arguments.Count == 0) + { + continue; + } + + if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpressionSyntax) + { + continue; + } + + var forTypeSymbol = ctx.SemanticModel.GetSymbolInfo(typeOfExpressionSyntax.Type).Symbol; + if (forTypeSymbol is null) + { + continue; + } + + var shortName = true; + if (arguments.Count > 1) + { + if (arguments[1].Expression is LiteralExpressionSyntax { Token.Text: "false" }) + { + shortName = false; + } + } + + list.Add(new EnumToProcess( + enumDeclarationTypeSymbol, forTypeSymbol, membersToProcess, enumNamespace, shortName)); + } + + return list; + } + + private static void GenerateCode(SourceProductionContext context, EnumToProcess enumToProcess) + { + var code = GenerateCode(enumToProcess); + context.AddSource($"{enumToProcess.FullCsharpName}_for_{enumToProcess.ForTypeSymbol.ToDisplayString()}.g", + SourceText.From(code, Encoding.UTF8)); + } + + private static string GenerateCode(EnumToProcess enumToProcess) + { + var builder = new CodeBuilder(); + + var isVisible = enumToProcess.EnumSymbol.IsVisibleOutsideOfAssembly(); + var methodVisibility = isVisible ? "public" : "internal"; + + var typeName = enumToProcess.ForTypeSymbol.ToDisplayString(); + + builder.AppendLine("/// ").AppendLine(); + + if (!string.IsNullOrEmpty(enumToProcess.FullNamespace)) + { + builder.Append("namespace ").Append(enumToProcess.FullNamespace!).AppendLine(); + builder.OpenBrackets(); + } + + builder.AppendLineWithIdent("[System.Serializable]"); + builder.AppendIdent().Append(methodVisibility).Append(" class ").AppendLine(enumToProcess.ClassName); + builder.OpenBrackets(); + + foreach (var member in enumToProcess.Members) + { + builder + .AppendIdent().Append("[UnityEngine.SerializeField] public ") + .Append(typeName).Append(" ") + .Append(member.Name).AppendLine(";"); + } + + builder.AppendLine(); + builder + .AppendIdent().Append("public ").Append(typeName) + .Append(" Get(").Append(enumToProcess.FullCsharpName).AppendLine(" key)"); + builder.OpenBrackets(); + builder.AppendLineWithIdent("return key switch"); + builder.OpenBrackets(); + foreach (var member in enumToProcess.Members) + { + builder.AppendIdent().Append(enumToProcess.FullCsharpName).Append(".").Append(member.Name) + .Append(" => ").Append(member.Name).AppendLine(","); + } + builder.AppendLineWithIdent("_ => throw new System.ArgumentOutOfRangeException(nameof(key), key, null),"); + builder.DecreaseIdent().AppendLineWithIdent("};"); + builder.CloseBrackets(); + + builder.CloseBrackets(); + + if (!string.IsNullOrEmpty(enumToProcess.FullNamespace)) + { + builder.CloseBrackets(); + } + + return builder.ToString(); } +} \ No newline at end of file diff --git a/UnityAttributes/UnityAttributes.csproj b/UnityAttributes/UnityAttributes.csproj index dc39ea0..ec372a0 100644 --- a/UnityAttributes/UnityAttributes.csproj +++ b/UnityAttributes/UnityAttributes.csproj @@ -1,7 +1,7 @@ - 1.3.1 + 1.4.0 netstandard2.0 UnityAttributes latest