nvim/.config/nvim/lua/plugins/coding/linter.lua

-- nvim/.config/nvim/lua/plugins/coding/linter.lua
-- nvim-lint — replaces null-ls for diagnostics.

return {
  "mfussenegger/nvim-lint",
  event = { "BufReadPost", "BufNewFile", "BufWritePost" },
  config = function()
    local lint = require("lint")

    lint.linters_by_ft = {
      sh         = { "shellcheck" },
      bash       = { "shellcheck" },
      zsh        = { "shellcheck" },
      python     = { "ruff" },
      dockerfile = { "hadolint" },
      markdown   = { "markdownlint" },
      yaml       = { "yamllint" },
    }

    -- Run on save + a few interactive events, debounced.
    local timer = vim.uv.new_timer()
    local function try_lint()
      timer:stop()
      timer:start(200, 0, vim.schedule_wrap(function()
        lint.try_lint()
      end))
    end

    vim.api.nvim_create_autocmd({ "BufWritePost", "BufReadPost", "InsertLeave" }, {
      group = vim.api.nvim_create_augroup("user_lint", { clear = true }),
      callback = try_lint,
    })

    vim.keymap.set("n", "<leader>cl", function()
      lint.try_lint()
    end, { desc = "run linters" })

    -- shellcheck: mark bash even for .sh files without a shebang.
    lint.linters.shellcheck.args = { "--shell=bash", "--format=json", "-" }
  end,
}