detekt adalah static analyzer yang fokus pada code smell, kompleksitas, dan potensi bug di kode Kotlin. Berbeda dengan ktlint yang berfokus pada style, detekt memeriksa struktur kode: fungsi terlalu panjang, kondisi terlalu kompleks, exception terlalu umum, dan sebagainya. detekt dapat dikonfigurasi melalui config/detekt.yml, diperluas dengan custom rules, dan diintegrasikan ke Gradle sebagai plugin. Pada chapter ini kita akan membangun mesin detekt sederhana yang mendemonstrasikan konfigurasi aturan dan eksekusi pemeriksaan.
Ruleset Utama detekt #
detekt mengelompokkan aturan ke dalam ruleset berdasarkan kategori:
| Ruleset | Contoh aturan | Tujuan |
|---|---|---|
| style | WildcardImport, MaxLineLength | Konvensi penulisan |
| complexity | LongMethod, ComplexCondition, CyclomaticComplexMethod | Kompleksitas kode |
| potential-bugs | TooGenericExceptionCaught, UnsafeCallOnNullableType | Potensi bug runtime |
| empty | EmptyFunctionBlock, EmptyCatchBlock | Kode mati |
| coroutines | GlobalCoroutineUsage, RedundantSuspendModifier | Best practice coroutine |
Konfigurasi detekt.yml #
File config/detekt.yml memuat konfigurasi tiap aturan: apakah aktif, threshold, dan severity. Contoh:
complexity:
LongMethod:
active: true
threshold: 30
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
ComplexCondition:
active: true
threshold: 3
empty-blocks:
EmptyFunctionBlock:
active: true
potential-bugs:
TooGenericExceptionCaught:
active: trueDemo Mesin detekt #
Berikut implementasi mesin detekt sederhana yang membaca konfigurasi dan memeriksa kode sumber.
// Demo detekt: static analyzer dengan ruleset (simulasi tanpa dependensi eksternal).
enum class Severity { ERROR, WARNING, INFO }
enum class Ruleset { STYLE, COMPLEXITY, POTENTIAL_BUG, COROUTINES, EMPTY }
data class TemuanDetekt(
val rule: String,
val ruleset: Ruleset,
val severity: Severity,
val file: String,
val baris: Int,
val pesan: String
)
class LaporanDetekt {
private val temuan = mutableListOf<TemuanDetekt>()
fun tambah(t: TemuanDetekt) = temuan.add(t)
fun tambah(rule: String, rs: Ruleset, sev: Severity, file: String, b: Int, p: String) =
temuan.add(TemuanDetekt(rule, rs, sev, file, b, p))
fun total() = temuan.size
fun perRuleset(): Map<Ruleset, Int> = temuan.groupingBy { it.ruleset }.eachCount()
fun perSeverity(): Map<Severity, Int> = temuan.groupingBy { it.severity }.eachCount()
fun cetak() {
println("=== detekt report ===")
println("Total temuan: ${temuan.size}")
println()
println("Per ruleset:")
perRuleset().forEach { (rs, n) -> println(" $rs: $n") }
println()
println("Per severity:")
perSeverity().forEach { (s, n) -> println(" $s: $n") }
println()
println("Detail (10 teratas):")
temuan.take(10).forEach {
println(" [${it.severity}] ${it.rule} (${it.ruleset}) di ${it.file}:${it.baris} -> ${it.pesan}")
}
}
}
// Simulasi detekt.yml — memuat konfigurasi aturan aktif/tidak
data class KonfigurasiDetekt(
val aktif: Map<String, Boolean> = mapOf(
"ComplexCondition" to true,
"LongMethod" to true,
"MagicNumber" to false, // dimatikan untuk baseline
"TooGenericExceptionCaught" to true,
"EmptyFunctionBlock" to true,
"ReturnCount" to true
),
val ambangLongMethod: Int = 30,
val ambangReturnCount: Int = 3
)
class MesinDetekt(private val cfg: KonfigurasiDetekt = KonfigurasiDetekt()) {
fun periksa(sumber: String, file: String): LaporanDetekt {
val laporan = LaporanDetekt()
var dalamFungsi = false
var kedalaman = 0
var panjangFungsi = 0
var namaFungsi = ""
sumber.lines().forEachIndexed { i, baris ->
val no = i + 1
val m = Regex("""fun\s+(\w+)\s*\(""").find(baris)
if (m != null) {
dalamFungsi = true
panjangFungsi = 0
namaFungsi = m.groupValues[1]
}
if (dalamFungsi) {
panjangFungsi++
if (panjangFungsi > cfg.ambangLongMethod && cfg.aktif["LongMethod"] == true) {
laporan.tambah("LongMethod", Ruleset.COMPLEXITY, Severity.WARNING,
file, no, "Fungsi `$namaFungsi` lebih dari ${cfg.ambangLongMethod} baris")
dalamFungsi = false
}
}
if (cfg.aktif["ComplexCondition"] == true) {
val kondisiAnd = baris.split("&&").size - 1
val kondisiOr = baris.split("||").size - 1
if (kondisiAnd + kondisiOr >= 3) {
laporan.tambah("ComplexCondition", Ruleset.COMPLEXITY, Severity.WARNING,
file, no, "Kondisi terlalu kompleks (${kondisiAnd + kondisiOr} operator boolean)")
}
}
if (cfg.aktif["TooGenericExceptionCaught"] == true && Regex("""catch\s*\(\s*\w+\s*:\s*Exception""").containsMatchIn(baris)) {
laporan.tambah("TooGenericExceptionCaught", Ruleset.POTENTIAL_BUG, Severity.WARNING,
file, no, "Hindari catch Exception; tangkap tipe spesifik")
}
if (cfg.aktif["EmptyFunctionBlock"] == true && Regex("""fun\s+\w+\([^)]*\)\s*(\{[^}]*\}|=\s*Unit)""").containsMatchIn(baris) &&
Regex("""\{\s*}""").containsMatchIn(baris)) {
laporan.tambah("EmptyFunctionBlock", Ruleset.EMPTY, Severity.INFO,
file, no, "Blok fungsi kosong")
}
}
return laporan
}
}
fun main() {
println("=== Demo detekt ===")
val kode = """
fun prosesPesanan(pesanan: Pesanan, pelanggan: Pelanggan, diskon: Diskon): Double {
if (pesanan.total > 100000 && pelanggan.lokal && diskon.aktif && pesanan.item > 5) {
println("Memproses")
}
val total = 0
for (i in 1..1000) {
total + i
}
return pesanan.total - diskon.nilai
}
fun tidakMelakukanApaApa() {}
fun baca(): String {
try {
return readLine() ?: ""
} catch (e: Exception) {
return ""
}
}
""".trimIndent()
val mesin = MesinDetekt()
val laporan = mesin.periksa(kode, "PesananService.kt")
laporan.cetak()
}Output yang Dihasilkan:
=== Demo detekt ===
=== detekt report ===
Total temuan: 3
Per ruleset:
COMPLEXITY: 1
EMPTY: 1
POTENTIAL_BUG: 1
Per severity:
WARNING: 2
INFO: 1
Detail (10 teratas):
[WARNING] ComplexCondition (COMPLEXITY) di PesananService.kt:2 -> Kondisi terlalu kompleks (3 operator boolean)
[INFO] EmptyFunctionBlock (EMPTY) di PesananService.kt:12 -> Blok fungsi kosong
[WARNING] TooGenericExceptionCaught (POTENTIAL_BUG) di PesananService.kt:17 -> Hindari catch Exception; tangkap tipe spesifikCustom Rules #
detekt mendukung custom rules dengan membuat subclass io.gitlab.arturbosch.detekt.api.Rule. Setiap custom rule mendefinisikan pola yang ingin diperiksa (mis. “semua data class harus meng-override toString”). Custom rules ditempatkan di modul terpisah dan dideklarasikan via META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider.
Integrasi Gradle #
Plugin io.gitlab.arturbosch.detekt menyediakan task detekt dan detektMain. Konfigurasi minimal:
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.6"
}
detekt {
buildUponDefaultConfig = true
config.setFrom(files("config/detekt.yml"))
parallel = true
}
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
reports {
html.required.set(true)
xml.required.set(true)
sarif.required.set(true)
}
}Custom Baseline #
Untuk proyek yang sudah ada dengan banyak pelanggaran, detekt menyediakan baseline: file detekt-baseline.xml yang mencatat pelanggaran yang sudah ada dan diabaikan. Aturan baru kemudian diberlakukan; pelanggaran lama tetap ditoleransi sampai diperbaiki bertahap.
Referensi Lanjutan #
- detekt — dokumentasi resmi detekt untuk lint dan code smell Kotlin
Penutup #
detekt adalah alat yang fleksibel dan dapat dikonfigurasi sesuai standar tim. Pada chapter berikutnya kita akan membahas ktlint: linter yang fokus pada style dan formatting, yang sering dipakai bersama detekt untuk mencakup spectrum kualitas kode yang lengkap.