dotnetコマンドでUnity用SourceGeneratorを自作する

# 概要
UnityでSourceGeneratorが話題になってるものの、公式サイト等では Visual Studio での作成方法ばかりだったのでmacやLinuxで作るべく`dotnet`コマンドを使って実装してみました。
(つまり公式の手順のうちDLLのプロジェクト作成とビルドを`dotnet`コマンドでやっただけ)
その時の手順メモです。

# さっと知りたい人向けに公式サイトとの違い
以下の手順を置き換えるだけでした。

> Visual Studioで、.NET Standard 2.0 をターゲットとする .NET 標準ライブラリプロジェクトを作成します。

```sh
dotnet new classlib -lang "C#" -f "netstandard2.0" -o "MyGenerateAttribute"
```

> Microsoft.CodeAnalysis NuGet パッケージをインストールします。

```sh
dotnet add package "Microsoft.CodeAnalysis.CSharp" --version "3.8.0"
```

# 作成方法(フルバージョン)
```sh
# プロジェクト新規作成
dotnet new classlib -lang "C#" -f "netstandard2.0" -o "MyGenerateAttribute"
# 必要ならソリューションを作成
dotnet new sln
dotnet sln add MyGenerateAttribute/MyGenerateAttribute.csproj
# 必要なパッケージをインストール
cd MyGenerateAttribute
dotnet add package "Microsoft.CodeAnalysis.CSharp" --version "3.8.0"
# コード書いたらビルド
dotnet build -c Release
cp bin/Release/netstandard2.0/*.dll /path/to/UnityProject/Assets/Plugins
```
上記で配置し終わったらUnityで配置したDLLのインスペクタを開いて
- `Select platforms for plugin`から`Any Platform`のチェックを外す
- `Include Platforms`から全てのチェックを外す
- `Asset Labels`を`RoslynAnalyzer`に設定する

とすれば準備は完了です。
あとはUnityで通常通りコードを書くだけです。

# 今回作成したコード
とりあえず目標としては*フィールドに設定することでGetプロパティを生成する*SourceGeneratorを作りたいと思います。
理由は
- 既存コードからテンプレート的にコードを生成するための最低限の解析手順が踏めそう
- 生成自体も分岐が少なく簡単に出力できそう

という点で、公式サンプルから解析部分を読み取ってその結果を使って何かする、というのにちょうど良さそうな大きさだったからです。

名前とか、細かい部分は適当なので「このパターンのコードだとエラーになる」とかあるので適宜修正してください...

## SourceGenerator のコード
```csharp
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace MyLib
{
    [Generator]
    public class MyGenerator : ISourceGenerator
    {
        // Attributeのコード
        private const string ATTRIBUTE_CODE =@"
        namespace MyGenerateAttributeNS {
            /// <summary>
            /// フィールドのGetプロパティを生成する
            /// </summary>
            [System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
            internal sealed class MySourceGeneratorAttribute : System.Attribute
            {
                /// <summary>
                /// プレフィックス
                /// </summary>
                public string Prefix { get; set; }
                /// <summary>
                /// フィールドのGetプロパティ生成
                /// </summary>
                public MySourceGeneratorAttribute() {
                }
            }
        }";

        public void Initialize(GeneratorInitializationContext context)
        {
            context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
        }

        public void Execute(GeneratorExecutionContext context)
        {
            // Attribute のソースコードを追加
            context.AddSource("MySourceGeneratorAttribute.g.cs", ATTRIBUTE_CODE);
            if (!(context.SyntaxReceiver is MySyntaxReceiver syntaxReceiver))
                return;

            // Attribute のソースコードを解析
            CSharpParseOptions options = (context.Compilation as CSharpCompilation)
                .SyntaxTrees
                .First()
                .Options as CSharpParseOptions;
            var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(ATTRIBUTE_CODE, encoding: Encoding.UTF8), options));
            // SourceGenerator のAttributeが付与されたシンボルを取り出す
            var attributeSymbol = compilation.GetTypeByMetadataName("MyGenerateAttributeNS.MySourceGeneratorAttribute");
            var fieldSymbols = new List<IFieldSymbol>();
            
            foreach (var field in syntaxReceiver.CandidateFields)
            {
                var model = compilation.GetSemanticModel(field.SyntaxTree);
                foreach (var variable in field.Declaration.Variables)
                {
                    var fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                    if (fieldSymbol.GetAttributes().Any(att => att.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
                    {
                        fieldSymbols.Add(fieldSymbol);
                    }
                }
            }
            // 実際にAttributeが付与されたソースコードから新しいソースコードを作成する
            foreach (var group in fieldSymbols.GroupBy(f => f.ContainingType))
            {
                var code = MakeGetPropertyCode(group.Key, group.ToArray(), attributeSymbol);
                context.AddSource($"{group.Key}__MySourceGeneratorAttribute.g.cs", code);
            }
        }

        private string MakeGetPropertyCode(INamedTypeSymbol classSymbol, IFieldSymbol[] fieldSymbols, ISymbol attributeSymbol)
        {
            if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
            {
                return null;
            }
            var namespaceString = classSymbol.ContainingNamespace.ToDisplayString();
            var codeBuilder = new StringBuilder();
            codeBuilder.AppendLine($"namespace {namespaceString} {{");
            codeBuilder.AppendLine($"public partial class {classSymbol.Name} {{");
            foreach (var fieldSymbol in fieldSymbols)
            {
                var fieldName = fieldSymbol.Name;
                var fieldType = fieldSymbol.Type;
                var attributeType = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
                var prefixConstruct = attributeType.NamedArguments.Single(keyValue => keyValue.Key == "Prefix").Value;
                if (prefixConstruct.IsNull)
                {
                    continue;
                }
                var prefix = prefixConstruct.Value?.ToString();
                codeBuilder.AppendLine($"public {fieldType.ToDisplayString()} {prefix}{fieldName.Substring(0, 1).ToUpper()}{fieldName.Substring(1)} => {fieldName};");
            }
            codeBuilder.AppendLine("}");
            codeBuilder.AppendLine("}");
            return codeBuilder.ToString();
        }

        internal class MySyntaxReceiver : ISyntaxReceiver
        {
            public List<FieldDeclarationSyntax> CandidateFields = new List<FieldDeclarationSyntax>();
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                if (!(syntaxNode is FieldDeclarationSyntax fieldDecSyntax))
                    return;
                if (fieldDecSyntax.AttributeLists.Count <= 0)
                    return;
                this.CandidateFields.Add(fieldDecSyntax);
            }
        }
    }
}
```

## Unity側のコード
```cs
namespace Data {
public partial class UserData
{
    [MyGenerateAttributeNS.MySourceGenerator(Prefix = "")]
    private int id;
    [MyGenerateAttributeNS.MySourceGenerator(Prefix = "User")]
    private string name;

    public UserData(int id, string name) {
        this.id = id;
        this.name = name;
    }
}
}
```
```cs
using UnityEngine;

public class MainScene : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        var userData = new Data.UserData(1, "Taro");

        Debug.Log($"Generated Property: {userData.Id} {userData.UserName}");
    }
}
```

# 作成のポイント
[Unityドキュメント](https://docs.unity3d.com/ja/2022.3/Manual/roslyn-analyzers.html)にも書いてありますがバージョン指定などがされているのでコマンド実行時に指定を忘れないようにするのがポイントです。

- .NET のバージョン
  - .NET Standard 2.0
dotnet new classlib -lang "C#" <font color="#F08">-f "netstandard2.0"</font>
- Microsoft.CodeAnalysis のバージョン
  - Microsoft.CodeAnalysis 3.8
dotnet add package "Microsoft.CodeAnalysis.CSharp" <font color="#F08">--version "3.8.0"</font>

また、これに伴ってMicrosoftの[SourceGeneratorサンプル](https://github.com/dotnet/roslyn-sdk/tree/16.7/samples/CSharp/SourceGenerators/SourceGeneratorSamples)も`16.7`のtagを参照するのが良さそうです。
`main`ブランチの最新だといくつか`Microsoft.CodeAnalysis.CSharp 3.8`ではまだ使えないメソッドがありました。

# 参考
Unity マニュアルのページ:
https://docs.unity3d.com/ja/2022.3/Manual/roslyn-analyzers.html
Microsoft の SourceGenerator サンプル:
https://github.com/dotnet/roslyn-sdk/tree/16.7/samples/CSharp/SourceGenerators/SourceGeneratorSamples