ktlint adalah linter dan formatter yang menerapkan Kotlin Coding Conventions resmi dari JetBrains. Berbeda dengan detekt yang fokus pada code smell, ktlint fokus pada style: indentasi, semicolon, panjang baris, wildcard import, dan sebagainya. ktlint tidak memerlukan konfigurasi — aturan default sudah mengikuti konvensi resmi. Pada chapter ini kita akan membangun mesin ktlint sederhana yang mendemonstrasikan ktlintCheck dan ktlintFormat.

Konvensi yang Diterapkan ktlint #

Beberapa aturan utama ktlint:

AturanDeskripsiAuto-fix
no-semicolonsHapus semicolon di akhir barisYa
indent4 spasi, bukan tabYa
max-line-lengthDefault 100 (dapat diubah via .editorconfig)Tidak
no-wildcard-importsLarang import x.*Tidak
colon-spacingSpasi setelah : pada return typeYa
trailing-commaTrailing comma pada argumen multi-barisYa

Demo ktlintCheck dan ktlintFormat #

Berikut implementasi ktlint sederhana yang mendukung dua mode: check (lapor pelanggaran) dan format (terapkan auto-fix).

kotlin
// Demo ktlint: linter + formatter untuk Kotlin Coding Conventions.
// Simulasi aturan formatting (.editorconfig) tanpa dependensi eksternal.

data class PelanggaranKtlint(
    val rule: String,
    val baris: Int,
    val kolom: Int,
    val pesan: String,
    val autoFixable: Boolean
)

class LaporanKtlint {
    private val temuan = mutableListOf<PelanggaranKtlint>()
    fun tambah(t: PelanggaranKtlint) = temuan.add(t)
    fun autofixable() = temuan.count { it.autoFixable }
    fun manual() = temuan.count { !it.autoFixable }
    fun total() = temuan.size
    fun cetak() {
        println("=== ktlint report ===")
        println("Total: ${total()} (${autofixable()} auto-fixable, ${manual()} manual)")
        println()
        temuan.forEach {
            val tag = if (it.autoFixable) "[AUTOFIX]" else "[MANUAL]"
            println("  $tag ${it.rule} di baris ${it.baris}:${it.kolom} -> ${it.pesan}")
        }
    }
}

class KtlintChecker {
    fun periksa(sumber: String): LaporanKtlint {
        val laporan = LaporanKtlint()
        sumber.lines().forEachIndexed { i, baris ->
            val no = i + 1
            // Aturan: no-semicolons (semicolon di akhir baris)
            if (baris.trimEnd().endsWith(";")) {
                laporan.tambah(PelanggaranKtlint("no-semicolons", no, baris.length,
                    "Hapus semicolon di akhir baris", autoFixable = true))
            }
            // Aturan: indent 4 spasi (bukan tab)
            if (baris.startsWith("\t")) {
                laporan.tambah(PelanggaranKtlint("indent", no, 1,
                    "Gunakan 4 spasi, bukan tab", autoFixable = true))
            }
            // Aturan: max-line-length (default 100/120)
            if (baris.length > 100) {
                laporan.tambah(PelanggaranKtlint("max-line-length", no, 100,
                    "Baris lebih dari 100 karakter (${baris.length})", autoFixable = false))
            }
            // Aturan: no-wildcard-imports
            if (Regex("""^import\s+\w+(\.\w+)*\.\*""").containsMatchIn(baris)) {
                laporan.tambah(PelanggaranKtlint("no-wildcard-imports", no, 1,
                    "Hindari wildcard import", autoFixable = false))
            }
            // Aturan: spacing-around-colon untuk deklarasi tipe
            val m = Regex("""fun\s+\w+\s*\([^)]*\)\s*:(?!\s)""").find(baris)
            if (m != null) {
                laporan.tambah(PelanggaranKtlint("colon-spacing", no, m.range.first + 1,
                    "Tambahkan spasi setelah titik dua return type", autoFixable = true))
            }
        }
        return laporan
    }

    fun format(sumber: String): String {
        // ktlintFormat: terapkan auto-fix saja
        return sumber.lines().joinToString("\n") { baris ->
            var hasil = baris
            hasil = hasil.replace("\t", "    ".repeat(1))  // tab -> 4 spasi (sederhana)
            hasil = hasil.trimEnd().removeSuffix(";")
            hasil
        }
    }
}

fun main() {
    println("=== Demo ktlint (Check) ===")

    val kodeKotor = """
import kotlin.collections.*

fun hitung(a:Int, b: Int):Int {
	val total = a + b;
	return total;
}

fun namaSangatPanjangYangMelebihiBatasStandarYaituSeratusKarakterDenganTambahanKataLagi(x: Int): Int = x
""".trimIndent()

    val checker = KtlintChecker()
    val laporan = checker.periksa(kodeKotor)
    laporan.cetak()

    println()
    println("=== Setelah ktlintFormat (autofix) ===")
    val bersih = checker.format(kodeKotor)
    println(bersih)

    println()
    println("=== Re-check setelah format ===")
    val laporan2 = checker.periksa(bersih)
    laporan2.cetak()
}

Output yang Dihasilkan:

text
=== Demo ktlint (Check) ===
=== ktlint report ===
Total: 7 (5 auto-fixable, 2 manual)

  [MANUAL] no-wildcard-imports di baris 1:1 -> Hindari wildcard import
  [AUTOFIX] colon-spacing di baris 3:1 -> Tambahkan spasi setelah titik dua return type
  [AUTOFIX] no-semicolons di baris 4:19 -> Hapus semicolon di akhir baris
  [AUTOFIX] indent di baris 4:1 -> Gunakan 4 spasi, bukan tab
  [AUTOFIX] no-semicolons di baris 5:14 -> Hapus semicolon di akhir baris
  [AUTOFIX] indent di baris 5:1 -> Gunakan 4 spasi, bukan tab
  [MANUAL] max-line-length di baris 8:100 -> Baris lebih dari 100 karakter (104)

=== Setelah ktlintFormat (autofix) ===
import kotlin.collections.*

fun hitung(a:Int, b: Int):Int {
    val total = a + b
    return total
}

fun namaSangatPanjangYangMelebihiBatasStandarYaituSeratusKarakterDenganTambahanKataLagi(x: Int): Int = x

=== Re-check setelah format ===
=== ktlint report ===
Total: 3 (1 auto-fixable, 2 manual)

  [MANUAL] no-wildcard-imports di baris 1:1 -> Hindari wildcard import
  [AUTOFIX] colon-spacing di baris 3:1 -> Tambahkan spasi setelah titik dua return type
  [MANUAL] max-line-length di baris 8:100 -> Baris lebih dari 100 karakter (104)

Bedah Output #

Output demo menunjukkan dua siklus check-format-check:

  1. Check pertama: 7 pelanggaran (5 auto-fixable, 2 manual). Auto-fixable mencakup semicolon dan tab; manual mencakup wildcard import dan baris terlalu panjang.
  2. Format: auto-fix diterapkan. Tab diubah menjadi 4 spasi; semicolon dihapus.
  3. Check kedua: tersisa 3 pelanggaran yang manual (wildcard import dan baris panjang).

Pelanggaran manual harus diperbaiki dengan refactor: hapus wildcard import dan pecah baris panjang menjadi beberapa baris.

Integrasi Gradle #

Plugin org.jlleitschuh.gradle.ktlint menyediakan task ktlintCheck dan ktlintFormat:

kotlin Contoh build.gradle.kts
plugins {
    id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}

ktlint {
    version.set("1.2.1")
    android.set(false)
    outputToConsole.set(true)
    ignoreFailures.set(false)  // build gagal bila ada pelanggaran
}

.editorconfig #

Beberapa aturan ktlint dapat dikonfigurasi via .editorconfig di root project:

ini Contoh .editorconfig
root = true

[*.{kt,kts}]
max_line_length = 120
indent_size = 4
insert_final_newline = true
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_function-signature = enabled

Referensi Lanjutan #

  • ktlint — repository resmi ktlint untuk formatting dan style Kotlin

Penutup #

ktlint dan detekt saling melengkapi: ktlint untuk style, detekt untuk code smell. Banyak tim memakai keduanya bersamaan. Pada chapter berikutnya kita akan membahas compiler warnings — peringatan yang diberikan compiler Kotlin sendiri, dan cara terbaik menanganinya.