detekt カスタムルールでコーディング規約を運用する
はじめに
detektとは
コードの品質を保つために、Linterは欠かせません。Kotlinプロジェクトではそのうちの一つとしてdetekt
が広く利用されています。detekt
はKotlinのベストプラクティスに基づいて多数のルールセットを提供する強力なツールです。
更にCIを利用してdangerやreviewdogのようなフィードバックツールにdetektの解析結果を連携することで、Pull Requestに解析結果を自動でコメントしてコードレビューの負荷を大きく軽減することも可能です。
コーディング規約は運用が難しい
チーム開発をするうえではコーディング規約(命名規則、インデント、コメントの付け方、コードスタイルの統一など)を決めることが多くあります。コーディング規約を決めることで、コードの一貫性、保守性を向上することができます。
ただ、コーディング規約を守るためには多大な努力が必要であり、形骸化するリスクに立ち向かう必要があります。
コーディング規約の運用を困難にするのは、以下のような理由が考えられます。
- ルールが厳密に明文化されていない
- 強制力がない
- 新規参画メンバーがコーディング規約の存在を認知できていない
- コーディング規約を指摘する古参メンバーがいなくなることで、忘れ去られる
detekt カスタムルールの有用性
コーディング規約を守るためには「ルールが厳密に明文化された」うえでCI等で「強制力」をもって日々チェックすることが望ましいです。ただプロジェクトによってコーディング規約は大きく異なるため、linterが提供する標準のルールではカバーできないことが多くあります。
嬉しいことにdetektにはカスタムルールを作成する機能が備わっています。慣れ親しんだkotlinのコードでコーディング規約を記述して、detektの解析ルールに追加することができます。
この記事の対象
この記事では、detektでカスタムルールを作成する方法について解説します。
- この記事で扱わないこと
- detektの初期導入方法
- detektとCIの連携方法
- 対象読者
- detektを既にプロジェクトに導入している
- detektでコーディング規約も守りたい
- この記事のゴール
- detektでカスタムルールを作成できる
- 作成したカスタムルールのテストコードも作成できる
以降の内容は公式templateをベースに既存プロジェクトへ導入する手順を解説していきます。
detektモジュールの導入
detekt モジュールの作成
detekt custome rulesは専用のbuild.gradle.kts
が必要になります。既存プロジェクトに導入する場合、:detekt
のモジュールを作成していくことにします。
下記の手順でdetektモジュールを追加します。
(Android Studioの場合)
File > New > New Module
Java or Kotlin Library
- Library name =
detekt
ライブラリの追加
detektカスタムルールに必要な依存を追加していきます。
- plugin
org.jetbrains.kotlin.jvm
- library
io.gitlab.arturbosch.detekt:detekt-api
io.gitlab.arturbosch.detekt:detekt-test
(version catalogの場合)
[versions]
detekt = "1.23.7"
jetbrainsKotlinJvm = "1.9.0"
[libraries]
detekt-api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" }
detekt-test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" }
[plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
root projectのbuild.gradle.kts
に以下の変更を追加します。
import io.gitlab.arturbosch.detekt.Detekt
plugins {
alias(libs.plugins.detekt)
// kotlin jvmを追加
+ alias(libs.plugins.jetbrainsKotlinJvm) apply false
}
dependencies {
// detektモジュールをdetekt pluginとして読み込む
+ detektPlugins(project(":detekt"))
}
// detektモジュールのカスタムルールがビルドが完了してから、detekt解析を実行する
+ tasks.withType<Detekt> { dependsOn(":detekt:assemble") }
detektモジュールのbuild.gradle.kts
に以下の変更を追加します。
plugins {
alias(libs.plugins.jetbrainsKotlinJvm)
}
dependencies {
compileOnly(libs.detekt.api) // "io.gitlab.arturbosch.detekt:detekt-api"
testImplementation(libs.detekt.test) // "io.gitlab.arturbosch.detekt:detekt-test"
// (任意)この辺はお好みのテストライブラリ
testImplementation(libs.kotlin.test)
testImplementation(libs.junit)
}
kotlin {
jvmToolchain(8)
}
// detektルールをテストするための設定
tasks.withType<Test>().configureEach {
useJUnitPlatform()
systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class")
systemProperty("compile-snippet-tests", project.hasProperty("compile-test-snippets"))
}
カスタムルールの作成
カスタムルールの仕組み
detektカスタムルールは以下の3つから構成されます
-
Rule
- detektのルールを定義するクラス
- これを継承してカスタムルールを作成する
-
RuleSetProvider
- 作成した
Rule
を登録するクラス
- 作成した
-
META-INF/services
- 作成した
RuleSetProvider
をdetektプラグインに連携するためのファイル
- 作成した
detektモジュールの構成は下記のようなイメージです。
.
└── src
├── main
│ ├── kotlin
│ │ └── $package
│ │ ├── MyRuleSetProvider.kt
│ │ └── MyRule.kt
│ └── resources
│ ├── META-INF
│ │ └── services
│ │ └── io.gitlab.arturbosch.detekt.api.RuleSetProvider
│ └── config
│ └── detekt.yml
└── test
└── kotlin
└── $package
└── MyRuleTest.kt
Rule
の作成(本編)
Rule
クラスを継承してカスタムルールを作成します。
必要な実装は以下の内容です。
-
issue
-
detekt.yml
の設定ファイルで指定する内容を定義
-
-
visitXxx
メソッド- kotlinコード解析時に呼び出されるメソッド
- 例:
visitClass()
,visitProperty()
,visitNamedFunction()
, etc...
- 例:
- コードの構造が
KtClass
やKtNamedFunction
のようなPSI(Program Structure Interface)として取得できる- これをチェックしてカスタムルールに違反しているかを判断する
- コードがルールに違反している場合、
report
メソッドでissueを出力する
- kotlinコード解析時に呼び出されるメソッド
-
report
メソッド- detektの解析実行時、issueを出力する
命名ルールやメソッドの制限など、好みのコーディング規約を実装することができます。
今回は例として「Repositoryが公開するメソッドはsuspend
であること」というルールを作ることにします。(Repositoryにある処理がmainスレッドを要求するのは、何か特別な事情か怪しい実装をしているかもしれないねの思い)
RepositorySuspendRule.kt
をdetektモジュールのmain
配下に作成します。
import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.psiUtil.isPublic
// Ruleクラスを継承することで、detekt カスタムルールとなる
class RepositorySuspendRule(config: Config) : Rule(config) {
// ymlで設定する項目の定義
override val issue = Issue(
id = javaClass.simpleName, // ymlで設定するルール名 今回はクラス名にする
severity = Severity.CodeSmell,
description = "Public function in repository should be suspend.",
debt = Debt.FIVE_MINS,
)
// classを解析時に呼び出される処理
override fun visitClass(klass: KtClass) {
super.visitClass(klass)
// クラス名に"Repository"が含まれていれば検査する
if (klass.name?.contains("Repository") == true) {
// 各関数について検査する
klass.body?.functions?.forEach { function ->
if (function.isPublic && !function.hasModifier(KtTokens.SUSPEND_KEYWORD)) {
// suspendのついていない公開された関数があればissueとして出力する
sendReport(function)
}
}
}
}
private fun sendReport(function: KtNamedFunction) {
report(
finding = CodeSmell(
issue = issue,
entity = Entity.from(function), // 出力される指摘箇所を指定する
message = "Public function in repository should be suspend.", // 出力されるメッセージ
)
)
}
}
RuleSetProvider
の作成
作成したRuleをdetektに登録する必要があります。
Ruleの登録はRuleSetProvider
を利用します。
MyRuleSetProvider.kt
(命名は任意)をdetektモジュールのmain配下に作成します。
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider
class MyRuleSetProvider : RuleSetProvider {
override val ruleSetId: String = "my-rule" // ymlで設定するトップレベルのルール名
override fun instance(config: Config): RuleSet {
return RuleSet(
id = ruleSetId,
rules = listOf(
RepositorySuspendRule(config), // 作成したルールを任意の数追加できる
),
)
}
}
META-INF/services
の作成
RuleSetProvider
を作成したら、それをdetektに連携する必要があります。
/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
を作成し、作成したRuleSetProvider
のパスを記述します。
.
└── src
└── main
├── kotlin
│ └── com
│ └── example
│ ├── MyRuleSetProvider.kt
│ └── RepositorySuspendRule.kt
└── resources
└── META-INF
└── services
└── io.gitlab.arturbosch.detekt.api.RuleSetProvider
com.example.MyRuleSetProvider
これでdetekt.yml
からRepositorySuspendRule
を指定できるようになりました。
作成したルールをactiveにする
detektのルールはデフォルトでOFFになっています。
ルールを作成しただけではdetektで実行されないためdetekt.yml
で指定する必要があります。
my-rule:
RepositorySuspendRule:
active: true
これでカスタムルールの追加は完了です🎉
./gradlew detekt
を実行すると追加したルールが反映されていることを確認できます。
カスタムルールのテスト
作成したルールが正しく動作しているかを確認するのに、実際のコードベースを変更するのは非効率です。detekt カスタムルールはテストコードにも対応しているため、ぜひ活用しましょう。
detekt/src/test/kotlin/io/github/kabos/bus/detekt/RepositorySuspendRuleTest.kt
を作成します。
以下は「Repositoryが公開するメソッドはsuspend
であること」のテストの例です。
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
@Suppress("NonAsciiCharacters")
@KotlinCoreEnvironmentTest
internal class RepositorySuspendRuleTest(private val env: KotlinCoreEnvironment) {
@Test
fun `公開された関数がsuspendになっている場合、Issueを検知しない`() {
val code = """
class FooRepository {
suspend fun get() {
// suspendなのでok
}
private fun save() {
// privateなのでsuspendがなくてもok
}
}
""".trim()
val result = RepositorySuspendRule(Config.empty).compileAndLintWithContext(env, code)
assertEquals(0, result.size)
}
@Test
fun `公開された関数がsuspendになっていない場合、Issueを検知する`() {
val code = """
class FooRepository : Repository {
fun get() {
// suspendになっていないのでNG
}
overrider fun save() {
// suspendになっていないのでNG
}
}
""".trim()
val result = RepositorySuspendRule(Config.empty).compileAndLintWithContext(env, code)
assertEquals(2, result.size)
}
}
まとめ
この記事ではdetektカスタムルールの作成方法について解説しました。慣れ親しんだkotlinのコードで直感的に様々なルールを定義することができます。
コーディング規約をdetektのルールとして明文化することで、レビュー負荷を軽減し、プロジェクトの品質を保つために役立てることができます。
detektを活用してKotlinの世界に秩序をもたらしましょう🎉
参考リンク
- 公式ドキュメント
- 公式template
- 今回の記事を個人プロジェクトに導入したPR
- detektカスタムルールを知ったきっかけ
Discussion