【Unity用】Incremantal Source Generator プロジェクトの作り方

# Incremental SourceGenerator？
Incremental SourceGeneratorはざっくり言うとパフォーマンスを改善したSourceGeneratorです！
**Unity 2022.3.12f1以降** で利用可能です。

今回はIncremental SourceGeneratorを開発するためのプロジェクト構築と、
一部のエクステンションの導入、及び、導入したExtensionを使って簡単なGeneratorを作る方法を解説したいと思います！

::: note
Riderを利用した場合で解説していきます。
:::

# Solutionの準備
早速、Incremental SourceGeneratorを作成していきましょう！

## Solution作成

なにはともあれ、まずはSolutionを作成します！
SDK:6.0
言語:C#
Framework:netstandard2.0
で作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/e978a35f-af71-2a77-1145-38055e295893.png)

### NuGetパッケージの追加
ActionsからNugetを開き、Microsoft.CodeAnalysis.CSharpの4.1.0を導入します。

![名称未設定-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/7e793a82-51e7-7626-728c-523b7cce87fa.png)

同様にPolySharpの最新版もインストールしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/c829dd2f-ab23-5e7d-eea0-bf3c2ab0aa45.png)


## csprojファイルの変更

Explorerの表示をFileSystem表示に切り替えて、Solution名.csproj ファイルを開きましょう。
以下を参考に、不足部分を追記してください！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/23127cf8-fb71-38cd-e99f-73176d0cab8d.png)


```
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <!--ココカラ-->
        <LangVersion>12</LangVersion>
        <Nullable>enable</Nullable>
        <IncludeBuildOutput>false</IncludeBuildOutput>
        <DevelopmentDependency>true</DevelopmentDependency>
        <IsRoslynComponent>true</IsRoslynComponent>
        <!--ココマデ追記-->
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
      <PackageReference Include="PolySharp" Version="1.14.1">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      </PackageReference>
    </ItemGroup>


</Project>
```

## Extensionの追加

Unityの都合で、Microsoft.CodeAnalysis.CSharpパッケージはver 4.1.0を導入しなければなりません。
ですが、4.3.0から導入されている```ForAttributeWithMetadataName```というメソッドが大変便利で、ぜひ使いたいです…！
そこで、以下のURLからファイルをDLしてプロジェクトに追加し、無理やり使えるようにします。

https://github.com/amenonegames/IncrementalSourceGeneratorSample/tree/main/IncrementalSourceGeneratorSample/IncrementalSourceGeneratorSample/Shims

:::note
大元は、以下のRoslyn公式Gitから必要なcsファイルをDLして、一部をコメントアウトしたものになります。


<details><summary>大元のURL郡</summary>

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/CSharpSyntaxHelper.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/GlobalAliases.cs

https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/Roslyn/Hash.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/SyntaxValueProvider.ImmutableArrayValueComparer.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/ISyntaxHelper.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithMetadataName.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithSimpleName.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs

https://github.com/dotnet/roslyn/blob/main/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/SymbolVisibility.cs

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.Pop.cs#L11

:::


# 基本的なクラスの構成
ここまでで、環境構築は完了しました！
もりもりGeneratorコードを書いていきましょう…！！
とは言え、見知らぬメソッドが多いので、
基本的なメソッドと、Providerの利用方法について簡単に解説します。

::: note
別途、[デバッガーの導入方法も記事にしています](https://qiita.com/amenone_games/private/0a99f38fd08911b62c3a)。
効率が大きく上がるので、本格的な実装の前にデバッガーの環境構築をオススメします。
:::

すべての処理は、Initializeメソッド内で行います。
ザックリ言うと、

1. 現在のコードの情報を提供する``` Provider ```を生成し、
2. Providerの情報を解釈してソースを作るActionを定義し、
3. そのActionを、コードコンパイル時のパイプラインに登録する

という流れになります。
Generatorの最小構成はこんな感じになります。

```csharp
[Generator(LanguageNames.CSharp)]
public class SourceGenerator : IIncrementalGenerator
{

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput( 
            static (context)=> {/* アトリビュートを作成する*/});

        //Providerの生成
        var provider = context.SyntaxProvider.ForAttributeWithMetadataName
        (
            context,
            "作成したアトリビュートの名前を文字列で与える",
            static (node, cancellation) => node is ClassDeclarationSyntax /*対象にしたい区分を指定する*/,
            static (cont, cancellation) => cont /*データの変形が必要な場合は指定する*/
        )
        .Collect();


        // 引数で与えたアクションをコンパイル時のパイプラインに登録
        context.RegisterSourceOutput(
            provider,
            // 以下のStaticアクションが生成コードの定義
            static (sourceProductionContext, metaDataArray) =>
            {
                /*収集したデータを使ってクラス等を生成する*/
            });
    }
    
}
```

順に詳細を追っていきます。

::: note
Action系の引数をstaticにしているのは、その方がパフォーマンス面で有利だからです。
:::

# Providerについて
Incremental Source Generatorでは、
Providerを生成してパイプラインに渡すことで、コードの解析情報にアクセスします。

contextからは、以下のProviderにアクセスできます。

* SyntaxProvider
* CompilationProvider
* AdditionalTextsProvider
* AnalyzerConfigOptionsProvider
* MetadataReferencesProvider
* ParseOptionsProvider

その中で、今回使っているのは、```SyntaxProvider```です。
おそらくこれが最も使うことになるProviderかと思います。
今回はその中でも、先程導入した``` ForAttributeWithMetadataName ```メソッドの使い方について紹介していきます。

::: note
各Providerの詳細が知りたい型は[こちらの資料](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md)をご覧ください
:::

## ForAttributeWithMetadataNameメソッド
```IncrementalGeneratorInitializationContext```のメソッドである```ForAttributeWithMetadataName```メソッドについて解説します。
このメソッドは、指定したアトリビュートのついた情報を取り出し、Providerとして返却します。
下記の通り、```context.SyntaxProvider.ForAttributeWithMetadataName```で呼び出します。

```csharp
//Providerの生成
var provider = context.SyntaxProvider.ForAttributeWithMetadataName
(
    context,
    "作成したアトリビュートの名前を文字列で与える",
    static (node, cancellation) => node is ClassDeclarationSyntax /*対象にしたい区分を指定する*/,
    static (cont, cancellation) => cont /*データの変形が必要な場合は指定する*/
)
```

### 最初の引数 : context
最初の引数にはcontextを与えます。
Initializeメソッドの引数として渡っているので、そのまま渡してください。

### 二番目の引数 : fullyQualifiedMetadataName
二番目の引数には検索したいアトリビュートの名前を文字列で渡します。名前空間も渡す必要があるのでご注意ください。

例えば以下のようなクラスを作ったとすると…
```csharp
namespace SourceGeneratorSample
{
    [AttributeUsage(AttributeTargets.Class,
                    Inherited = false, AllowMultiple = false)]
        sealed class SampleAttribute : Attribute{}
}
```
与える文字列は```"SourceGeneratorSample.SampleAttribute"```になります。

### 三番目の引数 : predicate
いわゆる[predicate](https://learn.microsoft.com/ja-jp/dotnet/api/system.predicate-1?view=net-7.0)です。Funcを使って要素を絞ることができます。
ここで、trueを返す要素のみが、providerに反映します。

SyntaxNodeとCancellationTokenを引数に取ります。
そのため、判定はSyntaxNodeから構文上可能な範囲に制限されます。

上記の ```(node, cancellation) => node is ClassDeclarationSyntax``` のように、
SyntaxNodeの型判定をすることが多いようです。

::: note
AttributeTargets.Fieldで指定した要素をソートする場合、
VariableDeclaratorSyntaxで返ってくるのでご注意ください。
:::

### 四番目の引数 : select
LINQのSelectのように、データの内容を編集してProviderにしたい場合に利用します。

::: note
IncrementalSourceGeneratorの最適化機能を活用するためには、なるだけ無駄なデータを```Register```に持って行かないほうが良いです。
```Provider```の時点で必要なデータを抽出する必要があります。

今回は記法に慣れることを目的にした記事なので割愛しますが、実際に書く場合は```select```や```where```を有効活用して、データを抽出してみてください！
:::

## .Collect()メソッド
前述のコードの末尾にくっついている
``` .Collect() ``` は、Provider内の要素を分割するためのメソッドです。

ソートされた複数の要素は、そのままだと、単一の大きな塊としてProviederに登録されてしまいます。
要素ごとに分割しておくことで、後々扱いやすくなるので、付けておきましょう。
``` .Collect() ```を使うことで、各要素を別々の```GeneratorAtributeSyntaxContext```に変換し、それを ```ImmutableArray``` に格納することができます。

▼イメージ図
```
IncrementalValuesProvider<TSource>                IncrementalValueProvider<ImmutableArray<TSource>>
          ┌───────────┐                                  ┌─────────────────────────┐
          │           │          Collect<TSource>        │                         │
          │  TSource  ├─────────────────────────────────►│ ImmutableArray<TSource> │
          │           │                                  │                         │
          └───────────┘                                  └─────────────────────────┘
             3 Items                                             Single Item

              Item1                                         [Item1, Item2, Item3]
              Item2
              Item3
```
>[図は incremental-generators.mdより引用](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md)
>The MIT License (MIT) Copyright (c) .NET Foundation and Contributors

# Registerメソッド

続いて、コード生成方法を登録するメソッドについて、解説していきます。


##  context.RegisterPostInitializationOutpuメソッド
コード例の以下の部分です。
```csharp
context.RegisterPostInitializationOutput( 
    static (context)=> {/* アトリビュートを作成する*/});
```

その名の通り、Initializationの後に実行されるコード生成の登録です。
RegisterPostInitializationOutputには、Providerは渡せません。
初回のみの起動になるので、コード生成のキーとなるアトリビュートの生成などにご利用ください。

##  context.RegisterSourceOutputメソッド
コード例の以下の部分です。コード生成を定義する本体部分になります。

```csharp
// 引数で与えたアクションをコンパイル時のパイプラインに登録
context.RegisterSourceOutput(
    provider,
    // 以下のStaticアクションが生成コードの定義
    static (sourceProductionContext, metaDataArray) =>
    {
        /*収集したデータを使ってクラス等を生成する*/
    });
```

RegisterSourceOutputメソッドは、
Provderから解析したコンパイル情報を受け取り、
それを利用して、新しいコードを生成するための登録メソッドです。

### 最初の引数:source
この引数には、上記で生成したProviderを与えてください。

### 二番目の引数:action
このActionが、コードの生成方法を定義するActionになります。
Actionは二つの引数を持ちます。

* Actionの1つ目の引数は SourceProductionContext です
その名の通り、実際にソースコードを生成するときに必要となるcontextです。``` Source Generators API ```によって自動的に提供されるので、1つ目の引数には脳死で```sourceProductionContext```と書いておきましょう。最終的には、このコンテキストに生成するコードのファイル名と、コード内容の文字列を渡してコード生成を行います。
　
 
* Actionの2つ目の引数は、上記の```Provider<T>```が、 ``` T ``` として持っていた型が入ります。
.Collect()を使ってArrayに変換しておけば、ここでforeaceして各要素にアクセスすることができます。

::: note
メモリアロケーションの関係で、staticなactionにすることをオススメします。
:::

# GeneratorAttributeSyntaxContext構造体
今回紹介した方法を使うと、アクションの二つ目の引数Tは、```GeneratorAttributeSyntaxContext```構造体となります。
この構造体は以下の4つのプロパティを持っています。
* ```SyntaxNode```型
* ```ISymbol```型
* ```SemanticModel```型
* ```ImmutableArray<AttributeData>``` 型

この情報を解析してコードを生成することになるので、軽く説明します！

:::note
実際に作るときには、これらの型からどのように欲しい情報が抽出できるか、ChatGPTに聞いてみるのがオススメです。
:::

### ```SyntaxNode```型
要素の構文的な側面を主に表します。
```VariableDeclaratorSyntax```や```ClassDeclarationSyntax```など、各要素を表す継承クラスにキャストすると使いやすいです。
```TargetNode```プロパティからアクセスできます。

### ```ISymbol```型
各要素の識別情報や宣言の種類などを表現しています。
広範な情報を取り出すことができ、非常に有用です。
```INamedTypeSymbol```や、```IFieldSymbol```型にキャストすると使いやすいです。
```TargetSymbol```プロパティからアクセスできます。

### ```SemanticModel```型
コード全体の意味論的な構造を表現します。
現在の要素から横断して、関連する別要素などを追加取得したい場合などに有用です。
```SemanticModel```プロパティからアクセスできます。

### ```ImmutableArray<AttributeData>``` 型
このアトリビュート自体が付与された時のデータを保持しています。
例えば以下の様にすると、アトリビュートとともに渡された引数の情報を判定することができます。
```Attributes```プロパティからアクセスできます。

```csharp
foreach (var attributeData in AttributeDatas)
{
    foreach (var arg in attributeData.ConstructorArguments)
    {
        //ここで、argに対して必要な判定が可能です。
    }
}
```

## コードの生成
Providerの情報を使って、生成したいコードの文字列を作ったら、
上記の``` sourceProductionContext ```をつかてそれをコンパイル結果に組み込みます。
以下のようにAddSourceメソッドを使ってコードを生成します。

```csharp
sourceProductionContext.AddSource
    ($"/*作りたいコード名*/.g.cs", SourceText.From(/*コード本体の文字列*/, Encoding.UTF8));
```

# 最小の構成
以上の技術を使って、クラス名だけを引用してpartialClassを生成するコードを書いてみました。

```csharp
using System;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
using Microsoft.CodeAnalysis.Text;

namespace IncrementalSourceGeneratorSample
{
    [Generator(LanguageNames.CSharp)]
    public class SourceGenerator : IIncrementalGenerator
    {

        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            //対象となるクラスを判別するアトリビュートを生成する
            context.RegisterPostInitializationOutput( static x => SetAttribute(x));
            
            //Providerを生成
            var provider = context.SyntaxProvider.ForAttributeWithMetadataName
            (
                context,
                "SourceGeneratorSample.SampleAttribute",
                static (node, cancellation) => node is ClassDeclarationSyntax,
                static (cont, cancellation) => cont
            )
                .Collect();


            context.RegisterSourceOutput(
                provider,
                static (sourceProductionContext, metaDataArray) =>
                {
                    //collectしたProviderをForeachループで一つずつ取り出す
                    foreach (var meta in metaDataArray)
                    {
                        //個別のproviderからシンボル情報を取り出す
                        var symbol = meta.TargetSymbol;
                        var name = symbol.Name;
                        //シンボルの情報を使って、コード本体の文字列を生成する
                        var source = $@"
    public partial class {name}
    {{
        public void Hello()
        {{
            System.Console.WriteLine(""Hello, {name}!"");
        }}
    }}";
                        //生成した文字列をコードとして生成する
                        sourceProductionContext.AddSource($"{name}.g.cs", SourceText.From(source, Encoding.UTF8));
                    }

                });
        }

        static void SetAttribute(IncrementalGeneratorPostInitializationContext context)
        {
            //Sampleというアトリビュートクラスの文字列を定義
            const string AttributeText = @"
using System;
namespace SourceGeneratorSample
{
    [AttributeUsage(AttributeTargets.Class,
                    Inherited = false, AllowMultiple = false)]
        sealed class SampleAttribute : Attribute
    {
    
        public SampleAttribute()
        {   
        }
        
    }
}
";                
            //定義した文字列から、コードを作ってコンパイルする
            context.AddSource
            (
                "SampleAttribute.cs",
                SourceText.From(AttributeText,Encoding.UTF8)
            );
        }
    }
}
```

このコードを、次のClass1を持つプロジェクトで走らせてみます。

```csharp
[Sample]
public partial class Class1 { }
```
無事、以下のようなクラスが生成されました！！
```csharp
public partial class Class1
{
    public void Hello()
    {
        System.Console.WriteLine("Hello, Class1!");
    }
}
```

::: note warn
最小構成でわかりやすく表示するために、
namespaceの対応や、クラスのアクセスレベルの対応などを行っていません。
以下のレポジトリではそれらの対応をしたコードを書いていますのでご参考ください。
:::

::: note alert
ビルド時に```CSC : error CS1617: Invalid option '12' for /langversion. Use '/langversion:?' to list supported values.```などのエラーが出る場合は、[最新の.NetSDK](https://dotnet.microsoft.com/ja-jp/download/dotnet)をインストールして、MSBuildのバージョンを更新してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/e3bcc739-6c5b-9338-b753-234796022c80.png)
:::

# 利用方法
お疲れ様でした！
ビルドして、DLLをUnity上に配置し、RoslynAnalyzorとして登録すれば、利用可能です！

Unity上での設定方法などは、以下URLにて詳しく解説してますので、必要に応じてご覧ください！

https://qiita.com/amenone_games/items/25c857608af0138eb646#%E5%B0%8E%E5%85%A5

# レポジトリ
最後に、同ジェネレーターを使用して作ったレポジトリを掲載しておきます！
いずれも比較的単純な実装例となるため、参考にし易いかと思います！

### 最も単純な例

▼ここまでの内容を反映したレポジトリ：最も単純な構成です

https://github.com/amenonegames/IncrementalSourceGeneratorSample


:::note

上記コードから、
namespaceへの対応と、クラスのアクセス修飾子の反映を施したコードに書き換えてあります。

:::

### フィールドの情報を元に生成している例

▼Propertyの自動実装を行うIncremental Source Generatorのレポジトリ

https://github.com/amenonegames/AutoGeneratePropertyForNewerThan2022.2

▼GetComponent系のボイラーコードを省略するIncremental Source Generator

https://github.com/amenonegames/AutoComponentPropertyForNewerThan2022.2/tree/main

### ClassやInterfaceを元に生成している例　複数のProviderを結合している例

▼クラスやインターフェースの情報からNullObjectを自動生成するIncremental Source Generator

https://github.com/amenonegames/NullObjectGeneratorForNewerThan2022.2

# 謝辞およびその他のリソース

### 参考にさせていただいたレポジトリ

本記事の執筆、及び上記レポジトリの作成にあたっては、hadashiA様のhadashiA様のVyaml内に使われているIncremental Source Generatorの実装を参考にさせていただきました。
この場を借りて深くお礼申し上げます。

https://github.com/hadashiA/VYaml/tree/master/VYaml.SourceGenerator

### SyntaxTreeの確認
SyntaxTreeの確認には[SharpLab](https://sharplab.io/)が使えます。
[SyntaxNode型](https://qiita.com/amenone_games/items/762cbea245f95b212cfa#syntaxnode%E5%9E%8B)をキャストして使うときに、どんな情報が含まれているか確認しておくと大変便利です！

https://x.com/harumak_11/status/1785520669633188162

また、RiderのPluginでSyntax Treeを表示してくれるPluginがありました。
もともとのPlugin開発が止まってしまっているので、Forkされている分を掲載します。
Forkの内部まで確認したわけではありませんので、一応ご利用は自己責任にてお願いいたします。

https://github.com/c0dycode/Rossynt

:::note info
上記ポストに対する[Koji Hasegawa様のリプライで知りました。](https://x.com/nowsprinting/status/1785538319293812884)感謝です！
私の手元のRider2023.2.1では無事動作しました
:::

:::note info
Rider 2025.1より、Syntax Visualizer機能が追加される予定です！

https://blog.jetbrains.com/dotnet/2025/01/08/rider-2025-1-roadmap/
:::