[C#] ScribanSourceGenerator によるコンパイル時コード生成



# はじめに

本記事では .NET向けのスクリプト言語エンジンの [scriban](https://github.com/scriban/scriban) を使用して、コンパイル時にC#コードを生成する [ScribanSourceGenerator](https://github.com/ufcpp/ScribanSourceGenerator) の使用方法を紹介します。

「scriban 言語の詳細なんて興味ねぇ！ とにかくコードを生成したいんだ！」 って人向けの内容です。

私自身が scriban を使用した経験がなくBing様に助けを求めたのですが、世間話程度の回答しかもらえなかったので、 Bing様をより賢くする目的で調査した内容をまとめました。

![bing_capture_230401.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249816/726e426e-90f3-92c4-8c44-61cde0465792.png)

# T4テキストテンプレート との比較

.NET のコード生成では 古来より [T4 テキスト テンプレート](https://learn.microsoft.com/ja-jp/visualstudio/modeling/code-generation-and-t4-text-templates) が用意されていますが、残念ながら Visual Studio (for Windows) でしか使用できません。 またメンテさている様子もないことから将来性を感じません。

私自身は上記理由のみで十分に T4 を忌避するのですが、日本C#界の父であり（私が勝手に思ってる）ScribanSourceGenerator 作者の ufcpp さんが 脱T4のメリット をまとめてくださっていますので以下に引用いたします。

[UfcppSample/Demo/2022/NoMoreT4 at master · ufcpp/UfcppSample](https://github.com/ufcpp/UfcppSample/tree/master/Demo/2022/NoMoreT4)  より

> 脱 T4 メリット
>
> - 速い
> - 普通の C# だけで書ける
>   - 補完・コード解析が働く
> - Visual Studio 以外で使える
>   - 「Visual Studio 上で保存時にコード生成」がきつかった
>   - この Roslyn Source Generator な時代に、誰も Source Generator 化しないあたりでお察し
> - csproj がすっきり
>   - `<None Update="T4Generator.tt">` みたいな謎タグ要らない
>   - `<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />` とかいう謎サービス要らない
>   - `<PackageReference Include="System.CodeDom" />` とかいうつらみあるパッケージ参照要らない
> - Visual Studio 自体のバージョン・表示言語に依存しない
>   - TextTemplatingFilePreprocessor の生成結果は「T4 ファイルを編集して保存した PC」に依存しちゃう
>   - 複数人編集すると Git 差分が悲惨なことになってた



# ScribanSourceGenerator によるコード生成

## 0. 前提

未確認ですが [ScribanSourceGenerator](https://www.nuget.org/packages/ScribanSourceGenerator) は Visual Studio 2022 以上（Roslyn 4 以上）でないと動作しないと思います。 [ISourceGenerator](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.isourcegenerator) はサポートしてなさそう（[IIncrementalGenerator](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.iincrementalgenerator) のみの提供）なためです。 

## 1. 準備

改めて整理するほど重要な内容ではないと思っていますが、話を順に進めるためワンパス通すところまでをまとめます。

**パッケージ導入**

まずは .NETプロジェクトに  [ScribanSourceGenerator](https://www.nuget.org/packages/ScribanSourceGenerator) パッケージをインストールしましょう。 本記事の作成時点の最新版は 1.1.0 です。

```ps
> dotnet add package ScribanSourceGenerator
```

**コード生成の方法**

コード生成には、Source Generator (C#9 以降) を利用する方法 と scribanテキストファイルをパースする方法 の2種類があります。 今回は より便利と思ってる前者 [^1] のみを紹介します。

**Source Generator によるコード生成**

対象の partial クラスに `ScribanSourceGenerator.ClassMemberAttribute` を付与し、引数の文字列に scribanスクリプト を記述します。 以下は scriban を使用しておらずコード生成の旨味はないです。

<INS>生成元のコード</INS>

```cs
namespace ScribanSourceGeneratorDemo;
// 生文字列リテラル は C#11 以降
[ScribanSourceGenerator.ClassMember("""
        public void SayHello() => System.Console.WriteLine("Hello!");
    """)]
public partial class Demo1 { }
```

<INS>生成されたコード</INS>

```cs
// <auto-generated/>
namespace ScribanSourceGeneratorDemo {
partial class Demo1 {
    public void SayHello() => System.Console.WriteLine("Hello!");
}}
```

コード生成のワンパス対応は以上です。



## 2. scribanスクリプト言語

scriban の言語仕様の中からコード生成に利用されそうな部分のみを紹介します。 言語の詳細仕様は [scribanの公式ドキュメント](https://github.com/scriban/scriban/blob/master/doc/language.md) をご確認ください。

### 2-1. ブロック

scribanスクリプト言語には 下記3つ のブロックが存在します。

1. コードブロック

   `{{` と `}}` で囲まれたブロックは、scribanスクリプトエンジン によって解釈されます。

   ```scriban
   {{
     x = 5     # 変数 x に 5 が設定されます。（何も表示されません）
     x         # 5 が表示されます
     x + 1     # 6 が表示されます
     x         # 5 が表示されます
     # 565 が隙間なく表示されます
   }}
   ```

2. テキストブロック

   `{{` と `}}` で囲まれて**ない**ブロックは、テキストとしてそのまま出力されます。

   ```scriban
   Hello this is {{ name }}, welcome to scriban!
   ______________          _____________________
   ^ text block            ^ text block
   ```

3. エスケープブロック

   `{%{` と `}%}` で文字のエスケープができますが、コード生成では使用しなさそうなので割愛します。

### 2-2. 言語仕様

コード生成で使用しそうな scribanスクリプト を C#コード と対比して紹介していきます。

**ソースコメント**

```scriban
{{ $x = 1	# シングルライン
   $y = 2	## マルチ
               ライン ##
}}
```

**変数定義**

- 動的型付け なので型指定は不要です。
- ローカル変数には `$` を付与します。
- シンボルは スネークケース で記述します。

```scriban
// cs: int x = 123;
// cs: int byte_max = 0xff;
// cs: string s1 = "abc";

{{ $x = 123
   $byte_max = 0xff
   $s1 = "abc"       # 'abc' の指定も可
}}
```

**配列定義**

- 宣言時の各要素は `[]` で括ります。（C# のリストパターン ライク）
- 配列は index でアクセスできます。（C# と同様）
- 配列の要素数は `size` で取得できます。 (C# では Length プロパティ)

```scriban
// cs: string[] ss = new[] { "false", "true" };
// cs: int length = ss.Length;
// cs: string s = ss[^1];

{{ $ss = ["false","true"]
   $length = $ss.size
   $s = ss[length - 1]
}}
```

**if ステートメント**

`if` の処理が単一文であっても必ず `end` で閉じる必要があります。忘れないようにしましょう。

```scriban
// cs: if (i < 0) i = 0;

{{ # 複数行
   if $i < 0
     $i = 0
   end
}}

{{ if $i < 0; $i = 0; end; # 単一行 }}
```

**for ステートメント**

範囲を `i..j` で指定できますが、C# の `Range` 構造体 と異なり、scriban では `j` も含まれます。C# に揃えたい場合は `i..<j` を使用しましょう。
`for` の処理が単一文であっても必ず `end` で閉じる必要があります。忘れないようにしましょう。

```scriban
// cs: for (int i = 0; i <= 3; i++) { }

{{ for $i in 0..3  # 3も含まれます }}
     ~~
{{ end }}
```

```scriban
// cs: for (int i = 0; i < 3; i++) { }

{{ for $i in 0..<3  # 3は含まれません }}
     ~~
{{ end }}
```

**foreach ステートメント**

scriban では `for` を使用します。 `foreach` は存在しません。

```scriban
// cs: string[] ss = new[] { "false", "true" };
// cs: foreach (var s in ss) { }

{{ $ss = ["false","true"]
   for $s in $ss }}
     ~~
{{ end }}
```

**関数定義**

`func` で定義して `ret` で結果を戻し `end` で閉じます。 デフォルト引数にも対応しています。

```scriban
// cs: int Add(int x, int y, int z = 1) => x + y + z;

{{ func add(x,y,z=1)
     ret x + y + z
   end
}}
add(1,2) = {{ add(1,2)  #4 }}
```

可変長引数 にも対応しています。

```scriban
// cs: int GetSum(params int[] values) => values.Sum();

{{ func get_sum(values...)
     $sum = 0
     for $x in values
       $sum += $x
     end

     # Referencing with index
     # for $i in 0..<values.size
     #   $sum += values[$i]
     # end
     ret $sum
   end
}}
get_sum(1,2,3,4) = {{ get_sum(1,2,3,4)  #10 }}
```

以上を抑えておけば、なんとなくコード生成できると思います。

### 2-3. 生成コードの改行

scriban では 空白 と 改行 のコード生成もコントロールできます。

デフォルトだとコードブロック前後の 空白 と 改行 が そのまま出力されるため、無駄に隙間の多いコードが生成されます。 実動作への影響はありませんが、生成コードを読む機会もあると思いますので、多少はケアしてもよいかと思います。

**改行を含む空白の削除 `-`**

<INS>削除指定なし</INS>

```scriban
The yard-pound system '
{{ $s }}' disappear.

# The yard-pound system '
# must' disappear.
```

<INS>前の空白と改行を削除</INS>

`{{-` は、前方に遡って 空白以外の文字が登場するまで空白を削除します。 改行も空白に含まれます。

```scriban
The yard-pound system '
{{- $s }}' disappear.

# The yard-pound system 'must' disappear.
```

<INS>後ろの空白と改行を削除</INS>

`-}}` は、後方に進んで  空白以外の文字が登場するまで空白を削除します。 改行も空白に含まれます。

```scriban
The yard-pound system '{{ $s -}}

   ' disappear.

# The yard-pound system 'must' disappear.
```

**改行で動作が異なる削除 `~`**

<INS>削除指定なし</INS>

```scriban
{{ for $i in 2..<4 }}
    {{ $i }}
    {{ $i * $i }}
{{ end }}

#
#    2
#    4
#
#    3
#    9
#
```

<INS>前方の削除</INS>

`{{~` は、前方に遡って最初に登場する**改行までの空白**を削除します。**改行は削除されません。**

```scriban
{{ for $i in 2..<4 }}
    {{ $i }}
    {{~ $i * $i }}
{{ end }}

#
#    2
#4
#
#    3
#9
#
```

<INS>後方の削除</INS>

`~}}` は、後方に進んで最初に登場する**改行までの空白 と 改行** を削除します。

```scriban
{{ for $i in 2..<4 }}
    {{ $i ~}}
    {{ $i * $i }}
{{ end }}

#
#    2    4
#
#    3    9
#
```



## 3. コード生成の実例

コード生成で頻繁に使用されていそうな例を用意しました。これをコピって改変するのが手っ取り早いと思います。

### 3-1. 組み込み型のオーバーロード

各組み込み型に対する `TryParse(this string s, out type result)` メソッドを生成する例です。

<INS>生成元のコード</INS>

```cs
namespace ScribanSourceGeneratorDemo;

[ScribanSourceGenerator.ClassMember("""
    {{
    $types = ["bool","byte","sbyte","short","ushort","int","uint","long","ulong","float","double","DateTime","DateTimeOffset","TimeSpan"]
    for $t in $types
    ~}}
        public static bool TryParse(this string s, out {{$t}} x) => {{$t}}.TryParse(s, out x);
    {{ end }}
    """)]
public static partial class Demo2 { }
```

<INS>生成されたコード</INS>

```cs
// <auto-generated/>
namespace ScribanSourceGeneratorDemo {
partial class Demo2 {
    public static bool TryParse(this string s, out bool x) => bool.TryParse(s, out x);
    public static bool TryParse(this string s, out byte x) => byte.TryParse(s, out x);
    public static bool TryParse(this string s, out sbyte x) => sbyte.TryParse(s, out x);
	~~~ 割愛 ~~~
}}
```

### 3-2. 引数個数のオーバーロード

可変長引数 `params` による ヒープアロケーション を回避するため、n個の引数までオーバーロード メソッド を生成する例です。

<INS>生成元のコード</INS>

```cs
namespace ScribanSourceGeneratorDemo;

[ScribanSourceGenerator.ClassMember("""
    {{
    func get_word(max,words...);
      $s1 = "";
      for $i in 1..max
        $s2 = "";
        for $w in words
          $s2 += $w 

          # Do not join index at the end.
          if !for.last; $s2 += $i; end
        end

        $s1 += $s2
        if !for.last
            $s1 += ", "
        end
      end
      ret $s1;
    end
    -}}
    {{ for $i in 2..16 ~}}
        public static async Task<({{ get_word $i "T" "" }})> WhenAll<{{ get_word $i "T" "" }}>({{ get_word $i "Task<T" "> task" "" }})
        {
            await Task.WhenAll({{ get_word $i "task" "" }});
            return ({{ get_word $i "task" ".Result" }});
        }
    {{ end }}
    """)]
public static partial class Demo3 { }
```

<INS>生成されたコード</INS>

```cs
// <auto-generated/>
namespace ScribanSourceGeneratorDemo {
partial class Demo3 {
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        await Task.WhenAll(task1, task2);
        return (task1.Result, task2.Result);
    }
    public static async Task<(T1, T2, T3)> WhenAll<T1, T2, T3>(Task<T1> task1, Task<T2> task2, Task<T3> task3)
    {
        await Task.WhenAll(task1, task2, task3);
        return (task1.Result, task2.Result, task3.Result);
    }
    public static async Task<(T1, T2, T3, T4)> WhenAll<T1, T2, T3, T4>(Task<T1> task1, Task<T2> task2, Task<T3> task3, Task<T4> task4)
    {
        await Task.WhenAll(task1, task2, task3, task4);
        return (task1.Result, task2.Result, task3.Result, task4.Result);
    }
	~~~ 割愛 ~~~
}}
```



## ソースコード

上記の実例コードは以下にアップしています。

[hsytkm/ScribanSourceGeneratorDemo - GitHub](https://github.com/hsytkm/ScribanSourceGeneratorDemo)



# おわりに

.NETのコンパイル時コード生成パッケージの ScribanSourceGenerator の使用方法を紹介しました。

Bing様 が回答を持っておられなかったので（また Qiita にも記事がなかったので）まとめてみました。本記事が Bing様 に取り込まれ頒布されることを願っています🙏


# 確認環境

- Windows 11 22H2
- Visual Studio 2022 17.5.3
- .NET 7 (C#11)
- ScribanSourceGenerator 1.1.0



# 参考にさせていただいたページ

[Visual Studio 2022 17.5 Preview 2 - ufcpp YouTube](https://www.youtube.com/watch?v=_f6UA3Vs3Kc&t=3646s&ab_channel=ufcpp)  こちらの動画で 本Generator の存在を知りました。

[ufcpp/ScribanSourceGenerator - GitHub](https://github.com/ufcpp/ScribanSourceGenerator)

[scriban/scriban - GitHub](https://github.com/scriban/scriban)



[^1]:   Source Generator の場合、生成元コードの クラス名 から生成コードにジャンプできます。scriban テキストファイルではインテリセンスが効かないため そのような操作ができません。