Kotlin Analysis APIを触ってみる
この記事はMoney Forward Kansai Advent Calendar 2024の2日目の記事です。
こんにちは、株式会社マネーフォワード大阪開発拠点でバックエンドエンジニアをしているTaskです。
Kotlinに新しく導入されたAnalysis APIを紹介したいと思います。
この記事では、以下の内容を扱います。
- Kotlin Analysis APIの簡単な紹介
- 簡単なIntelliJプラグインを使ったKotlin Analysis APIの挙動確認
以下の内容を扱いません。
- Kotlin Analysis APIの詳細な実装の紹介
- 既存のKotlinコンパイラの説明
また、本記事にはPSI Elementや構文解析に関係するワードが含まれています。
それらの知識があると望ましいですが、なくても内容が伝わるように努めています。
Kotlin Analysis APIとは
Kotlin Analysis APIとは、2024年6月のKotlin Festで発表され、同年の夏頃に公開されたAPIです。
Kotlin Analysis APIでは、Kotlinを構文解析した結果であるPSI Element(抽象構文木のノード)を経由して、Kotlinコンパイラによるコンパイルで得られた情報を取得できます。
ここでの情報とは、変数の型や、特定のスコープにおいてアクセス可能な変数群といった情報が含まれます。
これにより、既存のIntelliJ IDEAのプラグインを改善したり、Kotlinコンパイラを利用する静的解析ツールの性能を強化したりできます。
触ってみる
このAPIは現在IntelliJ Coreに依存しているため[1]、簡単なIntelliJプラグインを実装して挙動を確認してみます。
Kotlin Analysis APIが本当にコンパイル情報を取得できるかを確認するために、Kotlin 2.0で追加されたローカル変数のダウンキャスト情報の保持を題材にします。
class Cat {
fun purr() {
println("Purr purr")
}
}
fun petAnimal(animal: Any) {
val isCat = animal is Cat
if (isCat) {
// In Kotlin 2.0.0, the compiler can access
// information about isCat, so it knows that
// animal was smart-cast to the type Cat.
// Therefore, the purr() function can be called.
// In Kotlin 1.9.20, the compiler doesn't know
// about the smart cast, so calling the purr()
// function triggers an error.
animal.purr()
}
}
fun main() {
val kitty = Cat()
petAnimal(kitty)
// Purr purr
}
コメントにもあるように、以前のKotlinではスマートキャストを行う際には、if文の条件式の中で型チェックを行う必要がありました。
fun petAnimal(animal: Any) {
if (animal is Cat) {
animal.purr()
}
}
しかし、Kotlin 2.0では、コンパイラが isCat
に関する情報にアクセスできるようになったことから、コンパイラはisCat
がtrueな状況下でanimal
をCat
にキャストできます。
1. Plugin DevKitをインストールする
IntelliJプラグイン開発を行うには、まずお手持ちのIDEにPlugin DevKitをインストールする必要があります。
昔はIntelliJ IDEAに同梱されていたのですが、IntelliJ IDEA 2023.3からはプラグインとして提供される形になりました。
2. Projectを作成する
プラグインをインストールしたら、"File > New > Project..."から新規プロジェクトを作成してください。
メニューから"IDE Plugin"を選択できるようになるので、必要な情報を埋めて作成します。
3. 依存を追加、更新する
IntelliJ IDEAやPlugin DevKitのバージョンによっては、依存のバージョンが古くKotlin 2.0を扱えない可能性があります。
僕の場合は、以下のように更新しました。
plugins {
id("java")
// old: id("org.jetbrains.kotlin.jvm") version "1.9.25"
id("org.jetbrains.kotlin.jvm") version "2.0.21"
// old: id("org.jetbrains.intellij") version "1.17.4"
id("org.jetbrains.intellij.platform") version "2.1.0"
}
/*
old:
intellij {
version.set("2023.2.8")
type.set("IC") // Target IDE Platform
plugins.set(listOf(/* Plugin Dependencies */))
}
*/
dependencies {
intellijPlatform {
intellijIdeaCommunity("2024.3")
instrumentationTools()
bundledPlugin("org.jetbrains.kotlin")
}
}
また、src/resources/META-INF/plugin.xml
に以下を追記します。
<idea-plugin>
...
<depends>com.intellij.modules.platform</depends>
<depends>org.jetbrains.kotlin</depends>
</idea-plugin>
4. コードを書く
次に、コード解析を行うコードを実装していきます。
今回は、題材のanimal
変数の型を表示するプラグインを作成します。
IntelliJプラグインでは、AnAction#actionPerformed
がプラグイン実行のエントリーポイントとなるため、AnAction
を継承します。
Kotlin Analysis APIでは、変数の型を取得するexpressionType
という拡張プロパティが提供されているため、それを利用します[2]。
class SampleAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
// 現在エディタで開いているファイルを取得する
val currentOpeningFile = FileEditorManager.getInstance(project).selectedFiles[0]
// ファイルからKotlinファイルの構文情報を取得する
// この時点では、コンパイラの情報を持っていない
val ktFile = PsiManager.getInstance(project).findFile(currentOpeningFile) as KtFile
ApplicationManager.getApplication().executeOnPooledThread {
ApplicationManager.getApplication().runReadAction { // Analysis APIはReadスレッドからしか使えない
// Kotlinファイルから、`animal.purr()`の`animal`変数を取得する
// KtExpressionはPSI Elementの一種
val animal = ktFile.children[4].children[1].children[1].children[1].children[0].children[0].children[0] as KtExpression
// Kotlin Analysis APIを利用できるスコープ
analyze(animal) {
// Kotlin Analysis APIを利用し、`animal`の型を取得する
val animalTypeGottenByAnalysisApi = animal.expressionType
Notifications.Bus.notify(
Notification(
"Kotlin Analysis API sample",
"Type of animal is $animalTypeGottenByAnalysisApi",
NotificationType.INFORMATION,
),
project,
)
}
}
}
}
}
最後に、SampleAction
を実行するためのユーザの操作を定義します。
今回はTools
メニューバーにボタンを追加し、そこから実行できるようにします。
<idea-plugin>
...
<actions>
<action class="io.github.t45k.kotlin_analysis_api_trial.SampleAction" text="Sample">
<add-to-group group-id="ToolsMenu" anchor="first"/>
</action>
</actions>
</idea-plugin>
ここまでで、K1コンパイラに対応したプラグインが完成しました。
コードは以下から参照できます。
5. 実行してみる(K1コンパイラ)
それでは、K1コンパイラ上で実行してみます。
./gradlew runIde
でサンドボックス環境を起動できます。
サンドボックスのIntelliJ IDEAが起動したら、そこにサンプルコードを書いて"Tools > Sample"からプラグインを実行します。
実行結果は以下の通りです。
右下のNotificationに表示されている通り、animal
はAny型として解決されています。
(余談ですが、サンドボックス環境のK1コンパイラもanimal
をCat
型だと解決できないので、エディタ上でもpurr
メソッド呼び出しに対してエラーが起きています)
6. コードを書く(K2コンパイラ対応)
次に、このプラグインをK2コンパイラ環境で実行するために、いくつかの変更を加えます。
まず、プラグインの設定ファイルで、このプラグインがK2コンパイラをサポートしていることを宣言します。
<idea-plugin>
...
<extensions defaultExtensionNs="org.jetbrains.kotlin">
<supportsKotlinPluginMode supportsK2="true"/>
</extensions>
</idea-plugin>
そして、./gradlew runIde
でサンドボックス環境がK2モードで立ち上がるようにします。
tasks {
...
runIde {
jvmArgs = listOf("-Didea.kotlin.plugin.use.k2=true")
}
}
これで、K2コンパイラに対応したプラグインが完成しました。
コードは以下から参照できます。
7. 実行する(K2コンパイラ)
実行結果は以下の通りです。
右下のNotificationの通り、animal
をCat
型として解決できています。
これにより、Kotlin Analysis APIを用いることでコンパイラが持つ型解決の情報を、対応するPSI Elementから取得できることが確認できました。
加えて、K2モードを有効化した状態だと、Kotlin 2.0でのコンパイラの改善も活用できることが分かりました。
ドキュメントを読んでみる
ここまでは使用例の紹介でしたが、最後に軽くドキュメントを紹介して、裏側の仕組みや実装時の注意点について確認していきたいと思います。
Kotlin Analysis APIに関する拡張関数
The Analysis API is implemented on top of the Kotlin PSI, mostly as a set of extension functions and properties, providing access to semantic information.
Analysis APIは既存のKtElement
の拡張関数の形で提供されます。
実際に、今回使ったexpressionType
はKtExpression
の拡張プロパティの形で宣言されています。
Kotlin Analysis APIを利用できるコンテキスト
KaSession is the entry point for interacting with the Analysis API. It provides access to various components and utilities needed for analyzing code in Kotlin.
これらのAPIは、KaSession
をエントリーポイントとして利用できます。
analyze
メソッドはKaSession.() -> R
を引数に持ち、analyze(element) {}
のラムダ内でのみAnalysis APIが提供している拡張関数を利用できます[3]。
// OK
val ktElement = ...
analyze(ktElement) {
ktElement.expressionType
}
// NG
ktElement.expressionType
そのため、analyze内の処理をメソッドに切り出す際には、KaSession
の拡張関数の形で実装する必要があります。
val ktElement = ...
analyze(ktElement) {
check(ktElement)
}
fun KaSession.check(ktElement: KtElement) { ... }
まとめ
今回は、2024年の夏に公開されたKotlin Analysis APIを紹介し、簡単なプラグインを通してその有用性を紹介しました。
このAPIは現在活発に開発中であり、今後より便利なAPIが追加されたり、IntelliJ以外のプラットフォームでも利用できるようになることが期待されます。
特に、既存の静的解析ツール(detektやSonarQube)に組み込むことで、より詳細かつ的確な解析結果を得られるようになると思うので、楽しみにしています。
Discussion