【Unity】Incremental Source GeneratorでUnityに置いた設定ファイル(Additional files)を参照する方法

Incremental Source GeneratorでUnityに置いた設定ファイル(Additional files)を参照する方法です。

Unity2022.3.28f1

やりたいこと

Incremental Source Generatorを使うとコンパイル時に追加でソースコードを生成できます。
この方法は以下の記事にまとめています。

light11.hatenadiary.com

さて、上記の記事では Example アトリビュートを付けたクラスの部分クラスを生成する Incremental Source Generator を書いています。
いま、このアトリビュートについて、Example ではなく Unity 側に置いた設定ファイルに書かれたアトリビュートを使う仕組みに変えることを考えます。

Source Generator の Additional files を使うとこのように外部のファイルを読み込むことができます。

本記事ではこの方法についてまとめます。

設定ファイルを作成する

まず、Unity 側に設定ファイルを作成します。

Unity で Additional files を使うにはファイル名を以下の命名規則で作成する必要があります。

  • [任意の名前].[ソースジェネレータ名].additionalfile

したがって、今回は以下の名前で作成します。

  • Config.ExampleSourceGenerator.additionalfile

ファイルの中身はひとまず空白にしておきます。

Incremental Source Generatorを書き換える

次に、上記でリンクした記事で作成した Incremental Source Generator を以下のように書き換えます。

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

namespace ExampleSourceGenerator;

[Generator]
public class ExampleGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // AdditionalFile からアトリビュート名を取得
        var attributeNameProvider = context.AdditionalTextsProvider
            .Where(file => file.Path.Contains("Config.ExampleSourceGenerator.additionalfile"))
            .Select((file, _) =>
            {
                var text = file.GetText()?.ToString();
                return string.IsNullOrWhiteSpace(text) ? null : text.Trim();
            })
            .Where(attr => !string.IsNullOrWhiteSpace(attr))
            .Collect()
            .Select((list, _) => list.FirstOrDefault());
        
        var classDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                static (s, _) => s is ClassDeclarationSyntax,
                static (ctx, _) => (ClassDeclarationSyntax)ctx.Node
            );

        // Compilation, class declarations, attributeName を組み合わせて処理
        var combination = context.CompilationProvider
            .Combine(classDeclarations.Collect())
            .Combine(attributeNameProvider);

        context.RegisterSourceOutput(combination,
            static (spc, tuple) =>
            {
                var ((compilation, classes), attributeName) = tuple;
                if (attributeName is null)
                    return;

                foreach (var classDecl in classes)
                {
                    var model = compilation.GetSemanticModel(classDecl.SyntaxTree);
                    var symbol = model.GetDeclaredSymbol(classDecl);
                    if (symbol == null)
                        continue;

                    var hasAttribute = symbol
                        .GetAttributes()
                        .Any(attr => attr.AttributeClass?.ToDisplayString() == attributeName);

                    if (!hasAttribute)
                        continue;

                    GenerateSource(spc, classDecl);
                }
            });
    }

    private static void GenerateSource(SourceProductionContext context, ClassDeclarationSyntax classDeclaration)
    {
        var className = classDeclaration.Identifier.Text;
        var namespaceName = ((NamespaceDeclarationSyntax)classDeclaration.Parent!).Name.ToString();
        
        var source = $$"""
                       using System;
                       namespace {{namespaceName}}
                       {
                           public partial class {{className}}
                           {
                               public string GeneratedProperty { get; set; } = "Hello, World!";
                           }
                       }
                       """;
        var fileName = $"{className}.g.cs";
        context.AddSource(fileName, SourceText.From(source, Encoding.UTF8));
    }
}

コメントに書いたように、Config.ExampleSourceGenerator.additionalfile から対象のアトリビュート名を取得するように変更しています。

Unityで動作確認

ここまでできたらDLLをビルドして、Unityにインポートしてセットアップします。
この手順は上述の記事内にまとめているので割愛します。

インポートできたら、テスト用に以下のスクリプトを作成します。

using System;
using UnityEditor;
using UnityEngine;

namespace Example
{
    public class MyAttribute : Attribute
    {
    }
    
    [My]
    public partial class FooClass
    {
    }

    public static class Example
    {
        [MenuItem("Example/Test")]
        public static void Test()
        {
            var foo = new FooClass();
            Debug.Log(foo.GeneratedProperty);
        }
    }
}

この時点では Additional files に何も記載していないのでコンパイルエラーになります。
そのため次に、Config.ExampleSourceGenerator.additionalfile に以下のように記述します。

Example.MyAttribute

これでコンパイルエラーが出なくなればOKです。

Riderで動作確認

Riderでの動作確認については、基本的には上述の記事の手順で行えますが、追加で以下の手順が必要になります。

  1. Additional Files を動作確認用プロジェクト以下に作成
    1. 中身やファイル名はUnityに入れたものと同じでOK
  2. 以下のようにプロジェクト設定に AdditionalFiles の設定を追加する
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <LangVersion>11</LangVersion>
        <OutputType>Exe</OutputType>
    </PropertyGroup>

    <ItemGroup>
        <ProjectReference Include="..\ExampleSourceGenerator\ExampleSourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
    </ItemGroup>
    
    <ItemGroup>
        <AdditionalFiles Include="Config.ExampleSourceGenerator.additionalfile" />
    </ItemGroup>
    
</Project>

関連

light11.hatenadiary.com

参考

docs.unity3d.com