🔍

Luup Androidチームで採用しているdetektルール紹介

に公開

こんにちは、Luup Androidチームの土谷です。

今回は、私たちのチームで採用している静的解析ツール detekt のルール設定について紹介します。 Luupでは「標準機能の活用」と「独自ルールの実装」を使い分け、コード品質を仕組みで守る体制を整えています。

detektについて簡単におさらい

detektは、Kotlinプロジェクトのための静的コード解析ツールです。静的コード解析とは、プログラムを実際に実行することなく、ソースコードを分析して問題点を見つける手法のことです。detektは、皆さんの書いたKotlinのコードをさまざまな観点からチェックし、コード品質の向上をサポートしてくれます。

導入について

こちらを参考にしてください
https://github.com/detekt/detekt

1. 標準ルールを活用した設定

detektの標準機能である ForbiddenImport (特定のインポートを禁止するルール)を利用して、プロジェクト全体の統制を図っています。

Jakartaパッケージのインポートを禁止するルール(ビルドエラー防止)

  • 背景: 以前、ViewModelからロジックを独立させた際に import jakarta.inject.Singleton を使用したところ、Androidのエコシステムが依然として javax ベースであるため、ビルドエラーが発生する事象がありました。
  • 目的: Androidエコシステムの現状を鑑みると、私たちのプロジェクトで jakarta パッケージへ移行するのはまだ少し早いと判断しました。開発フェーズにおける不要な混乱を避け、確実にビルドが通る javax パッケージに統一しています。
  • 内容: jakarta.**(サブパッケージを含むすべて)のインポートを検出し、ビルド時に警告・エラーを出します。
// 問題のあるコード
import jakarta.inject.Inject // -> detektが警告(ビルドが通らなくなるため)

// 修正後のコード
import javax.inject.Inject // OK

JUnitのAssertではなく、Google Truthの利用を推奨するルール

  • 目的: テストコードの可読性を高め、失敗時のエラーメッセージをより分かりやすくするため、表現力豊かなGoogle Truthライブラリーへの移行を促します。
  • 内容: org.junit.Assert のインポート文を検出します。
// 問題のあるコード
import org.junit.Assert.assertEquals // -> detektが警告

// 修正後のコード
import com.google.common.truth.Truth.assertThat // OK

// 比較例
// JUnit:assertEquals("hoge", fuga.name)
// Truth:assertThat(fuga.name).isEqualTo("hoge") // より自然な文章に近い

2. 独自実装したカスタムルール

標準機能では対応できない、プロジェクト固有の規約については Kotlin でカスタムルールを作成しています。

① Sealed Class内のコレクション直接初期化禁止

  • 背景: sealed class の companion object 内で、そのクラスのサブクラス(objectなど)をリスト等で直接初期化すると、初期化のタイミング(または初期化順序)によって要素が null になる致命的なリスクがあります。 これは、特定の要素(例:HOGE)に最初にアクセスした際、クラスのロードプロセスの中で companion object が先に初期化されようとする一方、まだインスタンス化が終わっていない要素をリストに詰めようとしてしまう「自己参照による初期化不備」が発生するためです(Kotlinの既知の仕様)。
  • 目的: 実行時の NullPointerException や予期せぬ挙動を防ぎ、安全に全要素を参照できるようにします。
  • 内容: companion object 内で listOf などのコレクション初期化関数を直接使用している箇所を検出します。
sealed class Sample {
    object Alpha : Sample()
    object Beta : Sample()
    object Gamma : Sample()

    companion object {
        // ❌ NG: 初期化ループにより、要素が null になる可能性がある
        val values = listOf(Alpha, Beta, Gamma) 

        // ✅ OK (案1): カスタムゲッターを使用
        // アクセスされるたびに評価されるため、全要素の初期化完了が保証される
        val values get() = listOf(Alpha, Beta, Gamma)

        // ✅ OK (案2): lazyを使用
        // 最初にアクセスされたタイミングで一度だけ評価されるため安全
        val values by lazy { listOf(Alpha, Beta, Gamma) }
    }
}

② WebViewの直接利用禁止

  • 目的: プロジェクト内でWebViewの挙動を一元管理するため、特定の共通コンポーネント(例:CommonWebView)の使用を強制します。
  • 内容: android.webkit.WebView の呼び出しを検出し、代わりに CommonWebView を使うよう促します。
// 問題のあるコード
val webView = WebView(context) // -> detektが警告

// 修正後のコード
val commonWebView = CommonWebView(context) // OK

③ Repositoryクラスに@Singletonアノテーションを強制

  • 背景: Repository の一部にアノテーションを付け忘れてしまい、Repository 内にキャッシュされているはずの値が取得できなくなるという不具合が起こりました。
  • 目的: リポジトリクラスがDIフレームワークで適切にシングルトンとして管理されるよう、ルールを適用します。
  • 内容: クラス名に Repository が含まれるクラスで、@Singleton が付いていない場合に警告します。ただし、インターフェース、テストクラス、DIモジュール等は除外されます。
// 問題のあるコード
class HogeRepository @Inject constructor() { /* ... */ } // -> detektが警告

// 修正後のコード
@Singleton
class HogeRepository @Inject constructor() { /* ... */ } // OK

④ Repositoryの公開プロパティにおける直接代入禁止

  • 背景: 公開プロパティが宣言時に内部プロパティの値で直接初期化され、その後メソッド内で内部プロパティが更新されても、公開プロパティには反映されず、不具合が起こりました。
  • 目的: バッキングプロパティ(_property)を公開する際、直接代入ではなく getter を使用することでカプセル化を徹底し、意図しない値の保持や初期化の問題を防ぎます。
  • 内容: 公開プロパティがバッキングプロパティから直接代入されている場合に警告します。
class SampleRepository {
    private val _data = "value"

    // ❌ NG: 直接代入
    val data: String = _data 

    // ✅ OK: getterを使用
    val data: String get() = _data 
}

※ Flow や StateFlow 等のストリーム型は、利便性を考慮して除外しています。


3. ユニットテストによるルールの品質担保

カスタムルールは便利ですが、誤検知(偽陽性)で開発者の手を止めてはいけません。私たちは全てのカスタムルールに対して詳細なユニットテストを書いています。

「違反を正しく検出できるか」だけでなく、「許可すべきパターン(Flow型の代入やテストクラスなど)を誤って検知しないか」を厳密に検証することで、ルールの信頼性を担保しています。


4. AIレビューとdetektの役割分担:確実な「ハードゲート」の構築

最近では、AIによるコードレビュー(Claude Codeを用いた自動レビューなど)も積極的に活用しています。ここで「AIがあるならdetektは不要では?」と思われるかもしれませんが、私たちは明確に役割を使い分けています。

  • AIレビューの役割(高度・文脈的)「このPRの設計がプロジェクトのアーキテクチャドキュメントに違反していないか」「より良い命名や抽象化の提案」など、コードの背景や文脈を汲み取った高度なレビューを得意としています。
  • detektの役割(機械的・確実) 一方で、detektの最大の強みは CIを「確実に」失敗させられる(ハードゲートの構築) という点にあります。

AIレビューは非常に強力ですが、出力に揺らぎがあったり、指摘が「提案」に留まることもあります。対してdetektは、設定したルールに触れれば機械的にマージをブロックします。

「機械的に判断できる規約」はすべてdetektで自動化してハードゲート化し、人間やAIはより創造的で複雑なロジックの議論に集中する。この 「併用」 こそが、Luup Androidチームの品質担保の鍵となっています。

最後に

Luupでは、一緒に開発してくださるソフトウェアエンジニアを積極的に募集しています。
カジュアル面談も実施しておりますのでぜひお気軽にお声掛けください。
https://recruit.luup.sc/

Luup Developers Blog

Discussion