An asynchronous linter plugin for Neovim (>= 0.9.5) complementary to the built-in Language Server Protocol support.
With ale we already got an asynchronous linter, why write yet another one?
Because ale also includes its own language server client.
nvim-lint
instead has a more narrow scope: It spawns linters, parses their
output, and reports the results via the vim.diagnostic
module.
nvim-lint
complements the built-in language server client for languages where
there are no language servers, or where standalone linters provide better
results.
- Requires Neovim >= 0.9.5
nvim-lint
is a regular plugin and can be installed via the:h packages
mechanism or via a plugin manager.
For example:
git clone \
https://github.com/mfussenegger/nvim-lint.git
~/.config/nvim/pack/plugins/start/nvim-lint
- If using vim-plug:
Plug 'mfussenegger/nvim-lint'
- If using packer.nvim:
use 'mfussenegger/nvim-lint'
Configure the linters you want to run per file type. For example:
require('lint').linters_by_ft = {
markdown = {'vale'},
}
Then setup a autocmd to trigger linting. For example:
au BufWritePost * lua require('lint').try_lint()
or with Lua autocmds:
vim.api.nvim_create_autocmd({ "BufWritePost" }, {
callback = function()
-- try_lint without arguments runs the linters defined in `linters_by_ft`
-- for the current filetype
require("lint").try_lint()
-- You can call `try_lint` with a linter name or a list of names to always
-- run specific linters, independent of the `linters_by_ft` configuration
require("lint").try_lint("cspell")
end,
})
Some linters require a file to be saved to disk, others support linting stdin
input. For such linters you could also define a more aggressive autocmd, for
example on the InsertLeave
or TextChanged
events.
If you want to customize how the diagnostics are displayed, read :help vim.diagnostic.config
.
There is a generic linter called compiler
that uses the makeprg
and
errorformat
options of the current buffer.
Other dedicated linters that are built-in are:
Tool | Linter name |
---|---|
Set via makeprg |
compiler |
actionlint | actionlint |
alex | alex |
ameba | ameba |
ansible-lint | ansible_lint |
bandit | bandit |
bash | bash |
bean-check | bean_check |
biomejs | biomejs |
blocklint | blocklint |
buf_lint | buf_lint |
buildifier | buildifier |
cfn-lint | cfn_lint |
cfn_nag | cfn_nag |
checkmake | checkmake |
checkpatch.pl | checkpatch |
checkstyle | checkstyle |
chktex | chktex |
clang-tidy | clangtidy |
clazy | clazy |
clippy | clippy |
clj-kondo | clj-kondo |
cmakelint | cmakelint |
codespell | codespell |
commitlint | commitlint |
cppcheck | cppcheck |
cpplint | cpplint |
credo | credo |
cspell | cspell |
cue | cue |
curlylint | curlylint |
dash | dash |
deadnix | deadnix |
deno | deno |
dmypy | dmypy |
DirectX Shader Compiler | dxc |
djlint | djlint |
dotenv-linter | dotenv_linter |
editorconfig-checker | editorconfig-checker |
erb-lint | erb_lint |
ESLint | eslint |
eslint_d | eslint_d |
eugene | eugene |
fennel | fennel |
fish | fish |
Flake8 | flake8 |
flawfinder | flawfinder |
gawk | gawk |
gdlint (gdtoolkit) | gdlint |
GHDL | ghdl |
gitlint | gitlint |
glslc | glslc |
Golangci-lint | golangcilint |
hadolint | hadolint |
hledger | hledger |
hlint | hlint |
htmlhint | htmlhint |
HTML Tidy | tidy |
Inko | inko |
janet | janet |
joker | joker |
jshint | jshint |
jsonlint | jsonlint |
ksh | ksh |
ktlint | ktlint |
lacheck | lacheck |
Languagetool | languagetool |
luac | luac |
luacheck | luacheck |
markdownlint | markdownlint |
markdownlint-cli2 | markdownlint-cli2 |
markuplint | markuplint |
mlint | mlint |
Mypy | mypy |
Nagelfar | nagelfar |
Nix | nix |
npm-groovy-lint | npm-groovy-lint |
oelint-adv | oelint-adv |
opa_check | opa_check |
oxlint | oxlint |
perlcritic | perlcritic |
perlimports | perlimports |
phpcs | phpcs |
phpinsights | phpinsights |
phpmd | phpmd |
php | php |
phpstan | phpstan |
ponyc | pony |
prisma-lint | prisma-lint |
proselint | proselint |
protolint | protolint |
psalm | psalm |
puppet-lint | puppet-lint |
pycodestyle | pycodestyle |
pydocstyle | pydocstyle |
Pylint | pylint |
pyproject-flake8 | pflake8 |
quick-lint-js | quick-lint-js |
regal | regal |
Revive | revive |
rflint | rflint |
robocop | robocop |
rpmlint | rpmlint |
RPM | rpmspec |
rstcheck | rstcheck |
rstlint | rstlint |
RuboCop | rubocop |
Ruby | ruby |
Ruff | ruff |
salt-lint | saltlint |
Selene | selene |
ShellCheck | shellcheck |
slang | slang |
Snakemake | snakemake |
snyk | snyk_iac |
Solhint | solhint |
Spectral | spectral |
sphinx-lint | sphinx-lint |
sqlfluff | sqlfluff |
standardjs | standardjs |
StandardRB | standardrb |
statix check | statix |
stylelint | stylelint |
svlint | svlint |
SwiftLint | swiftlint |
systemd-analyze | systemd-analyze |
systemdlint | systemdlint |
tflint | tflint |
tfsec | tfsec |
tlint | tlint |
trivy | trivy |
typos | typos |
Vala | vala_lint |
Vale | vale |
Verilator | verilator |
vint | vint |
VSG | vsg |
vulture | vulture |
woke | woke |
write-good | write_good |
yamllint | yamllint |
yq | yq |
zsh | zsh |
You can register custom linters by adding them to the linters
table, but
please consider contributing a linter if it is missing.
require('lint').linters.your_linter_name = {
cmd = 'linter_cmd',
stdin = true, -- or false if it doesn't support content input via stdin. In that case the filename is automatically added to the arguments.
append_fname = true, -- Automatically append the file name to `args` if `stdin = false` (default: true)
args = {}, -- list of arguments. Can contain functions with zero arguments that will be evaluated once the linter is used.
stream = nil, -- ('stdout' | 'stderr' | 'both') configure the stream to which the linter outputs the linting result.
ignore_exitcode = false, -- set this to true if the linter exits with a code != 0 and that's considered normal.
env = nil, -- custom environment table to use with the external process. Note that this replaces the *entire* environment, it is not additive.
parser = your_parse_function
}
Instead of declaring the linter as a table, you can also declare it as a function which returns the linter table in case you want to dynamically generate some of the properties.
your_parse_function
can be a function which takes three arguments:
output
bufnr
linter_cwd
The output
is the output generated by the linter command.
The function must return a list of diagnostics as specified in :help diagnostic-structure
.
You can override the environment that the linting process runs in by setting
the env
key, e.g.
env = { ["FOO"] = "bar" }
Note that this completely overrides the environment, it does not add new
environment variables. The one exception is that the PATH
variable will be
preserved if it is not explicitly set.
You can generate a parse function from a Lua pattern or from an errorformat
using the function in the lint.parser
module:
parser = require('lint.parser').from_errorformat(errorformat)
The function takes two arguments: errorformat
and skeleton
(optional).
Creates a parser function from a pattern.
parser = require('lint.parser').from_pattern(pattern, groups, severity_map, defaults, opts)
The function allows to parse the linter's output using a pattern which can be either:
- A Lua pattern. See
:help lua-patterns
. - A LPEG pattern object. See
:help vim.lpeg
. - A function (
fun(line: string):string[]
). It takes one parameter - a line from the linter output and must return a string array with the matches. The array should be empty if there was no match.
The groups specify the result format of the pattern. Available groups:
lnum
end_lnum
col
end_col
message
file
severity
code
The order of the groups must match the order of the captures within the pattern. An example:
local pattern = '[^:]+:(%d+):(%d+):(%w+):(.+)'
local groups = { 'lnum', 'col', 'code', 'message' }
The captures in the pattern correspond to the group at the same position.
A mapping from severity codes to diagnostic codes
default_severity = {
['error'] = vim.diagnostic.severity.ERROR,
['warning'] = vim.diagnostic.severity.WARN,
['information'] = vim.diagnostic.severity.INFO,
['hint'] = vim.diagnostic.severity.HINT,
}
The defaults diagnostic values
defaults = {["source"] = "mylint-name"}
Additional options
lnum_offset
: Added tolnum
. Defaults to 0end_lnum_offset
: Added toend_lnum
. Defaults to 0end_col_offset
: offset added toend_col
. Defaults to-1
, assuming that the end-column position is exclusive.
You can import a linter and modify its properties. An example:
local phpcs = require('lint').linters.phpcs
phpcs.args = {
'-q',
-- <- Add a new parameter here
'--report=json',
'-'
}
You can also post-process the diagnostics produced by a linter by wrapping it.
For example, to change the severity of all diagnostics created by cspell
:
local lint = require("lint")
lint.linters.cspell = require("lint.util").wrap(lint.linters.cspell, function(diagnostic)
diagnostic.severity = vim.diagnostic.severity.HINT
return diagnostic
end)
See :help vim.diagnostic.config
.
If you want to have different settings per linter, you can get the namespace
for a linter via require("lint").get_namespace("linter_name")
. An example:
local ns = require("lint").get_namespace("my_linter_name")
vim.diagnostic.config({ virtual_text = true }, ns)
You can see which linters are running with require("lint").get_running()
.
To include the running linters in the status line you could format them like this:
local lint_progress = function()
local linters = require("lint").get_running()
if #linters == 0 then
return ""
end
return " " .. table.concat(linters, ", ")
end
Running tests requires busted.
See neorocks or Using Neovim as Lua interpreter with Luarocks for installation instructions.
busted tests/