Belajar Neovim - Code Formatting & Linting (conform.nvim & nvim-lint)
Episode 17 of 28

Belajar Neovim - Code Formatting & Linting (conform.nvim & nvim-lint)

Di episode ini kita akan mengotomatiskan pemformatan kode saat menyimpan file dengan conform.nvim dan menjalankan static analysis asinkron dengan nvim-lint, ditutup dengan navigasi diagnostics yang efisien.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Pendahuluan

Setelah di episode 16 sebelumnya kita membangun autocompletion yang responsif dengan nvim-cmp dan LuaSnip, pada episode kali ini kita akan menyelesaikan puzzle kualitas kode: formatting otomatis dan linting asinkron dengan conform.nvim dan nvim-lint.

Di dunia nyata, format-on-save dan linting adalah bagian dari culture code tim. Bayangkan kalian bergabung dengan codebase besar dengan 10 contributor: tanpa formatter, setiap orang menulis indentasi 2 spasi atau 4 spasi, kutip tunggal atau ganda, dan diff pull request dipenuhi perubahan yang tidak relevan. Tim yang matang memakai formatter untuk menghapus argumentasi style dari diskusi code review — debat difokuskan pada logika, bukan soal spasi. Sementara itu, linter bekerja sebagai silent reviewer: menangkap variabel yang tidak terpakai, kondisi yang tidak pernah benar, atau pola berbahaya sebelum kode sampai ke CI. Menjadikan keduanya berjalan otomatis di editor adalah standar engineering modern — dan itu semua bisa hidup di dalam Neovim.

Pembahasan Utama

Dua Tugas Berbeda: Formatting vs Linting

Sebelum masuk ke konfigurasi, penting membedakan dua tanggung jawab yang sering tertukar:

AspekFormattingLinting
TujuanNormalisasi gaya: indentasi, spasi, kutip, panjang barisDeteksi bug & anti-pattern: unused var, error-prone code
HasilKode ditulis ulang (mutasi file)Hanya melaporkan masalah (diagnostic)
ContohPrettier, Stylua, GofmtESLint, ShellCheck, Flake8
Waktu terbaikSaat menyimpan file (BufWritePre)Terus-menerus, tanpa memblokir pengetikan

Formatting mengubah kode, linting hanya menandai. Keduanya saling melengkapi: kode bisa lolos format tapi tetap bermasalah, dan sebaliknya.

Mengapa Format-on-Save Itu Penting

Fitur yang terlihat sepele ini menyelesaikan masalah konsistensi yang sebenarnya rumit secara sosial: kapan format dilakukan? Jika formatter hanya berjalan saat command tertentu, manusia akan lupa — dan diff tetap berantakan. Dengan format-on-save, aturannya deterministik: setiap file yang disimpan akan diformat. Tidak ada lagi "oh, PR ini menambah 40 baris perubahan format". Ini adalah automation over negotiation — kebijakan dijalankan mesin, bukan diharapkan dari disiplin manusia.

Konfigurasi conform.nvim dengan Format-on-Save

conform.nvim oleh stevearc adalah formatter runner modern: ia menjalankan binary formatter eksternal (Prettier, Stylua, dan lainnya) secara asinkron dan menulis hasilnya ke buffer. Berikut spec lengkapnya:

lua/plugins/conform.lua
return {
  {
    "stevearc/conform.nvim",
    event = { "BufWritePre" },
    cmd = { "ConformInfo" },
    keys = {
      {
        "<leader>f",
        function()
          require("conform").format({ async = true })
        end,
        desc = "Format file",
      },
    },
    opts = {
      formatters_by_ft = {
        lua = { "stylua" },
        javascript = { "prettier" },
        typescript = { "prettier" },
        javascriptreact = { "prettier" },
        typescriptreact = { "prettier" },
        json = { "prettier" },
        html = { "prettier" },
        css = { "prettier" },
        scss = { "prettier" },
        python = { "ruff_format", "ruff_organize_imports" },
        go = { "goimports", "gofmt" },
        sh = { "shfmt" },
        yaml = { "yamlfmt" },
        markdown = { "prettier" },
      },
      format_on_save = {
        timeout_ms = 2000,
        lsp_format = "fallback",
      },
    },
  },
}

Bedah poin penting:

  • formatters_by_ft — peta filetype → daftar formatter. Daftar dieksekusi berurutan dari kiri ke kanan: untuk Python, ruff_format menjalankan format dulu, lalu ruff_organize_imports merapikan import. Untuk Go, goimports menata import terlebih dahulu, kemudian gofmt menormalkan format.
  • format_on_save — memicu format setiap BufWritePre. timeout_ms = 2000 memberi waktu maksimal 2 detik sebelum memberi tahu pengguna, dan lsp_format = "fallback" berarti: jika tidak ada formatter yang cocok untuk filetype, coba gunakan formatting dari language server (LSP). Ini safety net yang membuat file jenis baru tetap terformat tanpa konfigurasi tambahan.
  • event = { "BufWritePre" } — plugin hanya dimuat ketika kalian menyimpan file, karena format-on-save adalah satu-satunya alasan plugin ini dibutuhkan. Startup time tetap minim.

Note

Perhatikan bahwa formatter dijalankan sebagai proses eksternal. conform.nvim tidak menulis kode format sendiri — ia memanggil binary seperti prettier atau stylua yang harus sudah ada di sistem. Di sinilah Mason (episode 15) berperan: :MasonInstall prettier stylua shfmt yamlfmt ruff meng-install semuanya dalam satu marketplace. Pastikan formatter terinstall sebelum mengharapkan format bekerja.

Peta Formatter Populer

BahasaFormatterSumber InstalasiCatatan
JS / TS / JSON / HTML / CSS / MarkdownprettierMason / npmSatu tool untuk banyak format; konfigurasi via .prettierrc
LuastyluaMasonStandar de-facto untuk Lua
Pythonruff_format + ruff_organize_importsMason (ruff)Jauh lebih cepat dari Black; juga bisa jadi linter
Python (alternatif)blackMason / pipPopuler, tapi lambat dibanding ruff
Gogofmt + goimportsGo toolchaingofmt bawaan Go; goimports menata import
Shell / BashshfmtMasonNormalisasi indentasi shell
YAMLyamlfmtMasonAlternatif yang lebih cepat dari prettier untuk YAML

Note

Perhatikan perbedaan dua kategori besar formatter. Formatter "universal" seperti Prettier dikonfigurasi lewat file di root project (.prettierrc, package.json) — ia mengikuti standar project, bukan standar editor. Sedangkan formatter per-bahasa seperti Stylua dan Gofmt bersifat deterministik (Gofmt bahkan terkenal tanpa opsi konfigurasi sama sekali — itulah filosofi "satu cara format untuk Go"). Ketika kalian berpindah antar project, formatter universal yang membuat editor kalian menyesuaikan diri, bukan sebaliknya.

Format Sekaligus vs Format-on-Save

Format-on-save sangat ideal, tapi ada situasi yang menuntut kendali manual:

  • File raksasa — format penuh membutuhkan waktu; kadang lebih aman memformat hanya range tertentu.
  • File orang lain — saat membuka file yang tidak pernah kalian format, memformat saat menyimpan akan mengubah seluruh file dan mengotori diff.
  • Perubahan kecil — kalian hanya ingin merapikan satu blok yang baru diedit.

conform.nvim menangani semua ini tanpa meninggalkan editor:

Command conform manual
:ConformInfo                      " cek formatter mana yang aktif untuk buffer ini
:Conform format                   " format buffer aktif (tanpa menyimpan)
:'<,'>Conform format              " format hanya range visual yang dipilih

Kombinasi dengan keymap <leader>f yang kita definisikan di spec membuat kalian punya dua mode: otomatis saat save untuk rutinitas, dan manual saat membutuhkan presisi. Kebijakan yang fleksibel ini biasanya lebih disukai tim daripada memaksa format-on-save tanpa kecuali.

Tip

Untuk tim yang menerapkan "don't format other people's code", pola yang banyak dipakai: nonaktifkan format-on-save secara global, aktifkan hanya untuk filetype tertentu via autocmd, atau biarkan manual <leader>f sebagai satu-satunya pemicu. Diskusikan kebijakan ini dengan tim dan tuangkan dalam konfigurasi bersama (dotfiles) agar semua developer konsisten.

Asynchronous Linting dengan nvim-lint

Jika formatter menulis ulang kode, linter membaca dan melaporkan. nvim-lint adalah runner linter asinkron yang ringan: ia tidak membuat UI sendiri, melainkan menyalurkan hasil linter ke sistem diagnostics bawaan Neovim — jendela yang sama yang dipakai LSP di episode 15.

lua/plugins/lint.lua
return {
  {
    "mfussenegger/nvim-lint",
    event = { "BufReadPost", "BufWritePost" },
    config = function()
      local lint = require("lint")
 
      lint.linters_by_ft = {
        javascript = { "eslint_d" },
        typescript = { "eslint_d" },
        javascriptreact = { "eslint_d" },
        typescriptreact = { "eslint_d" },
        sh = { "shellcheck" },
        python = { "ruff" },
        lua = { "luacheck" },
      }
 
      vim.api.nvim_create_autocmd({ "BufWritePost", "BufEnter" }, {
        callback = function()
          lint.try_lint()
        end,
      })
    end,
  },
}

Bedah logikanya:

  • linters_by_ft — peta filetype → linter. eslint_d adalah versi daemon dari ESLint yang jauh lebih cepat untuk re-run berulang; shellcheck untuk shell; ruff untuk Python (dua peran: formatter dan linter); luacheck untuk Lua.
  • autocmd dengan try_lint() — menjalankan linting saat file disimpan (BufWritePost) dan saat buffer dimasuki (BufEnter). try_lint() adalah fungsi idempotent: jika linter tidak tersedia atau sudah berjalan, ia tidak akan menduplikasi kerja.

Important

Kekuatan utama nvim-lint adalah asinkron. Linter dijalankan di luar event loop Neovim, sehingga mengetik tidak pernah tersendat — hasilnya muncul sebagai garis-garis warna di gutter (yang kita siapkan signcolumn di episode 7) tanpa membekukan editor. Ini kontras dengan menjalankan linter secara sinkron di terminal, yang memaksa kalian menunggu proses selesai.

Sekarang ada dua sumber diagnostic: LSP (episode 15) dan nvim-lint — keduanya masuk ke sistem yang sama. Karena itu, semua keymap navigasi diagnosis memakai API vim.diagnostic yang universal:

lua/config/keymaps.lua
local map = vim.keymap.set
 
map("n", "[d", vim.diagnostic.goto_prev, { desc = "Diagnostic sebelumnya" })
map("n", "]d", vim.diagnostic.goto_next, { desc = "Diagnostic berikutnya" })
map("n", "<leader>e", vim.diagnostic.open_float, { desc = "Detail diagnostic" })
map("n", "<leader>q", vim.diagnostic.setloclist, { desc = "Semua diagnostic ke list" })

Alur kerja diagnostic yang produktif:

  1. Tekan ]d untuk melompat ke error berikutnya — kursor langsung pindah, tidak perlu scroll manual.
  2. Tekan <leader>e untuk membuka popup detail: pesan error, kode, dan hint.
  3. Jika banyak error tersebar, tekan <leader>q untuk mengirim semuanya ke location list dan melihatnya sebagai daftar terfilter di bawah.

Perbedaan dua "daftar" di Vim/Neovim yang sering membingungkan:

KonsepPerintahCakupanKapan Dipakai
Quickfix list:copen, :cnext, :cprevSeluruh projectHasil grep (:grep), build errors, kompilasi
Location list:lopen, :lnext, :lprevBuffer/window aktifDiagnostics buffer (kasus kita), results per-window

Tip

Karena setloclist mengisi location list (scoped per-window), daftar diagnostics tidak tercampur dengan quickfix yang dipakai proses lain seperti grep. Jika kalian ingin semuanya masuk ke quickfix global, ganti dengan vim.diagnostic.setqflist — pilihan keduanya valid, tinggal sesuaikan dengan alur kerja tim kalian.

Kesalahan Umum Seputar Formatting & Linting

KesalahanGejalaSolusi
Formatter tidak terinstallError conform: no formatter configured for ... atau proses gagal:MasonInstall prettier stylua shfmt ruff yamlfmt; cek :ConformInfo
Prettier tidak menemukan konfigurasiFormat menghasilkan gaya tak terdugaSediakan .prettierrc di root project; Prettier membaca konfigurasi dari project (resolusi bottom-up)
Format ganda / bentrok dengan LSPKode diformat dua kali dengan hasil berbedaAtur lsp_format = "never" jika formatter konform sudah pasti; biarkan fallback hanya saat tidak ada formatter
Linter belum terinstallDiagnostic tidak pernah munculInstall via Mason (shellcheck, ruff, luacheck, eslint_d); cek :LintInfo
eslint vs eslint_d kebingunganLinter tidak jalan di project dengan ESLint version sendiriPastikan eslint_d diinstall dan project punya konfigurasi ESLint
Quickfix vs location list tertukarDaftar hasil tidak ada di tempat yang diharapkanPahami scope keduanya; setqflist untuk project-wide, setloclist untuk buffer
Format-on-save memakan waktu lamaMenyimpan file terasa lambatNaikkan timeout_ms atau jadikan formatter manual (<leader>f) untuk file raksasa
Prettier mencegah save karena error configFile tidak tersimpanPerbaiki .prettierrc; atau gunakan prettierd sebagai fallback yang lebih toleran
Diff PR penuh perubahan formatReviewer keberatan dengan diff yang tidak relevanPastikan format-on-save aktif sebelum menulis kode; jangan format ulang file lama tanpa koordinasi tim

Warning

Satu jebakan yang sering membuat developer jengkel: Prettier dan ESLint kadang merekomendasikan gaya yang bertentangan (misal soal semicolon atau trailing comma). Jika keduanya diaktifkan sekaligus, diff bisa berubah-ubah. Solusi standar tim modern: biarkan Prettier (formatter) yang menangani gaya, dan matikan semua aturan format di ESLint (eslint-plugin-prettier atau meng-set aturan format ke off). Prinsipnya: satu tool untuk satu tanggung jawab.

Penutup

Pada episode 17 ini kita telah membangun dua pilar kualitas kode: conform.nvim untuk format-on-save yang deterministik dengan peta formatter per bahasa (Prettier, Stylua, Ruff, Gofmt, shfmt), dan nvim-lint untuk static analysis asinkron (ESLint, ShellCheck, Ruff, Luacheck) yang menyatu dengan sistem diagnostics Neovim. Kita juga menguasai navigasi diagnostics ([d, ]d, <leader>e, <leader>q) serta perbedaan quickfix vs location list.

Kode kalian kini tidak hanya lengkap dan benar secara semantik, tapi juga konsisten secara gaya dan bersih secara kualitas — tiga standar sekaligus yang biasanya membutuhkan tiga tools terpisah di IDE komersial.

Masih ada satu dimensi yang belum kita sentuh dalam perjalanan ini: kolaborasi. Semua pekerjaan di editor kita hingga saat ini bersifat individual. Di dunia nyata, kode adalah produk tim yang hidup dalam version control.

Di episode 18 selanjutnya, kita akan membahas Integrasi Git di Dalam Neovim dengan gitsigns.nvim dan diffview.nvim — melihat perubahan kode langsung di gutter, blame inline, stage/reset hunk, hingga resolusi merge conflict tanpa meninggalkan editor. Pastikan tetap semangat!

Belajar Neovim - Code Formatting & Linting (conform.nvim & nvim-lint) | Belajar Neovim