しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Unity で Roslyn を使って属性からコード生成する

※ これは 2023/09/29 時点の Unity 2023.1.15f1 の情報です

最新版では動作が異なる可能性がありますのでご注意ください

前回で Unity の Roslyn を使った属性クラスコード生成はできたので、今度はこの属性定義を参照したコード生成をしてみる

生成コード側の SourceGenerator.cs を下記のように変更

一気に必要な記述が増えた

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SourceGenerator
{
    [Generator]
    public class CodeGenerator : ISourceGenerator
    {
        /// <inheritdoc/>
        public void Initialize(GeneratorInitializationContext context)
        {
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }

        private const string Code = @"using System;

namespace Sample
{
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
    public class SimplePropertyAttribute : Attribute
    {
        /// <summary>
        /// プロパティ名称
        /// </summary>
        public string PublicName { get; }

        /// <summary>
        /// フィールド名称
        /// </summary>
        public string LocalName { get; }

        /// <summary>
        /// コメント
        /// </summary>
        public string Comment { get; }

        /// <summary>
        /// プロパティ自動生成用属性
        /// </summary>
        /// <param name=""name"">名称</param>
        /// <param name=""comment"">コメント</param>
        public SimplePropertyAttribute(string name, string comment = null)
        {
            this.PublicName = char.ToUpper(name[0]) + name.Substring(1);
            this.LocalName = char.ToLower(name[0]) + name.Substring(1);
            this.Comment = comment;
        }
    }
}";

        /// <inheritdoc/>
        public void Execute(GeneratorExecutionContext context)
        {
            if (context.Compilation.AssemblyName.Equals("Assembly-CSharp")
                && context.SyntaxReceiver is SyntaxReceiver receiver)
            {
                context.AddSource("SimplePropertyAttribute.g.cs", SourceText.From(Code, Encoding.UTF8));

                // 属性のコードを生成時に参照できるようにする
                var options = (CSharpParseOptions)((CSharpCompilation)context.Compilation).SyntaxTrees[0].Options;
                var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(Code, options));

                this.CreateProperties(context, receiver, compilation);
            }
        }

        private void CreateProperties(GeneratorExecutionContext context, SyntaxReceiver receiver, Compilation compilation)
        {
            foreach (var classSyntax in receiver.TargetClasses)
            {
                var model = compilation.GetSemanticModel(classSyntax.SyntaxTree);
                var symbol = model.GetDeclaredSymbol(classSyntax);
                var name = symbol.Name;
                var properties = symbol.GetAttributes()
                    .Where(x => x.AttributeClass.Name == "SimplePropertyAttribute")
                    .Select(x => (Name: x.ConstructorArguments[0].Value.ToString(), Comment: x.ConstructorArguments[1].Value.ToString()))
                    .ToArray();

                var sb = new StringBuilder();
                sb.AppendLine("// <auto-generated/>");
                sb.AppendLine("using System.Collections.Generic;");
                sb.AppendLine();
                sb.AppendLine($"public partial class {name}");
                sb.Append("{");
                foreach (var property in properties)
                {
                    var intent = "\t";
                    var publicName = char.ToUpper(property.Name[0]) + property.Name.Substring(1);
                    var localName = char.ToLower(property.Name[0]) + property.Name.Substring(1);

                    sb.AppendLine("");
                    sb.AppendLine(intent + "/// <summary>");
                    sb.AppendLine(intent + $"/// {property.Comment}");
                    sb.AppendLine(intent + "/// </summary>");
                    sb.AppendLine(intent + $"public string {publicName}");
                    sb.AppendLine(intent + "{");

                    {
                        intent += "\t";

                        sb.AppendLine(intent + $"get => this.{localName};");
                        sb.AppendLine(intent + $"set");
                        sb.AppendLine(intent + "{");
                        {
                            intent += "\t";

                            sb.AppendLine(intent + $"if (this.{localName} != value)");
                            sb.AppendLine(intent + "{");
                            {
                                intent += "\t";

                                sb.AppendLine(intent + $"this.{localName} = value;");

                                intent = intent.Substring(0, intent.Length - 1);
                            }
                            sb.AppendLine(intent + "}");

                            intent = intent.Substring(0, intent.Length - 1);
                        }
                        sb.AppendLine(intent + "}");

                        intent = intent.Substring(0, intent.Length - 1);
                    }
                    sb.AppendLine(intent + "}");
                    sb.AppendLine(intent + $"private string {localName} = null;");
                }
                sb.AppendLine("}");

                context.AddSource($"{name}.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
            }
        }
    }

    internal class SyntaxReceiver : ISyntaxReceiver
    {
        /// <summary>
        /// 属性がついたコード生成対象クラス一覧
        /// </summary>
        public List<ClassDeclarationSyntax> TargetClasses = new List<ClassDeclarationSyntax>();

        /// <inheritdoc/>
        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
            switch (syntaxNode)
            {
                case ClassDeclarationSyntax classSyntax:
                    if (classSyntax.AttributeLists
                        .SelectMany(x => x.Attributes)
                        .Select(x => x.Name.NormalizeWhitespace().ToFullString().Split('.').Last())
                        .Contains("SimpleProperty"))
                    {
                        TargetClasses.Add(classSyntax);
                    }
                    break;
            }
        }
    }
}

重要なのは下記の箇所

                // 属性のコードを生成時に参照できるようにする
                var options = (CSharpParseOptions)((CSharpCompilation)context.Compilation).SyntaxTrees[0].Options;
                var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(Code, options));

属性クラスのコードのように、Roslyn による自動生成によるコードを利用するコードを参照する場合、こんな感じでいったんコンパイル結果にソースコードを含めないといけない模様

あとは下記のように ISyntaxReceiver を継承したコード変更の通知を受け取るためのクラスを利用しないといけない

    internal class SyntaxReceiver : ISyntaxReceiver
    {
        /// <summary>
        /// 属性がついたコード生成対象クラス一覧
        /// </summary>
        public List<ClassDeclarationSyntax> TargetClasses = new List<ClassDeclarationSyntax>();

        /// <inheritdoc/>
        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
            switch (syntaxNode)
            {
                case ClassDeclarationSyntax classSyntax:
                    if (classSyntax.AttributeLists
                        .SelectMany(x => x.Attributes)
                        .Select(x => x.Name.NormalizeWhitespace().ToFullString().Split('.').Last())
                        .Contains("SimpleProperty"))
                    {
                        TargetClasses.Add(classSyntax);
                    }
                    break;
            }
        }

SimpleProperty の属性が追加されているクラスを列挙して、TargetClasses に追加する処理を記述

この対象クラスに単純なプロパティコードを追加する生成をするようにした

次に VSCode のソリューションエクスプローラーの右クリックメニューから「リビルド」実行

出力先プロジェクトを開いている UnityEditor に戻り、[Ctrl] + [R] で再コンパイル

属性の付いた Test クラスを partial 指定に変更し、プロパティが生えているかどうか確認

ちゃんと生成できているっぽい