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:

RulesetContoh aturanTujuan
styleWildcardImport, MaxLineLengthKonvensi penulisan
complexityLongMethod, ComplexCondition, CyclomaticComplexMethodKompleksitas kode
potential-bugsTooGenericExceptionCaught, UnsafeCallOnNullableTypePotensi bug runtime
emptyEmptyFunctionBlock, EmptyCatchBlockKode mati
coroutinesGlobalCoroutineUsage, RedundantSuspendModifierBest practice coroutine

Konfigurasi detekt.yml #

File config/detekt.yml memuat konfigurasi tiap aturan: apakah aktif, threshold, dan severity. Contoh:

yaml Contoh config/detekt.yml
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: true

Demo Mesin detekt #

Berikut implementasi mesin detekt sederhana yang membaca konfigurasi dan memeriksa kode sumber.

kotlin
// 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:

text
=== 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 spesifik

Custom 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:

kotlin Contoh build.gradle.kts
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.