Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9749

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

$
0
0

前書き

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

環境

Roslynが作るコードツリーを覗くと同じ

プロジェクト準備

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

% dotnet nunit -o StubClassGenTest

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

% dotnet add StubClassGenTest package Microsoft.CodeAnalysis.CSharp

作成

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

usingNUnit.Framework;usingSystem.IO;usingSystem.Linq;usingSystem.Collections.Generic;usingSystem.Reflection;usingMicrosoft.CodeAnalysis;usingMicrosoft.CodeAnalysis.CSharp;usingMicrosoft.CodeAnalysis.CSharp.Syntax;namespaceGenSyntaxTest{publicclass_スタブクラス生成に関するテスト{privatestaticreadonlyCompilationUnitSyntaxsyntaxRoot;static_スタブクラス生成に関するテスト(){varsourceText=@"
            namespace GenSyntaxTest {
                public interface IColorDao {
                    IEnumerable<ColorData> ListAll();
                }
            }
            ";syntaxRoot=CSharpSyntaxTree.ParseText(sourceText).GetCompilationUnitRoot();}}}

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

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

publicstaticclassSyntaxGeneratorExtension{// (snip)}publicstaticclassSyntaxGeneratorHelper{// (snip)}

クラスパート生成

publicclass_スタブクラス生成に関するテスト{// (snip)[Test]publicvoid_クラス定義パートの生成(){varns=(NamespaceDeclarationSyntax)syntaxRoot.Members[0];varintf=(TypeDeclarationSyntax)ns.Members[0];Assert.AreEqual(SyntaxKind.InterfaceDeclaration,intf.Kind(),"元の型のSyntax");varcls=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メソッドは、拡張クラスのメソッドとして用意

publicstaticclassSyntaxGeneratorExtension{publicstaticClassDeclarationSyntaxToClassDeclaration(thisTypeDeclarationSyntaxinInterfaceSyntax,stringinSuffix){varintfName=inInterfaceSyntax.Identifier.Text;varclsTree=SyntaxFactory.ParseCompilationUnit($"public class {intfName.Substring(1)}{inSuffix}: {intfName}"+"{}");return(ClassDeclarationSyntax)clsTree.Members[0];}}

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

メソッドパート生成

publicclass_スタブクラス生成に関するテスト{// (snip)[Test]publicvoid_引数を持たないメソッドの生成(){varns=(NamespaceDeclarationSyntax)syntaxRoot.Members[0];varintf=(TypeDeclarationSyntax)ns.Members[0];Assert.That(intf.Members.Count,Is.EqualTo(1),"用意されたインターフェースのメソッド数");Assert.That(intf.Members[0],Is.InstanceOf<MethodDeclarationSyntax>(),"用意されたインターフェースのメソッドSyntax");varmeth=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メソッドはヘルパークラスに定義した

publicstaticclassSyntaxGeneratorHelper{publicstaticMethodDeclarationSyntaxToMethodStub(MethodDeclarationSyntaxinIntfMember){varreturnType=inIntfMember.ReturnType.WithLeadingTrivia(SyntaxFactory.Space);returninIntfMember.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への変換は、メソッドチェインで書けるよう拡張クラスに定義した

publicstaticclassSyntaxGeneratorExtension{// (snip)publicstaticSyntaxTokenListAsTokens(thisSyntaxTokeninSyntax){returnnewSyntaxTokenList(inSyntax);}}

名前空間宣言の生成

publicclass_スタブクラス生成に関するテスト{// (snip)[Test]publicvoid_名前空間宣言の生成(){varu1=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]");varu2=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]");varu2_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");varu2_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はヘルパクラスに定義した。

publicstaticclassSyntaxGeneratorHelper{// (snip)publicstaticUsingDirectiveSyntaxToUsingDirective(stringinUsing){returnSyntaxFactory.UsingDirective(SyntaxFactory.ParseName(inUsing).WithLeadingTrivia(SyntaxFactory.Space)).WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);}}

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

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

クラスの生成まで

publicclass_スタブクラス生成に関するテスト{// (snip)[Test]publicvoid_生成したクラスのビルド_and_実行(){varns=(NamespaceDeclarationSyntax)syntaxRoot.Members[0];varintf=(TypeDeclarationSyntax)ns.Members[0];varcls=intf.ToClassDeclaration("Impl");cls=cls.AddMembers(intf.Members.CollectMethod().Select(SyntaxGeneratorHelper.ToMethodStub).ToArray());varusings=new[]{"System.Collections.Generic"};varnewUnit=SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(cls.AsMemberDecls())).WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList());// TestContext.Progress.WriteLine(newUnit.ToFullString());}

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

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

publicstaticclassSyntaxGeneratorExtension{//  (snip)publicstaticIEnumerable<MethodDeclarationSyntax>CollectMethod<TSyntax>(thisIEnumerable<TSyntax>inSyntaxes)whereTSyntax:CSharpSyntaxNode{returninSyntaxes.OfType<MethodDeclarationSyntax>();}publicstaticSyntaxList<TSyntax>ToSyntaxList<TSyntax>(thisIEnumerable<TSyntax>inSyntaxes)whereTSyntax:CSharpSyntaxNode{returnnewSyntaxList<TSyntax>(inSyntaxes);}}

生成クラスのビルドまで

publicclass_スタブクラス生成に関するテスト{// (snip)[Test]publicvoid_生成したクラスのビルド_and_実行(){// (snip)using(varstream=newMemoryStream()){vardotnetCoreDirectory=System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();varopts=newCSharpCompilationOptions(outputKind:OutputKind.DynamicallyLinkedLibrary);varcompiler=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);varemitResult=compiler.Emit(stream);Assert.That(emitResult.Success,Is.True,"コンパイル結果");}}}

コード内でのコンパイラの起動は、こことか、ここを参考にした。

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

// (snip)namespaceGenSyntaxTest{// (snip)publicstructColorData{}publicinterfaceIColorDao{IEnumerable<ColorData>ListAll();}}

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

生成クラスの実行まで

publicclass_スタブクラス生成に関するテスト{// (snip)[Test]publicvoid_生成したクラスのビルド_and_実行(){// (snip)using(varstream=newMemoryStream()){// (snip)Assert.That(stream.Length,Is.GreaterThan(0),"生成したバイナリサイズ");stream.Position=0;varbuf=newbyte[stream.Length];stream.Read(buf,0,buf.Length);varasm=Assembly.Load(buf);Assert.That(asm.GetTypes().Length,Is.EqualTo(1));Assert.That((asm.GetTypes()[0]).FullName,Is.EqualTo("GenSyntaxTest.ColorDaoImpl"),"生成された型名");varinstance=(IColorDao)asm.CreateInstance("GenSyntaxTest.ColorDaoImpl");Assert.IsNull(instance.ListAll(),"スタブ関数へのアクセス");}}}

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

余談(2020/5/3時点)

Roslynいぢりをやろうと決心した翌日に、Source Generatorsが発表される。

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

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

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


Viewing all articles
Browse latest Browse all 9749

Trending Articles