Kotlin Compiler Plugin (kotlin-allopen) を追いかける
これは Kotlin AdventCalendar 2016 の 24 日目の記事です。メリークリスマスイブ!時間的にギリギリアウトでした・・・。
さて、少し前に Kotlin 1.0.6 RC が出ましたが、そのタイミングで kotlin-allopen
と kotlin-noarg
という 2 つのコンパイラープラグインが発表されました。
それぞれ、以下の働きをするコンパイラープラグインとなっています。
kotlin-allopen
任意のアノテーション (ユーザーにて指定できます) が付いているクラスのメンバーに、暗黙的にopen
修飾子を付与します。 Spring 等のフレームワークや Mockit などのライブラリに有益な物となっています。普段 Web アプリ開発に Kotlin + Spring Framework を使っているのでとても助かりそうです!kotlin-noarg
任意のアノテーション (ユーザーにて指定できます) が付いているクラスに、暗黙的に引数ゼロのコンストラクターを生成します。 主に JPA 対応として作成されたようです。
コンパイラープラグインの使い方は上のリンクを参照していただくとして、今回は kotlin-allopen
プラグインを題材に、コードにコメントを追加する形でコンパイラープラグインの作りについて追いかけてみたいと思います。リンクは、タグ v1.1-M04
です。 (v1.0.6 RC はタグが切られてないようなので、2016/12/24 時点で一番新しいタグをベースにしました。)
全体の仕組み
kotlin-allopen
プラグインに関するコードは以下のディレクトリにあります。kotlin-allopen
の本体は共通化されており、Gradle と Maven 向けのプラグイン実装が用意されています。
- plugins/allopen/allopen-cli
kotlin-allopen
の本体です。 - libraries/tools/kotlin-allopen
kotlin-allopen
の Gradle プラグインです。 - libraries/tools/kotlin-maven-allopen
kotlin-allopen
の Maven プラグインです。
今回はプラグイン本体のみ追いかけます。
プラグイン本体
ここから、プラグイン本体のコードを読んでいきます。コードは 2 ファイルのみで、大きくないためすぐ読めます。
- AllOpenPlugin.kt
プラグインのオプション引数を処理するクラスと、プラグインを登録するクラスが実装されています。 - AllOpenDeclarationAttributeAltererExtension.kt
プラグインの処理をおこなうクラスが実装されています。
AllOpenPlugin.kt
AllOpenPlugin.kt には、コンパイラープラグインに必要な 2 つのインターフェースを実装したクラスが定義されています。 コンパイラープラグインに必要なインターフェースは以下の 2 つです。
CommandLineProcessor
インターフェースComponentRegistrar
インターフェース
/* * Copyright 2010-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jetbrains.kotlin.allopen import com.intellij.mock.MockProject import org.jetbrains.kotlin.compiler.plugin.CliOption import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.CompilerConfigurationKey import org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension /** * コンパイラープラグインで扱う設定のキーを定義するオブジェクトです。 */ object AllOpenConfigurationKeys { /** * kotlin-allopen の対象とするアノテーションのリストを設定するキーです。 */ val ANNOTATION: CompilerConfigurationKey<List<String>> = CompilerConfigurationKey.create("annotation qualified name") } /** * kotlin-allopen プラグインに指定されたオプション引数を処理するクラスです。 */ class AllOpenCommandLineProcessor : CommandLineProcessor { /** * AllOpenCommandLineProcessor のコンパニオンオブジェクトです。 */ companion object { /** * オプション引数 annotation の定義です。 * * - オプション引数名は annotation です。 * - オプション引数には、プラグイン処理の対象とするアノテーションの完全修飾名を指定します。 * - オプション引数は複数指定可能です。 * * コマンドラインコンパイラーを使用する場合、以下の引数を指定してコンパイラープラグインを有効にします。 * * `-P <jar ファイルのパス>:<プラグイン ID>:<オプション引数名>=<アノテーションの完全修飾名>[, 複数指定する場合はカンマで区切る]` */ val ANNOTATION_OPTION = CliOption("annotation", "<fqname>", "Annotation qualified names", required = false, allowMultipleOccurrences = true) /** * コマンドラインコンパイラーで指定するプラグイン ID を定義します。 */ val PLUGIN_ID = "org.jetbrains.kotlin.allopen" } /** * CommandLineProcessor インターフェースで定義されている変数です。 * コンパニオンオブジェクトで定義したプラグイン ID を返します。 */ override val pluginId = PLUGIN_ID /** * CommandLineProcessor インターフェースで定義されている変数です。 * オプション引数のリストを返します。 */ override val pluginOptions = listOf(ANNOTATION_OPTION) /** * CommandLineProcessor インターフェースで定義されているメソッドです。 * 指定されたオプション引数を処理します。 * * オプション引数に指定した数だけ繰り返し実行されます。 * * @param option オプション引数を判別するための CliOption です。 * @param value オプション引数に指定された値です。 * @param configuration コンパイラーの設定等を保持するオブジェクトです。 */ override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) = when (option) { /* オプション引数 annotation が指定された場合の処理です。 */ ANNOTATION_OPTION -> { /* これまでに設定されたオプション引数 annotation のリストを、CompilerConfiguration から取得します。 * 設定を取得するキーは、AllOpenConfigurationKeys オブジェクトで定義しています。 */ val paths = configuration.getList(AllOpenConfigurationKeys.ANNOTATION).toMutableList() /* 今回の値をリストに追加します。 */ paths.add(value) /* CompilerConfiguration の値を上書きします。 */ configuration.put(AllOpenConfigurationKeys.ANNOTATION, paths) } /* kotlin-allopen プラグインが想定していないオプションが指定された場合は例外をスローします。 */ else -> throw CliOptionProcessingException("Unknown option: ${option.name}") } } /** * kotlin-allopen プラグインの処理を登録します。 */ class AllOpenComponentRegistrar : ComponentRegistrar { /** * ComponentRegistrar インターフェースで定義されているメソッドです。 * コンパイラープラグインの処理を登録します。 * * @param project Intellij IDEA で用意しているオブジェクトのようですが、あまり情報がないため不明です・・・。 * @param configuration コンパイラーの設定等を保持するオブジェクトです。 * AllOpenCommandLineProcessor で処理済みのものです。 */ override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { /* CompilerConfiguration から、kotlin-allopen 処理の対象とするアノテーションのリストを取得します。 * 取得できない場合はプラグイン処理を登録しません。 */ val annotations = configuration.get(AllOpenConfigurationKeys.ANNOTATION) ?: return if (annotations.isEmpty()) return /* kotlin-allopen 処理を登録します。 */ DeclarationAttributeAltererExtension.registerExtension(project, CliAllOpenDeclarationAttributeAltererExtension(annotations)) } }
AllOpenDeclarationAttributeAltererExtension.kt
AllOpenDeclarationAttributeAltererExtension.kt では、kotlin-allopen の処理をおこなうクラスを定義しています。
コンパイラーの拡張ポイント (という名前でいいのか?) は DeclarationAttributeAltererExtension
というインターフェースです。
コンパイラーのフロントエンドの org.jetbrains.kotlin.extensions
パッケージに定義されているインターフェース / クラスは数少ないのですが、まだ十分に用意されていないのかそれともこれで十分なのか、理解が足りず分かりません・・・。
/* * Copyright 2010-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jetbrains.kotlin.allopen import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.extensions.AnnotationBasedExtension import org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtModifierListOwner import org.jetbrains.kotlin.resolve.BindingContext /** * コンストラクターで指定されたアノテーションに対してプラグイン処理をおこなうクラスです。 * * @property allOpenAnnotationFqNames プラグイン処理をおこなうアノテーションのリストです。 */ class CliAllOpenDeclarationAttributeAltererExtension( private val allOpenAnnotationFqNames: List<String> ) : AbstractAllOpenDeclarationAttributeAltererExtension() { /** * AnnotationBasedExtension インターフェースで定義されているメソッドです。 * プラグイン処理をするアノテーションのリストを返します。 */ override fun getAnnotationFqNames(modifierListOwner: KtModifierListOwner?) = allOpenAnnotationFqNames } /** * プラグイン処理をおこなうクラスです。 * * 2 つのインターフェースを実装しています。 * * - DeclarationAttributeAltererExtension * - AnnotationBasedExtension */ abstract class AbstractAllOpenDeclarationAttributeAltererExtension : DeclarationAttributeAltererExtension, AnnotationBasedExtension { /** * AbstractAllOpenDeclarationAttributeAltererExtension のコンパニオンオブジェクトです。 */ companion object { /** * テスト用のアノテーションリストを定義します。 */ val ANNOTATIONS_FOR_TESTS = listOf("AllOpen", "AllOpen2", "test.AllOpen") } /** * DeclarationAttributeAltererExtension インターフェースで定義されているメソッドです。 * * Modality を変更するプラグイン処理を実装します。(たぶん・・・) * * @property modifierListOwner * @property declaration * @property containingDeclaration * @property currentModality * @property bindingContext */ override fun refineDeclarationModality( modifierListOwner: KtModifierListOwner, declaration: DeclarationDescriptor?, containingDeclaration: DeclarationDescriptor?, currentModality: Modality, bindingContext: BindingContext ): Modality? { /* 現在の Modality が FINAI 以外の場合はプラグインの処理をしません。 * Modality は以下のものがあります。 * Modality.FINAL * Modality.SEALED * Modality.OPEN * Modality.ABSTRACT */ if (currentModality != Modality.FINAL) { return null } /* ここは理解していません。コンパイラーを追いかけないと分からなそうです・・・。 */ val descriptor = declaration as? ClassDescriptor ?: containingDeclaration ?: return null /* コンストラクターで指定されたアノテーションが指定されている場合 */ if (descriptor.hasSpecialAnnotation(modifierListOwner)) { /* final 修飾子が付いている場合は final のまま、それ以外の場合は open に変更します。 */ return if (modifierListOwner.hasModifier(KtTokens.FINAL_KEYWORD)) Modality.FINAL // Explicit final else Modality.OPEN } /* null を返すと、変更なし。 */ return null } }
まとめ
ソースにコメントを付けるという簡易的な形で、コンパイラープラグインの処理について追いかけてみました。
このプラグインの他にも、plugins ディレクトリにいくつか実装があります。またプラグインの他にも、Intellij IDEA 用のプラグインのようなコードもあるので、コンパイラープラグインに興味のある方は追いかけてみてください。