Roslynによるインターフェースの実装クラスの構築

# 前書き
RoslynのCodeAnalysisを使用し、渡されたインターフェースから実装クラス構築に関するメモ。
構築の様を見るため、`null`を返すだけのスタブクラスにとどめていることに注意。

# 環境
[Roslynが作るコードツリーを覗く](https://qiita.com/ktz_alias/items/ed74a02256aac7978b4b)と同じ

# プロジェクト準備

手順の見極めのため、NUnitベース

```
% dotnet nunit -o StubClassGenTest
```

Roslyn APIのパッケージを加える

```
% dotnet add StubClassGenTest package Microsoft.CodeAnalysis.CSharp
```

#作成

以下のインターフェースに対する実装クラスを作成する。

```csharp
using NUnit.Framework;

using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace GenSyntaxTest {
    public class _スタブクラス生成に関するテスト {
        private static readonly CompilationUnitSyntax syntaxRoot;

        static _スタブクラス生成に関するテスト() {
            var sourceText = @"
            namespace GenSyntaxTest {
                public interface IColorDao {
                    IEnumerable<ColorData> ListAll();
                }
            }
            ";

            syntaxRoot = CSharpSyntaxTree.ParseText(sourceText).GetCompilationUnitRoot();
        }
    }
}
```

名前空間、クラス、メソッドについてのユニットテストを書くため、`CompilationUnitRoot`を定数として保持。

また、構文木の操作が何かと面倒なため、`Syntax`クラスに対する`Extension`および`Helper`を作成（内容は徐々に追記）

```csharp
public static class SyntaxGeneratorExtension {
    // (snip)
}

public static class SyntaxGeneratorHelper {
    // (snip)
}
```

## クラスパート生成

```csharp
public class _スタブクラス生成に関するテスト {
    // (snip)

    [Test]
    public void _クラス定義パートの生成() {
        var ns = (NamespaceDeclarationSyntax)syntaxRoot.Members[0];
        var intf = (TypeDeclarationSyntax)ns.Members[0];

        Assert.AreEqual(SyntaxKind.InterfaceDeclaration, intf.Kind(), "元の型のSyntax");

        var cls = intf.ToClassDeclaration("Impl");

        Assert.That(cls.Kind(), Is.EqualTo(SyntaxKind.ClassDeclaration), "生成したSyntax");
        Assert.That(cls.Modifiers.Any(SyntaxKind.PublicKeyword), Is.True, "生成したクラスのアクセス就職子");
        Assert.That(cls.Keyword.ToString(), Is.EqualTo("class"), "生成した型種");
        Assert.That(cls.Identifier.ToString(), Is.EqualTo("ColorDaoImpl"), "生成したクラスの型名");

        Assert.That(cls.BaseList.Types.Count, Is.EqualTo(1), "親クラス or インターフェースの数");
        Assert.That(cls.BaseList.Types[0].ToString(), Is.EqualTo("IColorDao"), "親インターフェース名");

        Assert.That(cls.Members.Count, Is.EqualTo(0), "生成されたメソッド数");
    }
}
```

`ToClassDeclaration`メソッドは、拡張クラスのメソッドとして用意

```csharp
public static class SyntaxGeneratorExtension {
    public static ClassDeclarationSyntax ToClassDeclaration(this TypeDeclarationSyntax inInterfaceSyntax, string inSuffix) {
        var intfName = inInterfaceSyntax.Identifier.Text;
        var clsTree = SyntaxFactory.ParseCompilationUnit($"public class {intfName.Substring(1)}{inSuffix}: {intfName}" + "{}");

        return (ClassDeclarationSyntax)clsTree.Members[0];
    }
}
```

アクセス修飾子や親クラスなど、直接構文木でやろうとするとかなり面倒。
`string interpolation`で構築した文字列をパーズすることで手を抜けそう。

## メソッドパート生成

```csharp
public class _スタブクラス生成に関するテスト {
    // (snip)

    [Test]
    public void _引数を持たないメソッドの生成() {
        var ns = (NamespaceDeclarationSyntax)syntaxRoot.Members[0];
        var intf = (TypeDeclarationSyntax)ns.Members[0];

        Assert.That(intf.Members.Count, Is.EqualTo(1), "用意されたインターフェースのメソッド数");
        Assert.That(intf.Members[0], Is.InstanceOf<MethodDeclarationSyntax>(), "用意されたインターフェースのメソッドSyntax");

        var meth = SyntaxGeneratorHelper.ToMethodStub((MethodDeclarationSyntax)intf.Members[0]);

        Assert.That(meth.Kind(), Is.EqualTo(SyntaxKind.MethodDeclaration), "生成したSyntax");
        Assert.That(meth.Modifiers.Any(SyntaxKind.PublicKeyword), Is.True, "生成したメソッドのアクセス就職子");
        Assert.That(meth.Identifier.ToString(), Is.EqualTo("ListAll"), "生成したメソッド名");
        Assert.That(meth.ReturnType.ToString(), Is.EqualTo("IEnumerable<ColorData>"), "生成したメソッドの戻り値型");

        Assert.That(meth.ParameterList.Parameters.Count, Is.EqualTo(0), "生成したメソッドの引数の数");

        Assert.That(meth.Body.Statements.Count, Is.EqualTo(1), "生成したメソッドの本文行数");
    }
}
```

`ToMethodStub`メソッドはヘルパークラスに定義した

```csharp
public static class SyntaxGeneratorHelper {
    public static MethodDeclarationSyntax ToMethodStub(MethodDeclarationSyntax inIntfMember) {
        var returnType = inIntfMember.ReturnType.WithLeadingTrivia(SyntaxFactory.Space);
            
        return 
            inIntfMember
            .WithSemicolonToken(default)
            .WithLeadingTrivia(SyntaxFactory.Space)
                .WithModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword).AsTokens())
            .WithBody(
                SyntaxFactory.Block(
                    SyntaxFactory.ReturnStatement(
                        SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
                        .WithLeadingTrivia(SyntaxFactory.Space)
                    )
                )
            )
        ;
    }        
}
```

注意点はインターフェースのメソッドに付与されたセミコロンを除去する必要があることのみ

また、`SyntaxToken`から`SyntaxTokenList`への変換は、メソッドチェインで書けるよう拡張クラスに定義した

```csharp
public static class SyntaxGeneratorExtension {
    // (snip)

    public static SyntaxTokenList AsTokens(this SyntaxToken inSyntax) {
        return new SyntaxTokenList(inSyntax);
    }
}
```

## 名前空間宣言の生成

```csharp
public class _スタブクラス生成に関するテスト {
    // (snip)

    [Test]
    public void _名前空間宣言の生成() {
        var u1 = SyntaxGeneratorHelper.ToUsingDirective("System");

        Assert.That(u1.Kind(), Is.EqualTo(SyntaxKind.UsingDirective), "生成されたusing[1]");
        Assert.That(u1.Name, Is.Not.InstanceOf<QualifiedNameSyntax>(), "生成された名前空間のSyntax[1]");
        Assert.That(u1.Name.ToString(), Is.EqualTo("System"), "生成された名前空間名[1]");

        var u2 = SyntaxGeneratorHelper.ToUsingDirective("System.Collections.Generic");

        Assert.That(u2.Kind(), Is.EqualTo(SyntaxKind.UsingDirective), "生成されたusing[2]");
        Assert.That(u2.Name, Is.InstanceOf<QualifiedNameSyntax>(), "生成された名前空間のSyntax[2]");
        Assert.That(u2.Name.ToString(), Is.EqualTo("System.Collections.Generic"), "生成された名前空間名[2]");

        var u2_3 = (QualifiedNameSyntax)u2.Name;

        Assert.That(u2_3.Right, Is.Not.InstanceOf<QualifiedNameSyntax>(), "生成された名前空間[2]の第3パートSyntax");
        Assert.That(u2_3.Right.ToString(), Is.EqualTo("Generic"), "生成された名前空間[2]の第3パート名");
        Assert.That(u2_3.Left, Is.InstanceOf<QualifiedNameSyntax>(), "生成された名前空間[2]の第2パートSyntax");

        var u2_2 = (QualifiedNameSyntax)u2_3.Left;

        Assert.That(u2_2.Right, Is.Not.InstanceOf<QualifiedNameSyntax>(), "生成された名前空間[2]の第2パートSyntax");
        Assert.That(u2_2.Right.ToString(), Is.EqualTo("Collections"), "生成された名前空間[2]の第2パート名");
        Assert.That(u2_2.Left, Is.Not.InstanceOf<QualifiedNameSyntax>(), "生成された名前空間[2]の第1パートSyntax");
        Assert.That(u2_2.Left.ToString(), Is.EqualTo("System"), "生成された名前空間[2]の第1パート名");
    }
}
```

`ToUsingDirective`はヘルパクラスに定義した。

```csharp
public static class SyntaxGeneratorHelper {
    // (snip)

    public static UsingDirectiveSyntax ToUsingDirective(string inUsing) {
        return 
            SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(inUsing).WithLeadingTrivia(SyntaxFactory.Space))
            .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
        ;
    }
}
```

`Roslyn`公式Wikiの[Getting Started C# Syntax Analysis](https://github.com/dotnet/roslyn/wiki/Getting-Started-C%23-Syntax-Analysis)では、1パートずつ`QualifiedNameSyntax`を組み立てる方法が提示されているが、ドット繋ぎが多いとクソめんどくさいことこの上ないため、クラス定義の時同様、文字列からパーズするのが楽だと思われる（そこまで高コストでもなさそう）。

## 生成クラスのビルドと実行

### クラスの生成まで

```csharp
public class _スタブクラス生成に関するテスト {
    // (snip)

    [Test]
    public void _生成したクラスのビルド_and_実行() {
        var ns = (NamespaceDeclarationSyntax)syntaxRoot.Members[0];
        var intf = (TypeDeclarationSyntax)ns.Members[0];
        var cls = intf.ToClassDeclaration("Impl");

        cls = cls.AddMembers(
            intf.Members.CollectMethod()
                .Select(SyntaxGeneratorHelper.ToMethodStub)
                .ToArray()
        );

        var usings = new[] {
            "System.Collections.Generic"
        };

        var newUnit = 
            SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(cls.AsMemberDecls()))
            .WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList())
        ;

        // TestContext.Progress.WriteLine(newUnit.ToFullString());
}
```

最終行のコメントを外すと、テスト出力で構築されたクラスのコードが確認できる。

`CollectMethod`、および`ToSyntaxList`メソッドは拡張クラスに定義し、メソッドチェインで書けるようにした。

```csharp
public static class SyntaxGeneratorExtension {
    //  (snip)
    public static IEnumerable<MethodDeclarationSyntax> CollectMethod<TSyntax>(this IEnumerable<TSyntax> inSyntaxes) 
        where TSyntax: CSharpSyntaxNode 
    {
        return inSyntaxes.OfType<MethodDeclarationSyntax>();
    }

    public static SyntaxList<TSyntax> ToSyntaxList<TSyntax>(this IEnumerable<TSyntax> inSyntaxes) 
        where TSyntax: CSharpSyntaxNode
    {
        return new SyntaxList<TSyntax>(inSyntaxes);
    }
}
```

### 生成クラスのビルドまで

```csharp
public class _スタブクラス生成に関するテスト {
    // (snip)

    [Test]
    public void _生成したクラスのビルド_and_実行() {
        // (snip)

        using(var stream = new MemoryStream()) {
            var dotnetCoreDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();

            var opts = new CSharpCompilationOptions(
                outputKind: OutputKind.DynamicallyLinkedLibrary
            );

            var compiler = CSharpCompilation.Create("autoGen", 
                syntaxTrees: new[] { SyntaxFactory.SyntaxTree(newUnit) },
                references: new[] {
                    AssemblyMetadata.CreateFromFile(typeof(object).Assembly.Location).GetReference(),
                    MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "netstandard.dll")),
                    MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "System.Runtime.dll")),
                AssemblyMetadata.CreateFromFile(typeof(IColorDao).Assembly.Location).GetReference(),
                },
                options: opts
            );

            var emitResult = compiler.Emit(stream);
                
            Assert.That(emitResult.Success, Is.True, "コンパイル結果");
        }
    }
}
```

コード内でのコンパイラの起動は、[ここ](https://josephwoodward.co.uk/2016/12/in-memory-c-sharp-compilation-using-roslyn)とか、[ここ](https://github.com/ufcpp-live/UfcppLiveAgenda/issues/5)を参考にした。

ビルドする上で必要となるため、インターフェースおよび戻り値の実定義を用意した。

```csharp
// (snip)

namespace GenSyntaxTest {
    // (snip)
    public struct ColorData {

    }

    public interface IColorDao {
        IEnumerable<ColorData> ListAll();
    }
}
```

ビルドに失敗した場合、`Emit`メソッドの戻り値である`EmitResult`の`Diagnostics`プロパティをにコンパイルエラーの内容が記録されている。

### 生成クラスの実行まで

```csharp
public class _スタブクラス生成に関するテスト {
    // (snip)

    [Test]
    public void _生成したクラスのビルド_and_実行() {
        // (snip)

        using(var stream = new MemoryStream()) {
            // (snip)
            Assert.That(stream.Length, Is.GreaterThan(0), "生成したバイナリサイズ");

            stream.Position = 0;
            var buf = new byte[stream.Length];
            stream.Read(buf, 0, buf.Length);

            var asm = Assembly.Load(buf);

            Assert.That(asm.GetTypes().Length, Is.EqualTo(1));
            Assert.That((asm.GetTypes()[0]).FullName, Is.EqualTo("GenSyntaxTest.ColorDaoImpl"), "生成された型名"); 

            var instance = (IColorDao)asm.CreateInstance("GenSyntaxTest.ColorDaoImpl");

            Assert.IsNull(instance.ListAll(), "スタブ関数へのアクセス");
        }
    }
}
```


ビルドにより生成したバイナリを`Assembly`として展開、リフレクションでインスタンス化、実行してるだけ。
テスト上、インターフェースの型が既知のため、静的キャストでお茶を濁す。

# 余談(2020/5/3時点)

`Roslyn`いぢりをやろうと決心した翌日に、[Source Generators](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/)が発表される。

当初、コンパイル時コード生成を[CodeGeneration.Roslyn](https://github.com/AArnott/CodeGeneration.Roslyn)に頼るつもりであったけど、突然の電撃発表に心揺らぐ。

ただし、まだファーストプレビューゆえの`Visual Studio`でとりあえず動くようにしましたレベルらしく、また生成結果も文字列でしか受け付けない模様なので、もう少し様子見するつもり。

構文木から、ソースコードへの変換も、Trivia（インデントや改行）を適切に付与しておけば、`ToFullString()メソッドで行えるため、100%無駄にはならなさそう（と信じたい）。