※ これは 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 指定に変更し、プロパティが生えているかどうか確認
ちゃんと生成できているっぽい