SourceGeneratorでらくらく(でもない)ソースコード自動生成

# ボイラープレートコードを自動実装した～い！
皆さんは**頻出する定形コードの実装**をどのようにやっていますか？私は[Excelの定義書から出力したり(宣伝)](https://qiita.com/ry18847/items/478f5ea4408f591ba2f2)、[T4テンプレートを使って静的にコード生成したり(宣伝)](https://qiita.com/ry18847/items/b2d12d49140a9077c3d2)しています。しかし、毎回Excelをいじってコード出力するのは面倒くさいし、デザイン時コード生成ではできない複雑な実装をしなければならないこともあります。
具体的には以下のようなコードです。

1. DBテーブル定義からCRUD操作の便利メソッドを作る[^1]
1. APIの送受信データ構造から定義クラスを生やして独自の構造の文字列にシリアライズする処理を自動実装する[^1]
1. APIの通信処理インターフェイスから特定のデータを送信/受信する具象クラスを生やす[^1]
1. 数値で扱うものをValueObjectに変えて`operator`を自動実装する[^2]
1. MVVMアプリでページネーションなどの類似した機能を持つ複数のViewModelに同じ実装を与える

<details><summary>補足：ページネーションについて</summary><div>

ページネーションはページ切替処理等の実装が必要ですが、実装内容は画面によらずほぼ同一の内容になります。これを抽象クラスで共通化すると、多重継承ができないため他にも類似した機能を持ちたい場合に困ります。普通のインターフェイスだと実装作業が各クラスに発生します。C#9の**インターフェイスのデフォルト実装**を使うこともできますが、VMの場合は「ページネーション機能を持つVM」という振る舞いをカプセル化して外部から扱うことはまずありません。このように、カプセル化やポリモーフィズムといった**オブジェクト指向設計の利点を活用しないなら、SourceGeneratorを使って各クラスに実装を閉じ込める**方が適切な場合があります。ただし**ほとんどはデフォルト実装を使うが特定の処理だけは開発者に実装を強制させたい**というケースならインターフェイスに軍配が上がります[^3]ので、例には上げましたが弊社ではこの方法を採っています。
</div></details>

[^1]: Excelからの自動実装でもまかなえますが、APIはコードベースで管理したほうが開発体験はいいです
[^2]: 株式会社Cysharpの河合さんの[UnitGenerator](https://github.com/Cysharp/UnitGenerator)が好例です。本稿を作るにあたって[河合さんのブログ](http://neue.cc/2020/12/15_597.html)を参考にさせていただきました
[^3]: `partial`メソッドを定義する方法もありますが、SourceGeneratorによる`partial`メソッドの活用は[**実装をSourceGeneratorが行う場合**で提案](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods#partial-methods)されています。実装を開発者が行いたい場合とは逆パターンですし、メソッドのシグネチャの統制がわかりやすくリファクタリング機能も使えるインターフェイスのほうが適切でしょう

かつてはコピペでコーディングされている方もいらっしゃったと思いますが、時代は2021年。**令和のボイラープレート実装はSourceGeneratorだ！**ということでSourceGeneratorについて説明していきたいと思います。
~~どういうわけかQiitaのSourceGeneratorの記事ってなぜか「XX作ってみた」系ばかりで作り方の説明を目的にする記事がないようですし~~

# 用意するもの

* Visual Studio 2019
* .NET Standard 2.0 SDKのインストール

SourceGeneratorによるジェネレーターのプロジェクトは**ターゲットフレームワークを`netstandard2.0`にする必要があります**。ただし、ジェネレータープロジェクトを参照するプロジェクトたちは`net5.0`で問題ありません。おそらく`netstandard2.0`以降である.NET Coreでも問題ないと思います(.NET Frameworkは未検証)。

# SourceGeneratorのしくみ
SourceGeneratorは以下のように**独自のジェネレーター属性**をつけてやると、その属性に紐付けた出力内容でソースコードを生成する仕組みになっています[^4]。ソースコードが作られるので、そのコードの編集はできませんが**ブレークポイントを置いてデバッグすることができます**。

```c#:sample.cs
[SampleGenerator]                // ジェネレーター対象の目印となる属性をつけて
public partial class Sample { }  // partialクラスを定義すると
```

```c#:sample_AutoGenerated.cs
// こんなクラスをVisual Studioが勝手に作ってくれる
// ただし「どういうルールを生成対象とするか」「どういうコードを作るか」は
// 自分でコンパイラの処理として組む必要がある
public partial class Sample
{
    string GeneretaedMethod() => "このクラスSampleは自動生成されました";
}
```

この出力内容はVisual Studioにキャッシュされますので、出力内容を変えても内容が反映されていなかったり、ビルドエラーになる場合はジェネレータを修正しても警告やエラーが出っぱなしになります。ジェネレータにバグを埋め込んで不正なコードを生成したりすると**VSが管理するRoslynCodeAnalysisService.exeが死んで何もできなくなります。**少しでもおかしなことになった場合は生成されたコードやジェネレータをよく確認しつつ、Visual Studioを再起動しましょう。

[^4]: 正確にはRoslynコンパイラに割り込んで「自分で構文解析した後に」「自分で文字列を作ってコンパイル対象として追加する」ことで生成される仕組みになっています。そのため属性を使わずに「特定の3文字から始まるクラスならコード生成」なんてことも可能です。ただ、「自動生成する対象は専用の属性をつける」というほうがあとから見て明らかにわかりやすいので、素直に属性を使いましょう

# ジェネレーターを作る前に
[T4テンプレートの説明記事(宣伝)](https://qiita.com/ry18847/items/b2d12d49140a9077c3d2#デザインの前に)でも書きましたが、**生成したいコードがどんなコードか**は先に考えておきましょう。特にSourceGeneratorはエラーが出ると面倒くさいので、明確にゴールを決めておくほうがイライラする回数は少ないでしょう[^5]。

今回のゴールは以下とします。必要なインターフェイスや抽象クラスはあらかじめ作っておきましょう。本稿ではコードを省略します。

1. 目的
    1. 独自の形式で符号化してサーバーと通信するための送受信データと処理を自動実装する。
1. 前提条件
    1. TCP通信とする。
    1. すべての送信データは`record`型で定義し、`IRequest`インターフェイスを実装する。このインターフェイスは、自分自身をバイト配列に変換する`byte[] Encode()`メソッドを持つ。
    1. すべての受信データは`record`型で定義し、`IResponse`インターフェイスを実装する。
    1. 各通信処理クラスは`ConnectionBase<T, U>`クラスを継承することとし、このクラスにTCP通信処理の大部分を記述する。`T`は`IRequest`、`U`は`IResponse`。このクラスは受信したバイト配列を受信データに変換する`protected abstract U Decode(byte[] received)`メソッドを持つ。
    1. 各通信処理クラスはDIのためにインターフェイスと具象クラスを1:1で持つこととする。
    1. 送受信するデータは**なるべく任意の型を指定可能とするが、SourceGeneratorで解析しやすいものに限る**。列挙体をメンバにしてデフォルト値を持たせたりという複雑なことはしない。
1. SourceGeneratorの動作のために必要なコード
    1. 「自動生成対象の送信データ」であることを示すための`RequestGeneretorAttribute`属性
    1. 「自動生成対象の受信データ」であることを示すための`ResponseGeneretorAttribute`属性
    1. 「自動生成対象の送受信処理」であることを示すための`ConnectionGeneretorAttribute`属性
    1. ビルド時に構文解析してソース生成するための処理
1. SourceGeneratorで作るゴールのコード

```c#:Request.cs
// こんな送信データは
[RequestGeneretor]
public partial record RequestSample(string Id, DateTime SendDate, int? Num);
// こう展開される
public partial record RequestSample : IRequest
{
    private IEnumerable<string> EnumerateMembers()
    {
        yield return Id;
        yield return SendDate.ToString();
        yield return Num?.ToString() ?? "";
    }

    public byte[] Encode()
    {
        /* エンコード処理 */
        return Encoding.UTF8.GetBytes(string.Join(",", EnumerateMembers.ToArray()));
    }
}
```

```c#:Response.cs
// こんな受信データは
[ResponseGeneretor]
public partial record ResponseSample(int MaxPoint, int MinPoint);
// こう展開される
public partial record ResponseSample : IResponse
{
    public const int MaxLength = 2;
    public static ResponseSample Decode(byte[] received)
    {
        /* デコード処理 */
        var text = Encoding.UTF8.GetString(received).Split(',');
        if (text.Length != MaxLength) throw new ArgumentException();
        return new ResponseSample(int.Parse(text[0]), int.Parse(text[1]));
    }
}
```

```c#:Connection.cs
// こんな通信処理は
[ConnectionGeneretor(typeof(RequestSample), typeof(ResponseSample))]
public interface IConnectionSample { }
// こう展開される
public class ConnectionSample : ConnectionBase<RequestSample, ResponseSample>, IConnectionSample
{
    protected override ResponseSample Decode(byte[] received) =>
        ResponseSample.Decode(received);
}
```

[^5]: おそらく1回は「なんじゃこりゃ！？」と叫びたくなり状況になります。ほとんどの場合、しょうもない文法不正ですのでよ～く内容を確認してください。ジェネレータ完成まで数十回はVSの再起動をすると思ってください。（これは将来的には改善されると思うのですが）

# ジェネレーターをつくろう
いきなり3つのことを考えるのは大変なので、まずは送信データの自動生成から行います。

## プロジェクトの作成
ソリューションエクスプローラーで[追加]→[新しいプロジェクト]を選択し、クラスライブラリを選択します。プロジェクト名はなんでもいいですが、わかりやすいように本稿では**`Generator`**プロジェクトとします。**ターゲットフレームワークで`.NET Standard 2.0`を選択することを忘れないようにしてください。**

<details><summary>忘れた場合</summary><div>

.csprojファイルを開いて、TargetFrameworkを`netstandard2.0`に手修正します。

```xml:Generator.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

</Project>
```
</div></details>

続いて、SourceGeneratorに必要なライブラリをNugetからインストールします。以下を実行してください。

```powershell:パッケージマネージャコンソール
Install-Package Microsoft.CodeAnalysis.Analyzers
Install-Package Microsoft.CodeAnalysis.CSharp
```

上記のインストールが完了すれば、ジェネレータークラス（ここでは`Generator.cs`とします）の作成が可能になります。以下のように`using`ブロックを記述し、`[Generator]`属性をつけて`ISourceGenerator`を実装してください。

```c#:Generator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Generator
{
    [Generator]
    public class Generator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            throw new NotImplementedException();
        }

        public void Initialize(GeneratorInitializationContext context)
        {
            throw new NotImplementedException();
        }
    }
}
```

`Execute`メソッドはソースの生成処理、`Initialize`メソッドは生成時の前処理を実装します。
ここで、SourceGeneratorのデバッグのためにデバッガーの起動を仕込んでおきます。`Debugger.Launch();`の行のコメントアウトを解除すると、Roslynコンパイラが働くたびにJust-In-Timeデバッガーが起動します。このデバッガーでSourceGeneratorのソリューションを選択することで、生成内容や構文解析内容を確認することができます。ただし頻繁に起動すると鬱陶しいので必要なとき以外はコメントアウトしておきましょう。

```c#:Generator.cs
using System.Diagnostics; // 追加する
        public void Initialize(GeneratorInitializationContext context)
        {
#if DEBUG
            if (!Debugger.IsAttached)
            {
                //Debugger.Launch();
            }
#endif
        }
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017924/e29bd161-1fbf-624b-0958-6ac97ccd4ccb.png)


## 初期化処理の実装と構文解析
ここでは、`Execute`時にややこしい構文解析を省略するために生成対象の属性である`RequestGeneratorAttirbute`を付与されたクラスの検索を行いましょう。
具体的には`ISyntaxReceiver`インターフェイスを実装するクラスを作ってそのクラスで検索し、そのクラスのインスタンスを`context.RegisterForSyntaxNotifications()`メソッドに登録することで`Execute`メソッドでの利用を行います。具体的なコードを見てください。

```c#:Generator.cs
    public void Initialize(GeneratorInitializationContext context)
    {
#if DEBUG
            if (!Debugger.IsAttached)
            {
                //Debugger.Launch();
            }
#endif
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    class SyntaxReceiver : ISyntaxReceiver
    {
        // attrは属性に引数がついたとき用
        public List<(RecordDeclarationSyntax type, AttributeSyntax attr)> Requests { get; } = new();
        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
            if (syntaxNode is RecordDeclarationSyntax r && r.AttributeLists.Count > 0)
            {
                // データのレコードの抽出
                var all = r.AttributeLists.SelectMany(x => x.Attributes);
                var reqAttr = all.FirstOrDefault(x => x.Name.ToString() is
                        "RequestGeneretor" or "RequestGeneretorAttribute");
                if (reqAttr != null)
                {
                    Requests.Add((r, reqAttr));
                }
            }
        }
    }
```

`OnVisitSyntaxNode`メソッドで構文解析をしています。引数の`syntaxNode`は構文木です。ここで前提条件に振り返ると、「すべての送信データは`record`型で定義」という規約にしていました。その「`record`型で定義」を特定するのが`syntaxNode is RecordDeclarationSyntax r`の部分になります。「RecordDeclarationSyntax」を分解すると「Recordを宣言する構文」になりますよね。つまり、**「この構文木が`record`の宣言をする構文木`RecordDeclarationSyntax`であるか」を判定すればよいことになります**。構文解析を行うと、`InterfaceDeclarationSyntax`を取得して`interface`の宣言を特定してインターフェイス名を取得したり、`MemberDeclarationSyntax`でクラス等のメンバーの宣言内容を特定したりすることが可能です。これらのクラスはすべて「`DeclarationSyntax`」というサフィックスがついたクラスになっているので、他の解析をしたければ探してみてください。`OnVisitSyntaxNode`の先頭にブレークポイントを置いてデバッグすることで、`syntaxNode`がどんな形で私達のコードを解釈しているかがわかると思います。

ところで、すべての`record`型を自動実装対象としたいわけではありません。`RequestGenerator`属性のついている`record`型のみを対象としていました。そのため、`record`の宣言に何らかの属性がついているかを`r.AttributeLists.Count > 0`で判定し、ついている属性の中に`RequestGeneratorAttribute`がいるかどうかを検索しています。属性付与は末尾の`Attribute`をつけてもつけなくても可能なため、属性の名前がフルネームの`RequestGeneratorAttribute`と略称の`RequestGenerator`のどちらかであるかを判定しています。
最終的に、自動実装対象となるコードの構文木(`syntaxNode`)を`Requests`メンバに`Add`して保持しておきます。`AttributeSyntax`も保持しているのは、将来的に属性の内容が拡張されたとき用です。今回は不要かもしれません。

## コード生成：RequestGeneratorAttribute.cs
初期化処理が完了すれば後は`Execute`メソッド内でコード生成を行います。まず、`RequestGeneratorAttribute`クラスの生成を行いましょう。自動生成は、`GeneratorExecutionContext`の`AddSource(string filename, string source)`メソッドを使います。生成したい文字列を作って`source`に指定することで、`filename`のファイル名で自動生成され、コンパイル対象に追加される仕組みです。

::: note
RequestGeneratorAttributeクラスを普通に作ることも可能ですが、そうすると「ジェネレーターで生成したコードを使うが自動生成自体はしないプロジェクト」にもジェネレータープロジェクトへの参照が必要になってしまいます。そのため、internalなRequestGeneratorAttributeを自動生成してジェネレーターの利用者プロジェクトの内部に閉じ込めることによってSourceGeneratorに関するコードを流出させないようにします。一般の開発者はジェネレーターの細かい仕組みを知る必要はないですし、ジェネレーターにバグがあるとVSが死ぬのを防ぐ必要もあるためです。
:::

ところで、コード生成といえば思い出すものがありませんか？そう、**T4テンプレート**です。`RequestGeneratorAttribute`は`Execute`メソッド内で生成するので、内容は不変ですがランタイムテキストテンプレートで定義します。注意点として、クラスを`internal`として定義することを忘れないでください。`public`にするとジェネレーターの利用者プロジェクトが複数存在する場合に名前の競合が発生してコンパイルエラーになります。また、[VS2019ではランタイムテキストテンプレートを追加しても`TextTemplatingFileGenerator`になってしまうので、追加した後は`TextTemplatingFilePreprocessor`に変えることも忘れずに行ってください。](https://qiita.com/ry18847/items/478f5ea4408f591ba2f2#ランタイムテキストテンプレートについて)

```T4:RequestGeneratorAttributeTemplate.tt
<#@ template debug="false" hostspecific="false" language="C#" linePragmas="false" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
// <auto-generated>
// THIS (.cs) FILE IS GENERATED BY Generator. DO NOT CHANGE IT.
// </auto-generated>
#pragma warning disable CS8669
#pragma warning disable CS8625
using System;
namespace Generator
{
    /// <summary>クライアントからサーバに送るデータであることを示します。<summary>
    [AttributeUsage(AttributeTargets.Class)]
    internal class RequestGeneratorAttribute : Attribute
    {
        public RequestGeneratorAttribute() { }
    }
}
```

T4テンプレートができたら後は文字列生成して`AddSource`します。以下のように`Execute`メソッドを実装してください。

```c#:Generator.cs
    public void Execute(GeneratorExecutionContext context)
    {
        SetDefaultAttribute(context);
    }
    /// <summary>自動生成する対象を示す属性のコードを生成に追加する</summary>
    private void SetDefaultAttribute(GeneratorExecutionContext context)
    {
        var attr1 = new RequestGeneratorAttributeTemplate().TransformText();
        context.AddSource("RequestGeneratorAttribute.cs", attr1);
    }

```

## コード生成：送信データ
すべての準備が整ったので、本当に生成したい内容の出力処理を書きます。
## テンプレート作成
最初に、自動生成する内容をT4テンプレートで記述しておきましょう。この作業により、構文解析で収集する必要のある情報も整理することができます。

```T4:RequestGeneratorTemplate.tt
<#@ template debug="false" hostspecific="false" language="C#" linePragmas="false" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
// <auto-generated>
// THIS (.cs) FILE IS GENERATED BY HanchoTrcGenerator. DO NOT CHANGE IT.
// </auto-generated>
#pragma warning disable CS8669
#pragma warning disable CS8625
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Generator;
namespace <#= Property.Namespace #>
{
    /// <summary>クライアントからサーバに送るデータであることを示します。/summary>
    public partial record <#= Property.Name #> : IRequest
    {
        private IEnumerable<string> EnumerateMembers()
        {
<# if (Property.Members.Any()) { #>
<# foreach (var member in Property.MembersToString) { #>
            <#= member #>;
<# }} else { #>
            yield break;
<# } #>
        }
        public byte[] Encode()
        {
            /* エンコード処理 */
            return Encoding.UTF8.GetBytes(string.Join(",", EnumerateMembers.ToArray()));
        }
    }
}
```
上記テンプレートより、必要なパラメータは以下であることがわかりました。

1. `record`を定義する名前空間
1. `record`の名前
1. `EnumerateMembers`メソッドで必要な、`record`に定義された各メンバーの名前と型
    1. `string`ならそのままメンバーを返す
    1. `Nullable`でないそれ以外なら`ToString()`の戻り値
    1. `Nullable`の場合は`null`のときは空文字、そうでなければ`ToString()`の戻り値

パラメータを渡すための`partial`クラスも作っておきます。これはコード生成プロセスでしか使用しないため、通常のファイルで問題ありません。

```c#:RequestGeneratorTemplate_partial.cs
namespace Generator
{
    partial class RequestGeneratorTemplate
    {
        public RequestGeneratorTemplate(RequestProtocol property)
        {
            Property = property;
        }

        public RequestProtocol Property { get; }
    }
    internal struct RequestProtocol 
    {
        public string Name;
        public string Namespace;
        public string[] MembersToString;
    }
}
```

## Executeメソッドの実行
`Execute`メソッドでは自動生成の対象となるコードを構文解析して必要な情報を取得します。対象となるコードの構文木は`SyntaxReceiver`クラスで持っていますので、`Initialize`メソッドで生成した`SyntaxReceiver`を取得します。もし`SyntaxReceiver`が取れなければそもそもソースコードがないので解析終了です。
構文解析はコードが長くなるため、専用のメソッド`GenerateRequest`に渡して処理します。`GenerateRequest`メソッドの内容は、`SyntaxReceiver`で抽出した対象のソース(`Requests`プロパティで保持)の構文を解析して必要なデータを取得(`GetProtocol`)し、T4テンプレートに流すだけです。

```c#:Generator.cs
    public void Execute(GeneratorExecutionContext context)
    {
        SetDefaultAttribute(context);

        var receiver = context.SyntaxReceiver as SyntaxReceiver;
        if (receiver == null) return;

        try
        {
            GenerateRequests(context, receiver);
        } 
        catch (Exception ex)
        {
            // logging
        }
    }
    /// <summary>送るデータのソースコードを生成する</summary>
    private void GenerateRequests(GeneratorExecutionContext context, SyntaxReceiver receiver)
    {
        try
        {
            if (!receiver.Requests.Any()) return;
            foreach (var (type, attr) in receiver.Requests)
            {
                var prop = GetProtocol(context, type);
                var template = new RequestGeneratorTemplate(prop);
                var code = template.TransformText();
                context.AddSource($"{prop.Namespace}.{prop.Name}_Auto.cs", code);
            }
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.ToString());
        }
    }
```
ここで、最初に作った属性生成メソッド`SetDefaultAttribute`は**必ず正常に実行されるようにしてください。**具体的には、`SetDefaultAttribute`以降のコードで例外が送出されないようにすべての処理を`try-catch`で囲みます。以下に、株式会社CySharpの河合さんのブログのSourceGeneratorに関する記事より引用します[^2]。

> やりかたは単純に最初に必要な属性を突っ込んでしまうという、ただそれだけなのですが一点注意なのは、この生成は絶対死守しましょう。Execute内で例外が発生したりすると、ここでAddSourceした属性の追加はキャンセルされます。
> (コード略)
> 特にIDEのインクリメンタルコンパイルが稼働している状態だと、入力途中の「不完全なコード」が頻繁に飛んできます。こうした不完全なコードによる不正な構文木を正しくハンドリングするのはかなり難しく、例外を飛ばしてしまうのは正直避けられません。しかし、何があっても最初に生成する属性のAddSourceだけは維持しないと、「入力途中の不完全コード→例外発生で属性が吹っ飛ぶ→属性が吹っ飛ぶので入力補完が効かないどころか書いてるものが全てエラーになる」という負のループが発生します。なので、これに関してはtry-catchで握り潰しOKです。

## GenerateRequestsメソッドでの構文解析
T4テンプレートの作成より、必要なパラメータはわかっています。表にまとめると以下内容となります。なお、解析を簡略化するため送信データの各メンバは[**位置指定パラメーター**](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/record#positional-syntax-for-property-definition)によって定義するものとします。

| パラメータ | 解析内容 |
|:-|:-|
| `record`を定義する名前空間  | ソースコードに記述した`namespace`  |
| `record`の名前  | ソースコードに記述した`record`の名前  |
| `record`に定義された各メンバーの名前  | `record`宣言時にパラメータとして指定した各メンバーの名前  |
| `record`に定義された各メンバーの型  | `record`宣言時にパラメータとして指定した各メンバーの型  |

以下に、私が実際に調べた範囲での構文解析の方法を記します。以下の節のコードはすべて、自分で定義した`GetProtocol(GeneratorExecutionContext context, RecordDeclarationSyntax syntax)`メソッドの中のコードです。構文解析結果はT4テンプレートの`partial`クラスに定義した`RequestProtocol`構造体として返します。
### 型の名前と属する名前空間の取得
以下のコードで取得できます。`GetSemanticModel`等の詳細はちゃんと調べてないのですが、おそらく`record`本体の構文木`syntax`を含むより上位のコード(ファイル全体？)の構文木を取得し、そこから`namespace`に記述された文字列を取得していると思われます。

```c#:名前空間の取得
    var typeSymbol = context.Compilation.
                             GetSemanticModel(syntax.SyntaxTree).
                             GetDeclaredSymbol(syntax);
    if (typeSymbol == null) throw new Exception("can not get typeSymbol.");

    var recordName = typeSymbol.Name; // 型名
    var ns = typeSymbol.ContainingNamespace.ToDisplayString(); // 名前空間の文字列
```

### 位置指定パラメーターの取得
位置指定パラメーターの構文木の型は`ParameterListSyntax`です。ここで、以下のようなレコードの宣言を考えます。レコード`A`は位置指定パラメーターがあり、`B`にはありません。

```c#
// パラメータあり
public partial record A(object Param1, object Param2, object Param3);
// なし
public partial record B;
```
この場合、`A`の後ろの`(object Param1, ~~)`の部分は[構文木の種別(`SyntaxKind`列挙体)](https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntaxkind?view=roslyn-dotnet-3.11.0)でいうと`ParameterList`になります。`SyntaxKind`の判定は構文木の各ノードの`IsKind`メソッドで判定可能です。そのため、以下のコードで位置指定パラメーターのカッコの中身の構文木を取得することができます。

```c#:位置指定パラメーターの構文木の取得
    // 位置指定パラメーターは高々1このみなのでFirstOrDefault
    var tmp = syntax.DescendantNodes().
                     FirstOrDefault(d => d.IsKind(SyntaxKind.ParameterList));
    if (tmp == null) return default; // 空のレコードをどうするかは仕様で決めておく
    // 位置指定パラメータの構文木
    var paramList = tmp as ParameterListSyntax;
```

位置指定パラメーターの構文木が取得できれば、後は各パラメータの構文木を取得して型情報や名前を解析するだけです。各パラメーターの構文木の型は`ParameterSyntax`となります。また、それぞれのパラメーターは`ParameterListSyntax`の`Parameters`プロパティから取得できます。よって、以下のコードで取得できます。

```c#:各パラメーターの構文木の取得
    // 各パラメータの構文木を収めたIEnumerable<T>
    var param = paramList.Parameters.Cast<ParameterSyntax>();
```
### レコードのパラメーターの解析と取得
レコードのパラメーターを解析して何がやりたいのかというと型名と名前の取得でした。名前は以下で簡単に取得できます。

```c#:各パラメーターの構文木の取得
    foreach (ParameterSyntax p in param) // 説明のためにvarでなく型を明示します
    {
        var name = p.Identifier.Text; // 各パラメータの名前を取得
    }

```

型情報は`ParameterSyntax`の`Type`に定義されているのですが、プリミティブ型やらジェネリックコレクションやらを考慮すると、任意の型を特定するのはそれなりに難解ですので、デバッガーで中身を見ながら必要な情報がどこにあるのか探すことをおすすめします。また、あらゆる型を受け入れ可能にすると前提条件で述べたような「デフォルト値の指定(デフォルト値の構文はどうやって解析する？)」「`IEnumerable<T>`の入れ子(入れ子は何段まで許容する？)」など頻度が低い割にややこしいコードも考慮する必要があり、今後の保守も考えると割りに合わないことになるかもしれません。遅くともこの段階で**自動生成対象のコードを記述する際のルール決め**をして利用者に情報共有するべきでしょう。

以下に、私が調べた型の特定方法を記述します。型名がわかっているなら文字列で引っ掛けるのが一番手っ取り早いです。配列の場合は`ArrayTypeSyntax`が絡んでくると思うのですが、どうなんでしょう？`IEnumerable<T>`の判定に近いやり方かもしれません。

| 型 | 特定する条件 |
|:-:|:-|
| `string`  | `p.Type.ToString() == "string"`  |
| `object`  | `p.Type.ToString() == "object"`  |
| `DateTime`  |  `p.Type.ToString() == "DateTime"` |
| `Nullable<T>`  | `p.Type is NullableTypeSyntax n`  |
| 組み込み型  | `p.Type is PredefinedTypeSyntax pre`  |
| `IEnumerable<T>`  |  `p.Type is GenericNameSyntax g && g.Identifier.Text == "IEnumerable"` |

<details><summary>補足：組み込み型について</summary><div>

似たようなものに**プリミティブ型**がありますが、これは**`int`や`double`などの予約語に含まれる数値型のうち`decimal`以外のもの**です。組み込み型は**組み込み値型**と**組み込み参照型**の2種類が存在します。組み込み値型は**プリミティブ型+`decimal`**で、組み込み参照型は**`object`と`string`と`dynamic`**です。`PredefinedTypeSyntax`は組み込み参照型＋組み込み値型の構文木となります[^6]。
</div></details>

### 最終的なGetProtocolメソッド
最終的に以下のようなコードになります。これで送信データ型を自動生成するGeneratorを作ることができました。長いので折りたたんでおきます。この例では解析と同時にコードに埋め込む`yield return`の行の内容を生成していますが、型名とメンバ名を返してT4テンプレート内で制御するほうが見通しがよくなるかもしれません。

<details><summary>GetProtocolメソッドのサンプルコード</summary><div>

```c#:GetProtocol
    private RequestProtocol GetProtocol(GeneratorExecutionContext context, RecordDeclarationSyntax syntax)
    {
        var typeSymbol = context.Compilation.
            GetSemanticModel(syntax.SyntaxTree).
            GetDeclaredSymbol(syntax);
        if (typeSymbol == null) throw new Exception("can not get typeSymbol.");

        // recordの各メンバーの名前を取得
        var protocol = new RequestProtocol
        {
            Name = typeSymbol.Name,
            Namespace = typeSymbol.ContainingNamespace.ToDisplayString(),
            MembersToString = Array.Empty<string>()
        };
        var tmp = syntax.DescendantNodes().
            FirstOrDefault(d => d.IsKind(SyntaxKind.ParameterList));
        if (tmp == null) return protocol; // 空のレコードならメンバーは不要

        var paramList = tmp as ParameterListSyntax;
        var param = paramList.Parameters.Cast<ParameterSyntax>();

        protocol.MembersToString = Create(param).ToArray();
        return protocol;
        static IEnumerable<string> Create(IEnumerable<ParameterSyntax> para)
        {
            foreach (var p in para)
            {
                var name = p.Identifier.Text;
                if (p.Type.ToString() == "string")
                {
                    yield return $"yield return {name}";
                }
                else if (p.Type.ToString() == "DateTime")
                {
                    yield return $"yield return {name}.ToString()";
                }
                else if (p.Type is NullableTypeSyntax n)
                {
                    yield return $"yield return {name}?.ToString() : string.Empty";
                }
                else if (p.Type is PredefinedTypeSyntax pre)
                {
                    yield return $"yield return {name}.ToString()";
                }
                else if (p.Type is GenericNameSyntax g && g.Identifier.Text == "IEnumerable")
                {
                    yield return $"foreach (var __{name}__ in {name}) yield return __{name}__.ToString()";
                }
            }
        }
    }
```
</div></details>

### 自動生成対象の属性に追加情報をつける場合
自動生成する型がジェネリック型の場合はジェネリック制約をかけたり、特定の定数値や初期値を持たせたいことがあります。この場合、目印となる属性に設定値をもたせたうえで、設定内容の構文木を解析して行います。解析方法さえわかれば列挙体のメンバ名の取得等も可能です。
例えば以下のようなパターンです。

``` c#:SampleAttribute.cs(これはSourceGenerator内部でT4から自動生成する)
namespace Generator
{
    /// <summary>設定値つき属性</summary>
    [AttributeUsage(AttributeTargets.Interface)]
    internal class SampleAttribute: Attribute
    {
        private string ComName;
        private Type RequestType;
        private Type ResponseType;
        private CommunicationType CommunicationType;
        /// <summary>
        /// 通信処理インターフェイスの内容を初期化します。
        /// </summary>
        /// <param name="trcName">通信先の処理名</param>
        /// <param name="reqestType">送信するデータの型。</param>
        /// <param name="responseType">受信するデータの型。</param>
        /// <param name="comType">通信の種別。</param>
        public SampleAttribute(string comName, Type reqestType, Type responseType, CommunicationType comType = CommunicationType.Normal)
        {
            ComName = comName;
            RequestType = reqestType;
            ResponseType = responseType;
            CommunicationType = comType;
        }
    }

    /// <summary>通信処理のタイプを規定します。</summary>
    internal enum CommunicationType
    {
        /// <summary>複数回の送信が可能な通常のタイプです。</summary>
        Normal,
        /// <summary>一回の受信でOnCompleteを送出するタイプです。</summary>
        OnlyOnce,
    }
}
```

```c#:Generator.cs
    /// <summary>
    /// 通信処理のソースコードを生成する
    /// </summary>
    /// <param name="context"></param>
    /// <param name="receiver"></param>
    private void GenerateCommunicator(GeneratorExecutionContext context, SyntaxReceiver receiver)
    {
        try
        {
            if (!receiver.Models.Any()) return;
            var list = new List<CommunicatorTemplate.CommunicatorProperty>();
            foreach (var (type, attr) in receiver.Models)
            {
                if (attr.ArgumentList is null) continue;

                var model = context.Compilation.GetSemanticModel(type.SyntaxTree);

                var typeSymbol = context.Compilation.
                    GetSemanticModel(type.SyntaxTree).
                    GetDeclaredSymbol(type);
                if (typeSymbol == null) throw new Exception("can not get typeSymbol.");
                var name = typeSymbol.Name;
                if (name.Length <= 1)
                {
                    throw new Exception("Length of interface name is too short.");
                }
                else if (name[0] != 'I')
                {
                    throw new Exception("Interface name has to start 'I'.");
                }
                var prop = new CommunicatorTemplate.CommunicatorProperty()
                {
                    Namespace = typeSymbol.ContainingNamespace.ToDisplayString(),
                    InterfaceName = name,
                    ClassName = name.Substring(1) // 先頭の一文字が "I" であることを期待する
                };

                for (int i = 0; i < attr.ArgumentList.Arguments.Count; i++)
                {
                    var arg = attr.ArgumentList.Arguments[i];
                    var expr = arg.Expression;
                    if (i == 0)
                    {
                        // 処理名はリテラルで指定可能
                        if (expr is LiteralExpressionSyntax literal &&
                            literal.IsKind(SyntaxKind.StringLiteralExpression))
                        {
                            // 設定された文字列リテラルを大文字変換して使用する
                            prop.TrcName = literal.Token.ValueText.ToUpper();
                        }
                        else
                        {
                            throw new Exception("need string literal");
                        }
                    }
                    else if (i == 1)
                    {
                        // request type
                        prop.RequestType = GetTypeName(model, expr);
                    }
                    else if (i == 2)
                    {
                        // response type
                        prop.ResponseType = GetTypeName(model, expr);
                    }
                    else if (i == 3)
                    {
                        // CommunicationType列挙体の設定値を取得する
                        if (expr is MemberAccessExpressionSyntax mem &&
                            mem.Expression is IdentifierNameSyntax syn &&
                            syn.Identifier.ValueText == "CommunicationType")
                        {
                            prop.ComKind = mem.Name.Identifier.ValueText;
                        } 
                        else
                        {
                            // 指定の列挙体でなければNG
                            // 属性の第4引数はoptionalなので未指定ならそもそもelse if (i == 3)の中に来ない
                            throw new Exception("invalid enum type");
                        }
                    }
                }
                list.Add(prop);
            }

            foreach (var prop in list)
            {
                var template = new CommunicatorTemplate(prop);
                var code = template.TransformText();
                context.AddSource($"{prop.Namespace}.{prop.InterfaceName}_Auto.cs", code);
            }

            static string GetTypeName(SemanticModel model, ExpressionSyntax expr)
            {
                // "typeof"キーワードかどうかを判定する
                if (expr is TypeOfExpressionSyntax typeOfExpr)
                {
                    // typeof(xxx)のxxxが型でないならそもそも異常コードなのでエラー
                    if (model.GetSymbolInfo(typeOfExpr.Type).Symbol is not ITypeSymbol typeSymbol)
                        throw new Exception("require type-symbol.");
                    return typeSymbol.Name;
                }
                else
                {
                    throw new Exception("type failed");
                }
            }
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.ToString());
        }
    }
```

```c#:ジェネレーター利用者の例
// こんな書き方で属性を設定すると
[Sample("Abc", typeof(SampleRequest), typeof(SampleResponse)]
public interface IAbcCommunicator { }
// こんなコードが自動生成できる
public class AbcComunicator : CommunicationBase<SampleRequest, SampleResponse>, IAbcCommunicator
{
    public string ComName => "ABC";
    public SampleResponse Request(SampleRequest req) => base.Request(req);
}
```

[^6]: 型の一覧は[MSDN](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/built-in-types)を参照してください。わかりやすい図が紹介されていたので[StackOverFlowのリンク](https://stackoverflow.com/questions/13464641/c-sharp-predefined-types-vs-primitive-types)も載せておきます。

# ジェネレーターを使おう
長々と記述したのは**SourceGeneratorの実装方法**です。SourceGeneratorができたならそれを使う利用者側も作らないといけません。

## 利用者プロジェクトを作る
通常の手順で新規プロジェクトを同じソリューションに追加してください。ここでは`Customer`プロジェクトとします。ターゲットフレームワークは`.NET Standard 2.0`でなくても問題ありません。初期値とすればよいでしょう(2021年9月現在なら`.NET 5`)。
`Customer`プロジェクトは`Generator`プロジェクトをプロジェクト参照してください。参照設定後、`Customer`プロジェクトに以下のNugetパッケージを追加します。

```powershell:パッケージマネージャコンソール
Install-Package Microsoft.Net.Compilers.Toolset
```
パッケージ追加後、**Customer.csprojファイルを保存してから**Generatorプロジェクトへの参照設定に`Analyzer`設定と`ReferenceOutputAssembly`設定を追加します。

```xml:Customer.csproj(変更前)
  <!-- 変更前 -->
  <ItemGroup>
    <ProjectReference Include="..\Generator\Generator.csproj" />
  </ItemGroup>
```

```xml:Customer.csproj(変更後)
  <!-- 変更後 -->
  <ItemGroup>
    <ProjectReference Include="..\Generator\Generator.csproj" 
                      OutputItemType="Analyzer" 
                      ReferenceOutputAssembly="false" />
  </ItemGroup>
```

ジェネレーターがエラーを吐かないのにコード生成されない場合、上記3つ「プロジェクト参照の追加」「Nugetライブラリの取得」「.csprojファイルの編集」のいずれかを忘れている可能性が高いです。それでもうまくいかない場合はVisual Studioの再起動をしましょう。利用者がWPFプロジェクトの場合は最後の補足も確認してください。

## コードを生成してみよう
ここまでくれば、後は自動生成用の属性のついた`record`型を定義するのみです。

```c#:SampleRequest.cs
using Generator;
namespace Customer
{
    [RequestGenerator]
    public partial record SampleRequest(string Id, DateTime SendDate, int? Num);
}
```
このようにコードを書いてビルドしてみてください。SourceGeneratorにエラーがなくビルド完了すると、`SampleRequest`を選択して「定義へ移動」を選択したときに移動先の候補が2つ表示されるはずです。手で作った覚えのない方を選択して、タブに以下の文言が表示されれれば自動生成されています(赤色部分はGeneratorの名前)。生成内容におかしな部分がないか、確認してみてください。はじめのうちは不正なコードを出力しがちなので、だいたいビルドエラーになって意味不明なエラーが出たりJust-In-Timeデバッガーが起動したりします。デバッガーでSourceGeneratorの構文解析内容やT4テンプレートの出力コードを見てみることで修正を行ってください。ただしVisual Studioは前に生成した内容のキャッシュを残しているため修正してもエラーは残り続けますので、Visual Studioの再起動を忘れずに実施してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017924/39b85ff9-322a-5c60-3ca4-d80d4e51e8f9.png)
送信データでエラーなく期待通りの結果が得られたら、後は受信データや通信処理の自動生成を同様に組むだけです。
# 自動生成したコードのテスト
SourceGeneratorによる生成コードのテストはおおよそ以下の2種類に分かれるのではないでしょうか。[T4テンプレートで生成したコードのテスト](https://qiita.com/ry18847/items/b2d12d49140a9077c3d2#自動生成したコードのテスト)と同様に、特定の属性をつけたコードを自動生成とする以上、属性を外せばコードは消えてしまいますので、実際に使うコードでテストするのはやめましょう。必要ならユニットテスト以降のフェーズで実施すべきです。

1. 自動生成内容に実装を含まない場合
    1. 抽象クラスを継承させるようなパターンでは、抽象クラスを継承するモックを作って抽象クラスそのもののテストを行ってください。これはSourceGeneratorとは無関係に行えるはずです。
1. 自動生成内容にテストの必要な実装を含ませる場合
    1. テストプロジェクトを利用者プロジェクトとみなして参照設定等を行います。その後、テストプロジェクト内でモックを自動生成させ、その内容を検証します。

# まとめ
SourceGeneratorの実装は非常に複雑で大変です。構文解析をしないといけないですし、とにかく**Visual Studioの再起動が頻発する**からです。これに関してはいずれ改善されるとは思うのですが、利用者が少ないと優先順位も下がってしまうでしょう。しかしみんなでSourceGeneratorを使うことで、**開発工数は減るわ残業は減るわVSは更に使いやすくなるわ~~元々クソコードだった部分をコピペで増やされたあげく修正作業を押し付けられることがなくなって済むわ~~**といいことずくめです。みんなでSouceGeneratorを駆使してボイラープレートコードを駆逐していきましょう！
**よいSouceGeneratorライフを！**

# 補足：CS8032警告が出て自動生成されない場合
利用者側で以下の警告が出ることがあります。

| コード | 説明 | プロジェクト |
|:-|:-|:-|
| CS8032  | アナライザー `Generator.Generator`のインスタンスは `C:\(ソースのパス)\Generator\Generator\bin\Debug\netstandard2.0\Generator.dll`: ファイルまたはアセンブリ `'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'`、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。 から作成できません。  | `Customer`  |

この警告が表示された場合、GeneratorプロジェクトがNugetからインストールした`Microsoft.CodeAnalysis.CSharp`を含むライブラリ`Microsoft.CodeAnalysis.dll`の**バージョンが高すぎる**ようです（SourceGenerator絡みで破壊的変更があったんでしょうか？）。` -version 3.9`と指定してインストールし直してみてください。

# 補足：WPFプロジェクトでジェネレーターを使用するとCS0234エラーになる場合
利用者側のビルド時に、自動生成対象の属性の定義が見つからないエラーとなることがあります。特徴的なのは、**Visual Studioの「定義へ移動」で自動生成ソースへ移動できるのにビルド時にエラーになる**、**エラー一覧タブにはエラーが表示されない**、さらに**利用者がコンソールプロジェクトだと起きないが、WPFプロジェクトだと起きる**という３点です。
以下エラーでは自動生成対象の属性をGenerateTargetAttribute.csに定義しているとします。

| コード | 説明 | プロジェクト |
|:-|:-|:-|
| CS0234 | (属性を使用するソースファイル): error CS0246: 型または名前空間の名前 'GenerateTargetAttribute' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください) | `Customer`  |

まず「定義へ移動」で移動できる時点で、実装したソースジェネレーターとジェネレータープロジェクトそのものは**正しく動作しています**。対策として、利用者側のプロジェクトファイルの`<PropertyGroup>`タグに以下を定義するとビルドが通るようになります。

```xml:Customer.csproj
  <PropertyGroup>
<!-- ターゲットフレームワーク等の定義
    <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
-->

<!-- 以下をそのまま追加 --><IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
  </PropertyGroup>
```

:::note
利用者のWPFプロジェクトのターゲットフレームワークが.NET 6だとこのプロパティはデフォルトで`true`になるようです。.NET 5だとopt-inのため自分で設定が必要です。
:::


この問題については私もまだきちんと理解できていませんが、githubの以下issueを見て解決しました。内容のわかる方がいらっしゃったらコメントにて教えていただけると幸いです。

https://github.com/dotnet/roslyn/issues/52427

https://github.com/dotnet/wpf/issues/3404

※Qiitaにすでに記事がありました。WPFでSourceGeneratorを使おうとすると起きる問題のようです。

https://qiita.com/soi/items/0406338b67c86145bf52

# 補足：VS2019以前で作成した利用者プロジェクトをVS2022のC#10.0のコードにコンバートするとビルドエラーになる場合
`Microsoft.Net.Compilers.Toolset`を参照しているプロジェクトでは、ビルド時に使用される`Roslyn`コンパイラのバージョンは言語バージョンやターゲットフレームワークではなくプロジェクトで参照している`Microsoft.Net.Compilers.Toolset`のバージョンになります。
コンバート後に「**エディタではエラーがないのにビルド時にエラーになる！**」という現象が発生した場合は`Microsoft.Net.Compilers.Toolset`のNugetパッケージを最新に更新してください。

https://qiita.com/ry18847/items/d677f94f124fa2777abe

# 補足：ターゲットフレームワークが`net6-windows`のWPFプロジェクトで急に`CS0102`エラーが発生した場合

2022年7月15日時点で最新の.NET6のSDK（NET SDK 6.0.302）にバグがあり、NET6のWPFプロジェクトでジェネレータを利用するとSourceGeneratorが2回動作してコード重複エラーが発生するようです。
6.0.301のSDKでビルドされるように`global.json`を設定することで対処可能です。
詳しくは以下をご確認ください。

https://qiita.com/ry18847/items/108693106cba428e4bf5?_sm_byp=iVVDJnws2rqVnrMR