kapt上でのコード生成をテストするkompile-testingのご紹介

@hotchemiです。みなさんは、やっていますか?

今回は、業務とは関係ない趣味で作ったkompile-testingの話をします。

概要

Googleが開発しているcompile-testingというツールがあります。これは、Javaコンパイル及びAnnotation Processingというコンパイル時にAnnotationのメタ情報を元にコードやファイル生成をする仕組みをテストする為のライブラリで、Daggerなどポピュラーなライブラリのテストに利用されています。以下の用に書く事でコンパイル後の状態をテストする事ができます。

 Compilation compilation =
     javac()
         .withProcessors(new MyAnnotationProcessor())
         .compile(JavaFileObjects.forResource("HelloWorld.java"));
 assertThat(compilation).succeeded();
 assertThat(compilation)
     .generatedSourceFile("GeneratedHelloWorld")
     .hasSourceEquivalentTo(JavaFileObjects.forResource("GeneratedHelloWorld.java"));

このライブラリはGoogleJava Core Libraries Teamが開発しているだけあって素晴らしいのですが、昨今の(特にAndroid界の)時代の趨勢としてはKotlinがその勢いを増してきています。

そうなれば当然Kotlinのコードを入力としてKotlinのコードを出力したいという要望が出てきますが、残念ながらcompile-testingはこの問題に対応していません。理由は単純で、Kotlinはkaptという独自のシステムをjavacとは別のタイミングで動作させる為です。

そこでこの問題に対応したものが、kompile-testingです。compile-testingとほぼ同じAPIインタフェースでKotlinのコード生成をテストできます。やりましたね。

kotlinc()
    .withProcessors(YourProcessor())
    .addKotlin("input.kt", """
        import kompile.testing.TestAnnotation

        @TestAnnotation
        class TestClass
        """.trimIndent())
        .compile()
        .succeededWithoutWarnings()
        .generatedFile("generatedKtFile.kt")
        .hasSourceEquivalentTo("class GeneratedKtFile")

Under the hood

これだけだと味気無いので、どういう仕組みで実現しているか簡単に解説します。Compiler.ktの処理をざっくり分類すると、以下の様な感じになります。

  • withProcessorsでプロセッサのclass情報を保持しておく
  • addKotlinで入力値をfileとして書き出す
  • compileでクラスパスとkaptのオプションを設定してあげてK2JVMCompiler#execを呼んで実際にコンパイラを起動する。コンパイラのexit codeと出力を戻り値として返してあげる

要するに実際にKotlin Compilerをプログラマブルに実行させてしまおう、という事ですね。実際そんなに難しい事はしていないのですが、compileの処理はもう少し説明をしてみます。また、前提としてKotlin compilerやkaptのオプションに関する詳細は以下のドキュメントに書かれています。

Compiler#compile

  • -dオプションでclassの出力先を指定しています。
  • -no-stdlibはクラスパスにstdlibが含まれていないとwarningが出るのをsuppressする為のオプションです。
  • -classpathでクラスパスを指定しています。生成ロジックに関してはこちらにまとまっています。classLoaderから引っ張り出してきてaarの場合はjarだけ抽出してあげています。力技ですね。
  • この辺でFileに書き出したinputファイルをコンパイル対象として指定しています。
  • annotationProcessorArgs()(定義)でkaptの実行及び関連オプションについて指定しています。公式ドキュメントにありますが、kaptはcompiler pluginという機構の一種として指定されておりXpluginオプションでjarを指定する事で実行できます。kaptのjarはkompile-testingをgradle dependenciesとして設定すると自動でクラスパスに追加されるようになっているのでそこから持ってきています。後のオプションは公式ドキュメントのコピペですが、apclasspathで指定するprocessorのpathはjarとして書き出さないといけない為writeServicesJarで保持しているProcessorのクラス情報をjarに書き出しています。力技ですね。
  • この辺で、kapt自体に渡すオプションの設定をしています。kapt.kotlin.generatedというオプションで指定されたディレクトリの中にKotlinファイルを書き出すとコンパイル対象になる為、Processor側でこのオプションを見ている事が殆どなのでテスト用に追加しています。
  • 最後に、今まで生成したオプションをK2JVMCompiler#execに渡します。K2JVMCompilerにはerrorを受け取るstreamを指定できる為コンパイル時に吐かれるerrorやwaningを受け取る事ができます。

最後に

現在このライブラリのversionは0.1.1となっており最低限の機能を実装した段階です。今後は主にパフォーマンス面の問題とcompile-testingに存在するAPIを補完する方向で改善を進めていく予定となっています。

また、リリースした所早速arrow-ktの開発チームに利用してもらっている様で、嬉しいですね。CompilerのAPIというのは業務では触れる事が少ない分野ですが、触れてみると沢山の楽しい発見がある為まとまった時間を取ってトライしてみると良い事があるかもしれません。

現場からは以上です。