サーバーサイドKotlinへ静的解析ツールを導入する方法
自分が担当しているサーバーサイドKotlinのプロジェクトに静的解析ツールを導入したので、その時の調査結果や実装方法などを共有します。
個人的には前職ではNode.jsで開発をしていたため、eslint + prettir + husky + lint-stagedのような使い心地の静的解析ツールの導入をKotlinでも実現したいと考えていました。
Kotlinにおける静的解析ツール
Kotlinの静的解析ツールとして有名なものは、ktlint、detekt、AndroidLintが挙げられます。
今回はサーバーサイドKotlinでの使用を考えていたので、AndroidLintを除いた2つを検討しました。
ktlint
ktlintはJavaScript Standard Styleとgofmtの精神に基づき、anti-bikesheddingを謳うKotlinの静的解析ツールです。
特徴としては、
- kotlinlang.orgとAndroid Kotlin Style Guideの公式コードスタイルを反映しており、細かい設定が不要な点
- フォーマッターを内蔵しているため、手動での修正が不要な点
などが挙げられます。
detekt
detektはktlintよりも後発の静的解析ツールです。
特徴としては
- コードの複雑度やコードの臭いなどをもとにリファクタリングの対象を指摘する点
- 高度なルール設定が可能な点
- @Suppressアノテーションを使用して、エラーを抑制できる点
などが挙げられます。
またdetektにはdetekt-formattingという拡張機能が提供されています。
detekt-formattingはktlintをラップしているため、detektでもktlintの解析ルールを簡単に利用できます。
選定結果
調査の結果、どちらも採用実績が多く、Kotlinの静的解析ツールにおけるデファクトスタンダードであることが分かりました。
その上で
- detektはktlintより後発かつルール設定がより詳細にできること
- detekt-formattingはktlintのラッパーであり、解析ルールが同じであること
- linter2種類の両使いはルールの競合などが複雑になり、依存関係も無用に増えるので避けたい
という観点から、detekt + detekt-formatting
を採用することにしました。
detektのセットアップ&使い方
それでは実際のdetektのセットアップや使い方について説明していきます。
ちなみに環境は以下の通りです。
Gradle:7.3.2
Kotlin:1.5.31
JVM:11.0.14 (Amazon.com Inc. 11.0.14+9-LTS)
また今回はSpring Initializrで作成したプロジェクトに導入する前提で解説しています。
Gradleにdetektを導入する
detektは以下のようにbuild.gradle.kts
に設定を追加することで利用が可能です。
plugins {
id("io.gitlab.arturbosch.detekt") version "[version]"
}
dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:[version]")
}
これによりGradleのtasksにdetektが追加され、./gradlew detekt
で静的解析を利用できるようになります。
基本的なカスタマイズ方法
detektでは静的解析の設定を細かく調整ができ、今回は基本的なカスタマイズについてのみ紹介します。
細かい部分は公式のドキュメントがちゃんと整備されているので、参考にしてください!
ルールの拡張
detektではbuild.gradle.kts内でクロージャを用いて、ルールの拡張などを行えます。
detekt {
// 様々な設定の変更が可能
}
また、より細かい設定の変更をしたい場合はbuildUponDefaultConfigを有効にし、独自のdetekt.yml
をconfigに追加することで、デフォルトの設定をもとにルールを上書きできます。
ちなみにデフォルトの設定は、GitHubで確認できます。
detekt {
config = files("config/detekt/detekt.yml")
buildUponDefaultConfig = true
}
例えば、1行あたりの文字数を制限するMaxLineLength
を無効にしたい場合は以下のように設定します。
style:
MaxLineLength:
active: false
エラーの抑制
せっかくのルールを破ることになるので基本的には使わない方が良いと思いますが、detektでは@Suppressアノテーションを利用することでエラーの抑制をできます。
@Suppress("[ErrorName]")
自分たちのカスタマイズ
基本的な方針として、新規プロダクトへdetektを導入しているため、detektの推奨ルールを遵守するようにしています。
その上で、より開発がスムーズになるように以下のカスタマイズを現在(2022年3月)は使用しています。
detekt {
source = files(".")
autoCorrect = true
}
detektプラグインのデフォルトでは、source = files("src/main/java", "src/test/java", "src/main/kotlin", "src/test/kotlin")
になっています。
そのため、プロジェクト直下のbuild.gradle.kts
などがdetektの走査範囲外になっています。
今回はフォーマットもdetektの機能を使っているため、プロジェクト全体を走査するようsource = files(".")
という設定をしています。
またautoCorrectを有効にすることで、フォーマッターによって自動で修正を適用するようにしています。
チームでdetektを利用するための工夫
せっかく静的解析ツールを使用するのであれば、常にコミットされるコードは全て正しい状態である方が望ましく、チーム全体で機械的に静的解析するようにしたいと考えました。
そこで今回はgithooksのpre-commitを利用し、コミット時に強制的に./gradlew detekt
が起動するようにしました。
pre-commitファイル
pre-commitファイルは、公式のサンプルを利用しています。
#!/usr/bin/env bash
echo "Running detekt check..."
OUTPUT="/tmp/detekt-$(date +%s)"
./gradlew detekt > $OUTPUT
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
cat $OUTPUT
rm $OUTPUT
echo "***********************************************"
echo " Detekt failed "
echo " Please fix the above issues before committing "
echo "***********************************************"
exit $EXIT_CODE
fi
rm $OUTPUT
このbashスクリプトは、5行目で実行したdetektの終了ステータスを受け取り、6行目のif文で終了ステータスが0以外の場合に13行目のexitでコミットが中断されます。
もしこの仕組みが正常に機能しないとプロセスが適切に落ちず、たとえdetektのチェックでエラーになったとしてもコミットが完了してしまいます。
そのため、もし改造する場合は注意が必要です。
githooksの設定
さてpre-commitファイルの準備は整いましたが、次に問題になるのがgithooksの設定をチームで共有する方法です。
この部分については、過去に使用したことのあるhuskyの設定方法を参考にしました。
huskyのversion6以降では専用の準備コマンドにより、.husky
というディレクトリを作成し、hooksPathに設定されます。
今回の実装では、.githooks
というディレクトリを作成し、その中にpre-commitファイルを配置しGitで管理することで、githooksの設定をチームに共有するという方針にしました。
.githooks
└── pre-commit
その上で開発者用のドキュメントにhooksPathを変更するコマンドを記載することで、チームで共通のgithooksが使えるようにしました。
$ git config core.hooksPath .githooks
CircleCIへの設定追加
基本的にはpre-commitで正しい状態のコミットのみがpushされてくる想定をしていますが、pre-commitが上手く起動せずdetektのチェックをすり抜けることもあります。
そのため、CircleCIのジョブの1つとしてもdetektのチェックを行うよう設定を追加しています。
- run:
name: Run Detekt
command: ./gradlew detekt
今後、実現したいことについて
今回の実装では./gradlew detekt
をそのまま使っているため、pre-commit時に全ファイルの走査が行われています。
これは今後、プロダクトのコードが増えてきた際、静的解析に掛かる時間という形で足かせになると思われます。
現在は開発初期段階であることやコマンドライン上からファイルを指定する方法が簡単に見つからなかったことなどを理由に深堀りをしていません。
しかし問題が顕在化する前までにpre-comitを改造し、lint-stagedのようにコミットするファイルのみに限定した静的解析が行われるようにしたいと考えています!
さいごに
以上でサーバーサイドKotlinのプロジェクトにdetektを導入し、運用できます。
detektはデフォルトで運用するとルールが厳しい部分もありますが、自分自身が書いたコードの良し悪しに気づけるため、導入して良かったです!
現在、自分の所属するマネーフォワード 名古屋開発拠点ではエンジニアを募集中です!
- 社会の成長を加速させるような革新的なプロダクト開発に携わりたい
- 「開発拠点立ち上げ」「新規プロダクト開発」といったチャレンジングな環境で自分自身を飛躍的に成長させたい
- 名古屋や東海エリアに愛着があり、地方のITコミュティを盛り上げていきたい
という方は、ぜひご応募ください。カジュアル面談からでも大歓迎です!
名古屋開発拠点 求人ページ
Discussion