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.
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
Plug 'mfussenegger/nvim-lint'use 'mfussenegger/nvim-lint'Configure the linters you want to run per file type. For example:
require('lint').linters_by_ft = {
markdown = {'vale'},
}
To get the filetype of a buffer you can run := vim.bo.filetype.
The filetype can also be a compound filetype. For example, if you have a buffer
with a filetype like yaml.ghaction, you can use either ghaction, yaml or
the full yaml.ghaction as key in the linters_by_ft table and the linter
will be picked up in that buffer. This is useful for linters like
actionlint in combination with vim.filetype patterns like
[".*/.github/workflows/.*%.yml"] = "yaml.ghaction",
Then setup a autocmd to trigger linting. For example:
au BufWritePost * lua require('lint').try_lint()
or with Lua auto commands:
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 |
| cmake-lint | cmake_lint |
| codespell | codespell |
| commitlint | commitlint |
| cppcheck | cppcheck |
| cpplint | cpplint |
| credo | credo |
| cspell | cspell |
| cue | cue |
| curlylint | curlylint |
| dash | dash |
| dclint | dclint |
| 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 |
| fieldalignment | fieldalignment |
| fish | fish |
| Flake8 | flake8 |
| flawfinder | flawfinder |
| fortitude | fortitude |
| fsharplint | fsharplint |
| 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 |
| json5 | json5 |
| jsonlint | jsonlint |
| json.tool | json_tool |
| ksh | ksh |
| ktlint | ktlint |
| lacheck | lacheck |
| Languagetool | languagetool |
| lslint | lslint |
| 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 |
| tofu | tofu |
| oxlint | oxlint |
| perlcritic | perlcritic |
| perlimports | perlimports |
| phpcs | phpcs |
| phpinsights | phpinsights |
| phpmd | phpmd |
| php | php |
| phpstan | phpstan |
| pmd | pmd |
| 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 |
| redocly | redolcy |
| 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 |
| sqruff | sqruff |
| 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 |
| ts-standard | ts-standard |
| twig-cs-fixer | twig-cs-fixer |
| 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 |
| zizmor | zizmor |
| zlint | zlint |
| 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:
outputbufnrlinter_cwdThe 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, from an errorformat
or for SARIF using the functions in the lint.parser module:
parser = require("lint.parser").for_sarif()
The function takes an optional argument:
skeleton: Default values for the diagnosticsparser = 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:
:help lua-patterns.:help vim.lpeg.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:
lnumend_lnumcolend_colmessagefileseveritycodeThe 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 to lnum. Defaults to 0end_lnum_offset: Added to end_lnum. Defaults to 0end_col_offset: offset added to end_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',
'-'
}
Some linters are defined as function for lazy evaluation of some properties. In this case, you need to wrap them like this:
local original = require("lint").linters.terraform_validate
require("lint").linters.terraform_validate = function()
local linter = original()
linter.cmd = "my_custom"
return linter
end
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/
API docs is generated using vimcats:
vimcats -t -f lua/lint.lua lua/lint/parser.lua > doc/lint.txt