/nvim-lint

An asynchronous linter plugin for Neovim complementary to the built-in Language Server Protocol support.

Primary LanguageLuaGNU General Public License v3.0GPL-3.0

nvim-lint

An asynchronous linter plugin for Neovim (>= 0.6.0) complementary to the built-in Language Server Protocol support.

Motivation & Goals

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.

Installation

  • Requires Neovim >= 0.6.0
  • 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'

Usage

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 (requires 0.7):

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.

Available Linters

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
ansible-lint ansible_lint
bandit bandit
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
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
DirectX Shader Compiler dxc
djlint djlint
dotenv-linter dotenv_linter
editorconfig-checker editorconfig-checker
erb-lint erb_lint
ESLint eslint
eslint_d eslint_d
fennel fennel
fish fish
Flake8 flake8
flawfinder flawfinder
gdlint (gdtoolkit) gdlint
GHDL ghdl
gitlint gitlint
glslc glslc
Golangci-lint golangcilint
hadolint hadolint
hlint hlint
htmlhint htmlhint
HTML Tidy tidy
Inko inko
janet janet
joker joker
jshint jshint
jsonlint jsonlint
ktlint ktlint
lacheck lacheck
Languagetool languagetool
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
RPM rpmspec
rstcheck rstcheck
rstlint rstlint
RuboCop rubocop
Ruby ruby
Ruff ruff
salt-lint saltlint
Selene selene
ShellCheck shellcheck
snyk snyk_iac
Solhint solhint
sqlfluff sqlfluff
standardjs standardjs
StandardRB standardrb
statix check statix
stylelint stylelint
SwiftLint swiftlint
systemdlint systemdlint
tflint tflint
tfsec tfsec
tlint tlint
trivy trivy
typos typos
Vala vala_lint
Vale vale
Verilator verilator
vint vint
vulture vulture
woke woke
write-good write_good
yamllint yamllint
zsh zsh

Custom Linters

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:

from_errorformat

parser = require('lint.parser').from_errorformat(errorformat)

The function takes two arguments: errorformat and skeleton (optional).

from_pattern

parser = require('lint.parser').from_pattern(pattern, groups, severity_map, defaults, opts)

The function allows to parse the linter's output using a Lua regular expression pattern.

  • pattern: The regular expression pattern applied on each line of the output
  • groups: The groups specified by 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' }
  • severity: 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,
}
  • defaults: The defaults diagnostic values
defaults = {["source"] = "mylint-name"}
  • opts: Additional options

    • lnum_offset: Added to lnum. Defaults to 0
    • end_lnum_offset: Added to end_lnum. Defaults to 0
    • end_col_offset: offset added to end_col. Defaults to -1, assuming that the end-column position is exclusive.

Customize built-in linters

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)

Display configuration

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)

Get the current running linters for your buffer

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

Alternatives

Development ☢️

Run tests

Running tests requires busted.

See neorocks or Using Neovim as Lua interpreter with Luarocks for installation instructions.

busted tests/