📝

サーバーサイドKotlinへ静的解析ツールを導入する方法

2022/03/04に公開

自分が担当しているサーバーサイドKotlinのプロジェクトに静的解析ツールを導入したので、その時の調査結果や実装方法などを共有します。

個人的には前職ではNode.jsで開発を行っていたため、eslint + prettir + husky + lint-stagedのような使い心地の静的解析ツールの導入をKotlinでも実現したいと考えていました。

Kotlinにおける静的解析ツール

Kotlinの静的解析ツールとして有名なものは、ktlint、detekt、AndroidLintが挙げられます。
今回はサーバーサイドKotlinでの使用を考えていたので、AndroidLintを除いた2つを検討しました。

ktlint

ktlintはJavaScript Standard Stylegofmtの精神に基づき、anti-bikesheddingを謳うKotlinの静的解析ツールです。

特徴としては、

  • kotlinlang.orgAndroid Kotlin Style Guideの公式コードスタイルを反映しており、細かい設定が不要な点
  • フォーマッターを内蔵しているため、手動での修正が不要な点

などが挙げられます。

https://pinterest.github.io/ktlint/

detekt

detektはktlintよりも後発の静的解析ツールです。

特徴としては

  • コードの複雑度やコードの臭いなどをもとにリファクタリングの対象を指摘する点
  • 高度なルール設定が可能な点
  • @Suppressアノテーションを使用して、エラーを抑制できる点

などが挙げられます。

またdetektにはdetekt-formattingという拡張機能が提供されています。
detekt-formattingはktlintをラップしているため、detektでもktlintの解析ルールを簡単に利用できます。

https://detekt.dev/

選定結果

調査の結果、どちらも採用実績が多く、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に設定を追加することで利用が可能です。

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内でクロージャを用いて、ルールの拡張などを行えます。

build.gradle.kts
detekt {
    // 様々な設定の変更が可能
}

https://detekt.dev/gradle.html#kotlin-dsl-3

また、より細かい設定の変更をしたい場合はbuildUponDefaultConfigを有効にし、独自のdetekt.ymlをconfigに追加することで、デフォルトの設定をもとにルールを上書きできます。
ちなみにデフォルトの設定は、GitHubで確認できます。

build.gradle.kts
detekt {
    config = files("config/detekt/detekt.yml")
    buildUponDefaultConfig = true
}

例えば、1行あたりの文字数を制限するMaxLineLengthを無効にしたい場合は以下のように設定します。

detekt.yml
style:
    MaxLineLength:
        active: false

https://detekt.dev/configurations.html

エラーの抑制

せっかくのルールを破ることになるので基本的には使わない方が良いと思いますが、detektでは@Suppressアノテーションを利用することでエラーの抑制をできます。

@Suppress("[ErrorName]")

https://detekt.dev/suppressing-rules.html

自分たちのカスタマイズ

基本的な方針として、新規プロダクトへdetektを導入しているため、detektの推奨ルールを遵守するようにしています。
その上で、より開発がスムーズになるように以下のカスタマイズを現在(2022年3月)は使用しています。

build.gradle.kts
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ファイルは、公式のサンプルを利用しています。

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のチェックを行うよう設定を追加しています。

config.yml
- run:
      name: Run Detekt
      command: ./gradlew detekt

今後、実現したいことについて

今回の実装では./gradlew detektをそのまま使っているため、pre-commit時に全ファイルの走査が行われています。
これは今後、プロダクトのコードが増えてきた際に静的解析に掛かる時間という形で足かせになると思われます。

現在は開発初期段階であることやコマンドライン上からファイルを指定する方法が簡単に見つからなかったことなどを理由に深堀りをしていません。
しかし問題が顕在化する前までにpre-comitを改造し、lint-stagedのように実際にコミットするファイルのみに限定した静的解析が行われるようにしたいと考えています!

さいごに

以上でサーバーサイドKotlinのプロジェクトにdetektを導入し、運用できます。
detektはデフォルトで運用するとルールが厳しい部分もありますが、自分自身が書いたコードの良し悪しに気づけるようになるため、導入して良かったです!


現在、自分の所属するマネーフォワード 名古屋開発拠点ではエンジニアを募集中です!

  • 社会の成長を加速させるような革新的なプロダクト開発に携わりたい
  • 「開発拠点立ち上げ」「新規プロダクト開発」といったチャレンジングな環境で自分自身を飛躍的に成長させたい
  • 名古屋や東海エリアに愛着があり、地方のITコミュティを盛り上げていきたい

という方は、ぜひご応募ください。カジュアル面談からでも大歓迎です!

名古屋開発拠点 求人ページ

https://hrmos.co/pages/moneyforward/jobs/1804486623567261761

Discussion