【C#】ソースジェネレータを使ってみる

C# 9.0からSource Generatorという機能が追加されています。
ざっくり言えば「ビルド時、コンパイル前にプログラムを生成するプログラムを実行する」機能です。
今回はGitの情報をプログラムに埋めてみます。

## Source Generatorを開発するための準備
### Visual Studioに追加コンポーネントを入れる
標準では入っていないのでVisual Studio Installerから追加します。
`.NET Compiler Platform SDK`をインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199145/b91e8f6f-b57b-a52c-e01a-34eb99932476.png)

・・・これコマンドでできないんですかね？
アップデートとかもGUIでやるのめんどくさいなーと思ってます。知らないだけかな・・

### プロジェクトの準備
なにはともあれ初期化します。いつものやつで、名前は「SourceGen」とします。
```
dotnet new sln --name SourceGen
dotnet new classlib	--name SourceGen
dotnet sln add
```

で、csprojを編集します。
- Microsoft.CodeAnalysis.CSharpへの参照を追加
- TargetFrameworkをnetstandard2.0に
  - （Visual Studioが未だに.NET Frameworkを抜け出せていないのが理由らしいです。）
- IsRoslynComponentをtrueに
- AnalyzerLanguageをcsに

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>11</LangVersion>
    <IsRoslynComponent>true</IsRoslynComponent>
    <AnalyzerLanguage>cs</AnalyzerLanguage>
  </PropertyGroup>
 
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
  </ItemGroup>

</Project>
```

## Gitから情報取得
今回は現在のブランチ名をソースに埋めてみます。

期待する結果としては
```cs
using SourceGen;

namespace SourceGenConsoleTest;

public class Program
{
	public static void Main(string[] args)
	{
		Console.WriteLine(HogeMoge.BranchName);
	}
}

[GenerateGitInformation]
public partial class HogeMoge
{
}
```
で、
```
> master
```
が表示できればOKです。
gitの情報を取得するのはProcess.Startで出力をいい感じに読み取ればできるので割愛します。

## ソースコードの出力
SourceGeneratorは`IIncrementalGenerator`をroslynが認識して実行することで動きます。
Attributeで言語も指定します。

```cs
[Generator(LanguageNames.CSharp)]
public class GitInformationGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
    }
}
```

IncrementalGeneratorInitializationContextに対してアレコレすることでソース生成をします。

まず`GenerateGitInformation`を生成させます。
参照元のプロジェクトで展開されるので、アクセスレベルはinternalで良いです。
```cs
public void Initialize(IncrementalGeneratorInitializationContext context)
{
    context.RegisterPostInitializationOutput(
		static context =>
		{
			context.AddSource(
				"GitVersionSourceGenerationAttribute.cs",
				"""
				namespace SourceGen;
				using System;

				[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
				internal sealed class GenerateGitInformationAttribute : Attribute
				{
				}
				""");
		});
}
```

そして、上記で追加した属性に対してソース生成を行います。
属性使わずにソース生成してもいいのですが、勝手に色々やられると厄介になりがちので、属性をつけることを起点としたほうがいいと思います。

`context.SyntaxProvider.ForAttributeWithMetadataName`で該当ソースを取得できるのでまずはそうします・

```cs
public void Initialize(IncrementalGeneratorInitializationContext context)
{
    // 省略

	var source = context.SyntaxProvider.ForAttributeWithMetadataName(
		"SourceGen.GenerateGitInformationAttribute", // 取得する属性
		(_, _) => true, // trueでOK 追加でなにか条件があればここにラムダ式とかでかける
		(syntaxContext, _) => syntaxContext); // 調べていません。ごめんなさい＞＜
}
```

sourceが取れたら、ごにょごにょして`context.RegisterSourceOutput`に渡します。
（正確には、ごにょごにょするメソッドを`context.RegisterSourceOutput`にわたす）

```cs
public void Initialize(IncrementalGeneratorInitializationContext context)
{
    // 省略

	var source = context.SyntaxProvider.ForAttributeWithMetadataName(
		"SourceGen.GenerateGitInformationAttribute", // 取得する属性
		(_, _) => true, // trueでOK 追加でなにか条件があればここにラムダ式とかでかける
		(syntaxContext, _) => syntaxContext); // 調べていません。ごめんなさい＞＜

	context.RegisterSourceOutput(source, Emit);
}

private static void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source)
{
	var typeNode = (TypeDeclarationSyntax)source.TargetNode;
	var typeSymbol = (INamedTypeSymbol)source.TargetSymbol;

    // 名前空間を考える。partialクラスで定義するので同じ名前空間にする
	var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace ? "" : $"namespace {typeSymbol.ContainingNamespace};";

    // ソースを考える
	var code =
		$$"""
		// <auto-generated/>
		{{ns}}

		partial class {{typeSymbol.Name}}
		{
			public static string BranchName => "{{GetBranchName()}}";
		}
		""" ;

    // ファイル名に使う
	var fullType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
		.Replace("global::", "")
		.Replace("<", "_")
		.Replace(">", "_");

    // 拡張子はg.csとする
	context.AddSource($"{fullType}.GitInformationGenerator.g.cs", code);

    // 割愛。
	string GetBranchName() => "master";
}
```

## デバッグ準備
ソース生成をされる側のプロジェクトを作って、先程作ったジェネレータを参照します。

```xml
 <ItemGroup>
    <ProjectReference Include="..\..\SourceGen\SourceGen.csproj">
      <OutputItemType>Analyzer</OutputItemType>
      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
    </ProjectReference>
  </ItemGroup>
```

これがちゃんと動けばOKです！

```cs
using SourceGen;

namespace SourceGenConsoleTest;

public class Program
{
	public static void Main(string[] args)
	{
		Console.WriteLine(HogeMoge.BranchName);
	}
}

[GenerateGitInformation]
public partial class HogeMoge
{
}
```

## まとめ
今回はgitの情報を取りましたが、色々とつかいみちがありそうです。
そのうちソースジェネレータのデバッグ方法を記事にしたいと思います。