Compiler menangkap kesalahan sintaks dan tipe, tetapi banyak masalah kualitas kode tidak terdeteksi pada level compiler. Variabel var yang sebenarnya tidak perlu mutable, baris terlalu panjang, fungsi terlalu kompleks, atau blok catch kosong adalah contoh masalah yang tidak membuat kompilasi gagal namun mengurangi kualitas kode. Static analysis memeriksa kode sumber tanpa menjalankannya untuk menemukan pola-pola tersebut. Pada chapter ini kita akan memahami mengapa static analysis penting, alat utama di ekosistem Kotlin, dan cara mengintegrasikannya ke dalam CI/CD.

Mengapa Static Analysis Penting #

Static analysis bekerja pada level kode sumber, bukan runtime. Hal ini memberi beberapa keuntungan:

  • Cepat: pemeriksaan berlangsung dalam hitungan detik, tidak perlu setup runtime.
  • Deterministik: tidak ada masalah timing atau environment.
  • Dapat diotomatisasi: dijalankan pada setiap commit, sebelum code review manual.
  • Tidak bergantung test coverage: menemukan masalah bahkan pada kode yang belum tertest.

Tiga Alat Utama Ekosistem Kotlin #

AlatFokusContoh aturan
detektCode smell, kompleksitas, potensi bugLongMethod, ComplexCondition, TooGenericExceptionCaught
ktlintStyle dan konvensiIndent, no-semicolons, max-line-length
IntelliJ inspectionsVarietas luas, integrasi IDEUnused variable, redundant cast, parameter shadowing

Demo Static Analysis Sederhana #

Berikut implementasi sederhana yang mensimulasikan cara static analyzer memindai kode sumber. Mesin ini menerima kode sebagai string dan melaporkan pelanggaran aturan.

kotlin
// Demo konsep static analysis: memeriksa kode tanpa menjalankannya.
// Simulasi sederhana dari beberapa aturan detekt/ktlint/IntelliJ inspections.

data class Pelanggaran(
    val aturan: String,
    val severity: String,
    val file: String,
    val baris: Int,
    val pesan: String
)

class LaporanStatik {
    private val temuan = mutableListOf<Pelanggaran>()

    fun tambah(p: Pelanggaran) = temuan.add(p)
    fun tambah(aturan: String, severity: String, file: String, baris: Int, pesan: String) =
        temuan.add(Pelanggaran(aturan, severity, file, baris, pesan))

    fun cetak() {
        println("Total temuan: ${temuan.size}")
        println()
        println("%-30s %-10s %-20s %-5s %s".format("Aturan", "Severity", "File", "Baris", "Pesan"))
        println("-".repeat(95))
        temuan.sortedBy { it.baris }.forEach {
            println("%-30s %-10s %-20s %-5d %s".format(it.aturan, it.severity, it.file, it.baris, it.pesan))
        }
    }

    fun totalBySeverity(): Map<String, Int> = temuan.groupingBy { it.severity }.eachCount()
}

// Mock "AST walker" yang memeriksa kode sumber sebagai string.
class AnalisisStatik {
    fun periksa(sumber: String, file: String): LaporanStatik {
        val laporan = LaporanStatik()
        sumber.lines().forEachIndexed { i, baris ->
            val no = i + 1
            // Aturan: variabel `var` dengan kemungkinan mutable -> peringatan
            if (Regex("""\bvar\s+\w+""").containsMatchIn(baris)) {
                laporan.tambah("MutableVariable", "CODE_SMELL", file, no,
                    "Pertimbangkan `val` kecuali memang perlu mutable")
            }
            // Aturan: panjang baris > 100
            if (baris.length > 100) {
                laporan.tambah("MaxLineLength", "STYLE", file, no,
                    "Baris lebih dari 100 karakter")
            }
            // Aturan: println di production code
            if ("println" in baris) {
                laporan.tambah("PrintStackTrace", "WARNING", file, no,
                    "Hindari println di production code; gunakan logger")
            }
            // Aturan: TODO
            if ("TODO" in baris) {
                laporan.tambah("TODO", "INFO", file, no,
                    "TODO belum diselesaikan")
            }
        }
        return laporan
    }
}

fun main() {
    println("=== Demo Static Analysis ===")

    val kodeSumber = """
        data class Pengguna(val id: Int, var nama: String)  // var: mutable

        fun main() {
            var total = 0
            println("Mulai")  // println di production code
            // TODO: hapus kode berikut
            val daftarPanjangSekaliYangMelebihiBatasStandarKonvensiYaituSeratusKarakter = listOf(1, 2, 3)
            for (x in daftarPanjangSekaliYangMelebihiBatasStandarKonvensiYaituSeratusKarakter) {
                total += x
            }
        }
    """.trimIndent()

    val analisis = AnalisisStatik()
    val laporan = analisis.periksa(kodeSumber, "Pengguna.kt")
    laporan.cetak()

    println()
    println("=== Ringkasan per severity ===")
    laporan.totalBySeverity().forEach { (sev, n) ->
        println("  $sev: $n")
    }
}

Output yang Dihasilkan:

text
=== Demo Static Analysis ===
Total temuan: 4

Aturan                         Severity   File                 Baris Pesan
-----------------------------------------------------------------------------------------------
MutableVariable                CODE_SMELL Pengguna.kt          1     Pertimbangkan `val` kecuali memang perlu mutable
MutableVariable                CODE_SMELL Pengguna.kt          4     Pertimbangkan `val` kecuali memang perlu mutable
PrintStackTrace                WARNING    Pengguna.kt          5     Hindari println di production code; gunakan logger
TODO                           INFO       Pengguna.kt          6     TODO belum diselesaikan

=== Ringkasan per severity ===
  CODE_SMELL: 2
  WARNING: 1
  INFO: 1

Klasifikasi Severity #

Static analyzer umumnya mengklasifikasikan temuan berdasarkan severity:

  • ERROR: masalah yang harus diperbaiki sebelum merge (mis. kemungkinan NPE).
  • WARNING: masalah yang harus ditinjau (mis. catch Exception terlalu umum).
  • INFO: saran gaya (mis. nama variabel terlalu pendek).
  • CODE_SMELL: bukan bug, tetapi mengurangi readability.

Integrasi CI/CD #

Pada pipeline CI, static analysis dijalankan sebagai step terpisah setelah kompilasi. Output berupa laporan (HTML, JSON, atau SARIF) yang dapat ditampilkan sebagai comment di pull request. GitHub Actions, GitLab CI, dan Jenkins memiliki plugin untuk parsing laporan detekt dan ktlint.

Kapan Tidak Memakai Static Analysis #

Static analysis tidak menggantikan test. Beberapa masalah hanya dapat ditemukan dengan menjalankan kode (mis. bug logic, masalah concurrency, integrasi dengan sistem eksternal). Static analysis menemukan pola kode bermasalah; test menemukan perilaku bermasalah. Keduanya saling melengkapi.

Referensi Lanjutan #

  • detekt — dokumentasi resmi detekt untuk static analysis kode Kotlin
  • ktlint — repository resmi ktlint sebagai pelengkap detekt

Penutup #

Static analysis adalah lapisan pertama pertahanan terhadap masalah kualitas kode. Pada chapter berikutnya kita akan mendalami detekt sebagai static analyzer utama di ekosistem Kotlin: konfigurasi, ruleset, custom rule, dan integrasi Gradle.