Skip to content

Linting

Blak uses mfussenegger/nvim-lint to run standalone linters — anything that isn’t already an LSP. It fires on the events in lint.events (default: BufWritePost, BufReadPost, InsertLeave).

Spec: lua/blak/plugins/formatting.lua (formatting and linting share a file). The runner itself lives in lua/blak/core/linting.lua.

Defaults

lint = {
events = { "BufWritePost", "BufReadPost", "InsertLeave" },
linters_by_ft = {},
}

No linters are configured in core. Linters arrive via language extras:

ExtraAdds
lang.pythonruff for python
lang.gogolangcilint for go
lang.markdownmarkdownlint for markdown
lang.bashshellcheck for sh/bash
lang.dockerhadolint for dockerfile
lang.terraformtflint for terraform

lang.python-pro uses Ruff’s native LSP diagnostics and code actions instead of adding a separate nvim-lint entry, so it avoids duplicate Ruff diagnostics. The TypeScript extras do the same with ESLint: they enable the eslint language server rather than running eslint_d through nvim-lint. See Using eslint_d anyway if you want the standalone daemon back.

Adding a linter

-- ~/.config/blak/lua/blak/user.lua
return {
lint = {
linters_by_ft = {
sh = { "shellcheck" },
dockerfile = { "hadolint" },
},
},
mason = {
ensure_installed = { "shellcheck", "hadolint" },
},
}

Or wrap it in an extra so it’s reversible.

Tuning when it runs

return {
lint = {
-- Only on write, not on every InsertLeave or BufReadPost
events = { "BufWritePost" },
},
}

Customizing a linter

nvim-lint exposes each linter as require("lint").linters.<name>. Tweak in user.lua:

vim.schedule(function()
require("lint").linters.shellcheck.args = {
"--severity=warning",
"--shell=bash",
"-",
}
end)

Using eslint_d anyway

The TypeScript extras rely on the eslint language server, so nothing runs eslint_d by default. To add it back, list it for the filetypes you want and install it through Mason:

return {
extras = {
enabled = { "lang.typescript" },
},
lint = {
linters_by_ft = {
javascript = { "eslint_d" },
javascriptreact = { "eslint_d" },
typescript = { "eslint_d" },
typescriptreact = { "eslint_d" },
},
},
mason = {
ensure_installed = { "eslint_d" },
},
}

Expect ESLint diagnostics from both sources unless you also disable the eslint language server.

Failure handling

Blak resolves each linter’s command before running it. A linter that isn’t installed (shellcheck missing from $PATH, say) is skipped — no Error running shellcheck: ENOENT on every save, and no diagnostics from that linter.

When a linter does run but writes something Blak can’t parse — eslint_d printing a config resolution error to stdout is the common case — the message is reported once as a warning instead of being attached to line 1 of your buffer as a fake diagnostic.

To see what’s actually wired up and what’s missing:

:checkhealth blak

The Linters section lists every configured linter and whether its binary was found. Run :BlakToolsInstall after adding a Mason-installable linter, or install it yourself.

Inspecting

:lua print(vim.inspect(require("lint").linters_by_ft))
:lua require("lint").try_lint() " trigger now
:lua require("lint").get_running() " what's running