🐷

Kotlin Compiler Pluginのテストについて

2021/03/31に公開

はじめに

本記事では、Kotlin Compiler Pluginのテストについて紹介します。

Kotlin Compiler Pluginとは?

Kotlin Compiler Pluginとは以下の特徴があります。

  • バイトコードを上書きできる
  • コード記述の自動化ができる
  • Kaptで使われている

Kotlin Compiler Pluginは、まだ安定版の機能ではなく、公式ドキュメントもありません。しかし、Kotlinのロードマップを確認したところ、Kotlin 1.5のターゲットではなさそうですが、Stable Compiler Plugin APIに向けて開発中のようです。

roadmap

Kotlin Compiler Pluginを使ったライブラリの紹介

Kotlin Compiler Pluginの特徴を簡単に説明しましたが、ここからNoCopy Compiler Pluginライブラリを用いてKotlin Compiler Pluginの利用例を説明します。このライブラリでは、@NoCopyアノテーションをセットしたデータクラスからcopy()関数を削除したクラスを生成することで、copy()関数を利用できないようにしています。

まずは次のUserデータクラスにcopy()関数を利用することで任意のnameプロパティに変更できることを確認します。

fun main() {
    val user = User(name = "tommykw")
    println(user) // User(name=tommykw)
    val copied = user.copy(name = "hoge")
    println(copied) // User(name=hoge)
}

data class User(val name: String)

次にNoCopy Compiler Pluginの依存関係を追加した上で、Userデータクラスに@NoCopyをセットして実行した場合、copy()の箇所でコンパイルエラーになることがわかります。

fun main() {
    val user = User(name = "tommykw")
    println(user) // User(name=tommykw)
    val copied = user.copy(name = "hoge") // Unresolved reference: copy
    println(copied)
}

@NoCopy
data class User(val name: String)

ここまで、NoCopy Compiler Pluginを使ってKotlin Compiler Plugin APIの利用例について説明しました。

Kotlin Compiler Pluginのテスト

kotlin-compile-testingライブラリを利用することでKotlinで生成されたコードのテストを実施できます。引き続き、NoCopy Compiler Pluginを例にテストコードの説明をします。NoCopy Compiler Pluginでは次の通りテストを実施しています。

NoCopyPluginTests.kt
class NoCopyPluginTests {

    @Rule
    @JvmField
    var temporaryFolder: TemporaryFolder = TemporaryFolder()

    @Test
    fun `@NoCopy annotated non-data class should fail compilation`() {
        val result = compile(kotlin("NonDataClass.kt",
                """
          package dev.ahmedmourad.nocopy.compiler
          import dev.ahmedmourad.nocopy.annotations.NoCopy
          @NoCopy
          class NonDataClass(val a: Int)
          """
        ))
        assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
        assertThat(result.messages).contains("NonDataClass.kt: (3, 1): Only data classes could be annotated with @NoCopy!")
    }

    @Test
    fun `@NoCopy annotated data class should compile just fine`() {
        val result = compile(kotlin("DataClass.kt",
                """
          package dev.ahmedmourad.nocopy.compiler
          import dev.ahmedmourad.nocopy.annotations.NoCopy
          @NoCopy
          data class DataClass(val a: Int)
          """
        ))
        assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
        assertThat(result.messages).isEmpty()
    }
    
    private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation {
        return KotlinCompilation().apply {
            workingDir = temporaryFolder.root
            compilerPlugins = listOf<ComponentRegistrar>(NoCopyPlugin())
            inheritClassPath = true
            sources = sourceFiles.asList()
            verbose = false
            jvmTarget = JvmTarget.JVM_1_8.description
        }
    }

    private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result {
        return prepareCompilation(*sourceFiles).compile()
    }
}

最初のテストケースでは、クラスに対して@NoCopyをセットした場合にコンパイルエラーとなります。もう一方のテストケースでは、データクラスに@NoCopyをセットした場合にエラーにならず、コンパイルが通ります。

kotlin-compile-testingのcompile()関数は以下のステップで実行されます。

  • スタブを生成する
  • aptを実行する
  • Kotlinソースをコンパイルする
  • Javaソースをコンパイルする

また、compile()関数の戻り値はKotlinCompilation.Resultオブジェクトで、コンパイルの終了結果とコンパイル結果のメッセージなど取得できます。

KotlinCompilation.kt
inner class Result(
    val exitCode: ExitCode,
    val messages: String
) {
    val classLoader = URLClassLoader(arrayOf(outputDirectory.toURI().toURL()),
			this::class.java.classLoader)

    val outputDirectory: File get() = classesDir

    val sourcesGeneratedByAnnotationProcessor: List<File>
				= kaptSourceDir.listFilesRecursively() + kaptKotlinGeneratedDir.listFilesRecursively()

    val compiledClassAndResourceFiles: List<File> = outputDirectory.listFilesRecursively()

    val generatedStubFiles: List<File> = kaptStubsDir.listFilesRecursively()

    val generatedFiles: Collection<File>
				= sourcesGeneratedByAnnotationProcessor + compiledClassAndResourceFiles + generatedStubFiles
}

最後に

NoCopy Compiler Pluginを基に、Kotlin Compiler Pluginのテストについて紹介させてもらいました。NoCopy Compiler Pluginに限らず、他のライブラリでも、kotlin-compile-testingを利用しているライブラリが多いので、テストコードを勉強していきたいと思います。

参考記事

Discussion