test: lint EXX error codes #8155

Problem:
- Choosing a new EXX error code is tedious.
- It's possible to accidentally use an EXX error code for different
  purposes.

Solution:
Add a lint check which requires EXX error codes to have a :help tag.
This also avoids duplicates because `make doc` does `:helptags ++t doc`
which fails if duplicates are found.
This commit is contained in:
Justin M. Keyes
2026-04-16 10:48:11 -04:00
committed by GitHub
parent 42e9d8dfd1
commit bc6d946cca
23 changed files with 322 additions and 52 deletions
+4 -1
View File
@@ -27,5 +27,8 @@ jobs:
git diff --color --exit-code
fi
- name: Validate docs
- name: lintdoc
run: make lintdoc
- name: linterrcodes
run: make linterrcodes
+5
View File
@@ -288,6 +288,11 @@ add_custom_target(lintcommit
COMMAND $<TARGET_FILE:nvim_bin> --clean -l ${PROJECT_SOURCE_DIR}/scripts/lintcommit.lua main)
add_dependencies(lintcommit nvim_bin)
add_custom_target(linterrcodes
COMMAND $<TARGET_FILE:nvim_bin> --clean -l ${PROJECT_SOURCE_DIR}/scripts/linterrcodes.lua
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
add_dependencies(linterrcodes nvim_bin)
add_custom_target(lint)
add_dependencies(lint lintc lintlua lintsh lintquery)
+1 -1
View File
@@ -147,7 +147,7 @@ functionaltest-lua: | nvim
$(CMAKE) --build build --target functionaltest
FORMAT=formatc formatlua formatquery format
LINT=lintlua lintsh lintc clang-analyzer lintcommit lintdoc lintdocurls lint luals lintquery
LINT=lintlua lintsh lintc clang-analyzer lintcommit lintdoc lintdocurls lint luals lintquery linterrcodes
TEST=functionaltest unittest
generated-sources benchmark $(FORMAT) $(LINT) $(TEST) doc: | build/.ran-cmake
$(CMAKE) --build build --target $@
+1 -1
View File
@@ -408,7 +408,7 @@ after adding them, the returned |extmark| id can be used. >lua
See also |vim.hl.range()|.
==============================================================================
Floating windows *api-floatwin* *floating-windows*
Floating windows *api-floatwin* *floating-windows* *E5601* *E5602*
Floating windows ("floats") are displayed on top of normal windows. This is
useful to implement simple widgets, such as tooltips displayed next to the
+3 -3
View File
@@ -20,7 +20,7 @@ files matching `*.c`. You can also use autocommands to implement advanced
features, such as editing compressed files (see |gzip-example|). The usual
place to put autocommands is in your vimrc file.
*E203* *E204* *E143* *E855* *E937* *E952*
*E203* *E204* *E143* *E855* *E937* *E952* *E1312*
WARNING: Using autocommands is very powerful, and may lead to unexpected side
effects. Be careful not to destroy your text.
- It's a good idea to do some testing on an expendable copy of a file first.
@@ -201,7 +201,7 @@ was last defined. Example: >
See |:verbose-cmd| for more information.
==============================================================================
5. Events *autocmd-events* *E215* *E216*
5. Events *autocmd-events* *E215* *E216* *E1155*
You can specify a comma-separated list of event names. No white space can be
used in this list. The command applies to all the events in the list.
@@ -711,7 +711,7 @@ FocusGained Nvim got focus.
*FocusLost*
FocusLost Nvim lost focus. Also (potentially) when
a GUI dialog pops up.
*FuncUndefined*
*FuncUndefined* *E454*
FuncUndefined When a user function is used but it isn't
defined. Useful for defining a function only
when it's used. The pattern is matched
+1 -1
View File
@@ -42,7 +42,7 @@ Channels opened by Vimscript functions operate with raw bytes by default. For
a job channel using RPC, bytes can still be read over its stderr. Similarly,
only bytes can be written to Nvim's own stderr.
*channel-callback*
*channel-callback* *E904* *E906* *E921* *E5407*
- on_stdout({chan-id}, {data}, {name}) *on_stdout*
- on_stderr({chan-id}, {data}, {name}) *on_stderr*
- on_stdin({chan-id}, {data}, {name}) *on_stdin*
+1 -1
View File
@@ -1184,7 +1184,7 @@ variable, it needs to be preceded by a backslash. Therefore you need to use
Also see |`=|.
==============================================================================
7. Command-line window *cmdline-window* *cmdwin*
7. Command-line window *cmdline-window* *cmdwin* *E1292*
*command-line-window*
In the command-line window the command line can be edited just like editing
text in any window. It is a special kind of window, because you cannot leave
+35 -21
View File
@@ -80,8 +80,8 @@ include the kitchen sink... but it's good for plumbing."
==============================================================================
Developer guidelines *dev-guidelines*
PROVIDERS *dev-provider*
------------------------------------------------------------------------------
Providers *dev-provider*
A primary goal of Nvim is to allow extension of the editor without special
knowledge in the core. Some core functions are delegated to "providers"
@@ -123,7 +123,8 @@ Sometimes a GUI or other application may want to force a provider to
:runtime autoload/provider/clipboard.vim
DOCUMENTATION *dev-doc*
------------------------------------------------------------------------------
Documentation *dev-doc*
- "Just say it". Avoid mushy, colloquial phrasing in all documentation
(docstrings, user manual, website materials, newsletters, …). Don't mince
@@ -165,7 +166,7 @@ DOCUMENTATION *dev-doc*
`treesitter.txt`
- Otherwise, add them to `lua.txt`
Documentation format ~
DOCUMENTATION FORMAT
For Nvim-owned docs, use the following strict subset of "vimdoc" to ensure
the help doc renders nicely in other formats (such as HTML:
@@ -184,7 +185,7 @@ Strict "vimdoc" subset:
- Parameters and fields are documented as `{foo}`.
- Optional parameters and fields are documented as `{foo}?`.
C docstrings ~
C DOCSTRINGS
Nvim API documentation lives in the source code, as docstrings (doc
comments) on the function definitions. The |api| :help is generated
@@ -231,8 +232,7 @@ in src/nvim/api/win_config.c like this: >
/// @return Window handle, or 0 on error
Lua docstrings ~
*dev-lua-doc*
LUA DOCSTRINGS *dev-lua-doc*
Lua documentation lives in the source code, as docstrings on the function
definitions. The |lua-vim| :help is generated from the docstrings.
@@ -300,7 +300,19 @@ vim.paste in runtime/lua/vim/_core/editor.lua like this: >
--- @returns false if client should cancel the paste.
STDLIB DESIGN GUIDELINES *dev-lua*
EXX ERROR CODES *dev-error-codes*
To choose a new "EXX" error code (e.g. |E5555|), just print a message with
a new EXX number: >
emsg(_("E996: Invalid thing"));
<
You can confirm that the new error code isn't used by running: >
make linterrcodes
<
The `linterrcodes.lua` check requires the new error code to have a help tag.
------------------------------------------------------------------------------
Stdlib design *dev-lua*
See also |dev-naming|.
@@ -343,8 +355,8 @@ preference):
5. `vim.notify` (sometimes with optional `opts.silent` (async, visitors))
- High-level / application-level messages. End-user invokes these directly.
*dev-patterns*
Interface conventions ~
INTERFACE CONVENTIONS *dev-patterns*
Where applicable, these patterns apply to _both_ Lua and the API:
@@ -390,7 +402,8 @@ Where applicable, these patterns apply to _both_ Lua and the API:
end)
<
API DESIGN GUIDELINES *dev-api*
------------------------------------------------------------------------------
API design *dev-api*
See also |dev-naming|.
@@ -409,7 +422,7 @@ See also |dev-naming|.
- Avoid functions that depend on cursor position, current buffer, etc. Instead
the function should take a position parameter, buffer parameter, etc.
Where things go ~
WHERE THINGS GO
- API (libnvim/RPC): exposes low-level internals, or fundamental things (such
as `nvim_exec_lua()`) needed by clients or C consumers.
@@ -425,8 +438,6 @@ burden. Discoverability encourages code re-use and likewise avoids redundant,
overlapping mechanisms, which reduces code surface-area, and thereby minimizes
bugs...
Naming conventions ~
In general, look for precedent when choosing a name, that is, look at existing
(non-deprecated) functions. In particular, see below...
@@ -600,7 +611,8 @@ recommended (compare these 12(!) functions to the above 3 functions): >
nvim_win_get_ns(…)
nvim_tabpage_get_ns(…)
API-CLIENT *dev-api-client*
------------------------------------------------------------------------------
API client *dev-api-client*
*api-client*
API clients wrap the Nvim |API| to provide idiomatic "SDKs" for their
@@ -615,7 +627,7 @@ These clients can be considered the "reference implementation" for API clients:
- https://github.com/neovim/node-client
- https://github.com/neovim/pynvim
Standard Features ~
STANDARD FEATURES
- API clients exist to hide msgpack-rpc details. The wrappers can be
automatically generated by reading the |api-metadata| from Nvim. |api-mapping|
@@ -625,7 +637,7 @@ Standard Features ~
- Clients should handle |nvim_error_event| notifications, which will be sent
if an async request to nvim was rejected or caused an error.
Package Naming ~
PACKAGE NAMING
API client packages should NOT be named something ambiguous like "neovim" or
"python-client". Use "nvim" as a prefix/suffix to some other identifier
@@ -641,7 +653,7 @@ Examples of API-client package names:
- ❌ NO: python-client
- ❌ NO: neovim_
API client implementation guidelines ~
API CLIENT IMPLEMENTATION GUIDELINES
- Separate the transport layer from the rest of the library. |rpc-connecting|
- Use a MessagePack library that implements at least version 5 of the
@@ -664,6 +676,7 @@ API client implementation guidelines ~
https://github.com/msgpack-rpc/msgpack-rpc
------------------------------------------------------------------------------
EXTERNAL UI *dev-ui*
External UIs should be aware of the |api-contract|. In particular, future
@@ -671,7 +684,7 @@ versions of Nvim may add new items to existing events. The API is strongly
backwards-compatible, but clients must not break if new (optional) fields are
added to existing events.
Standard Features ~
STANDARD FEATURES
External UIs are expected to implement these common features:
@@ -695,8 +708,9 @@ External UIs are expected to implement these common features:
- Handle the "restart" UI event so that |:restart| works.
- Detect capslock and show an indicator if capslock is active.
Multigrid UI ~
*dev-ui-multigrid*
MULTIGRID UI *dev-ui-multigrid*
- A multigrid UI should display floating windows using one of the following
methods, using the `win_float_pos` |ui-multigrid| event. Different methods
can be selected for each window as needed.
+1 -1
View File
@@ -325,7 +325,7 @@ name or a part of a buffer name. Examples:
diff mode (e.g., "file.c.v2")
==============================================================================
5. Diff anchors *diff-anchors*
5. Diff anchors *diff-anchors* *E1549*
Diff anchors allow you to control where the diff algorithm aligns and
synchronize text across files. Each anchor matches each other in each file,
+1 -1
View File
@@ -456,7 +456,7 @@ Creating New Menus *creating-menus*
*:me* *:menu* *:noreme* *:noremenu*
*E330* *E327* *E331* *E336* *E333*
*E328* *E329* *E337* *E792*
*E328* *E329* *E337* *E792* *E1310*
To create a new menu item, use the ":menu" commands. They are mostly like
the ":map" set of commands (see |map-modes|), but the first argument is a menu
item name, given as a path of menus and submenus with a '.' between them,
+1 -1
View File
@@ -4,7 +4,7 @@
NVIM REFERENCE MANUAL by Thiago de Arruda
Nvim job control *job* *job-control*
Nvim job control *job* *job-control* *E901* *E903* *E947* *E948*
Job control is a way to perform multitasking in Nvim, so scripts can spawn and
control multiple processes without blocking the current Nvim instance.
+1 -1
View File
@@ -359,7 +359,7 @@ numbers. To disambiguate these cases, we define:
3. Table with string keys, at least one of which contains NUL byte, is also
considered to be a dictionary, but this time it is converted to
a |msgpack-special-map|.
*lua-special-tbl*
*lua-special-tbl* *E5100* *E5101* *E5102*
4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point
value:
- `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
+1 -1
View File
@@ -353,7 +353,7 @@ conversion needs to be done. These conversions are supported:
Try getting another iconv() implementation.
==============================================================================
Input with a keymap *mbyte-keymap*
Input with a keymap *mbyte-keymap* *E544*
When the keyboard doesn't produce the characters you want to enter in your
text, you can use the 'keymap' option. This will translate one or more
+4 -3
View File
@@ -889,9 +889,10 @@ Create or update a progress-message by calling |nvim_echo()| with
existing progress-message.
Events: ~
• msg_show |ui-messages| event is fired for ext-ui upon creation/update of a
progress-message
• Updating or creating a progress message also triggers the |Progress| autocommand.
• msg_show |ui-messages| event is fired for ext-ui upon creation/update of
a progress-message
• Updating or creating a progress message also triggers the |Progress|
autocommand.
Example: >lua
local progress = {
+1 -1
View File
@@ -6115,7 +6115,7 @@ A jump table for the options with a short description can be found at |Q_op|.
designated regions of the buffer are spellchecked in
this case.
*'spellsuggest'* *'sps'*
*'spellsuggest'* *'sps'* *E5700*
'spellsuggest' 'sps' string (default "best")
global
Disallowed in |modeline|. |no-modeline-option|
+2 -2
View File
@@ -12,7 +12,7 @@ explanations are in chapter 27 |usr_27.txt|.
Type |gO| to see the table of contents.
==============================================================================
1. Search commands *search-commands*
1. Search commands *search-commands* *E654*
*/*
/{pattern}[/]<CR> Search forward for the [count]'th occurrence of
@@ -401,7 +401,7 @@ prepend one of the following to the pattern:
You can also use the 'regexpengine' option to change the default.
*E864* *E868* *E874* *E875* *E876* *E877* *E878*
*E864* *E868* *E874* *E875* *E876* *E877* *E878* *E1273* *E1279*
If selecting the NFA engine and it runs into something that is not implemented
the pattern will not match. This is only useful when debugging Vim.
+1 -1
View File
@@ -359,7 +359,7 @@ If there is a file with exactly the same name as the ".spl" file but ending in
".sug", that file will be used for giving better suggestions. It isn't loaded
before suggestions are made to reduce memory use.
*E758* *E759* *E778* *E779* *E780* *E782*
*E758* *E759* *E778* *E779* *E780* *E782* *E5042*
When loading a spell file Vim checks that it is properly formatted. If you
get an error the file may be truncated, modified or intended for another Vim
version.
+2 -2
View File
@@ -1157,7 +1157,7 @@ running) you have additional options:
already set (registers, marks, |v:oldfiles|, etc.)
will be overwritten.
*:wsh* *:wshada* *E137*
*:wsh* *:wshada* *E137* *E138*
:wsh[ada][!] [file] Write to ShaDa file [file] (default: see above).
The information in the file is first read in to make
a merge between old and new info. When [!] is used,
@@ -1341,7 +1341,7 @@ exactly four MessagePack objects:
- `*` (Unknown) Any other entry type is allowed for compatibility
reasons, see |shada-compatibility|.
*E575* *E576*
*E574* *E575* *E576*
Errors in ShaDa file may have two types:
1. E575 for “logical” errors.
2. E576 for “critical” errors.
+2 -2
View File
@@ -37,7 +37,7 @@ instead of "s:" when the mapping is expanded outside of the script.
There are only script-local functions, no buffer-local or window-local
functions.
*:fu* *:function* *E128* *E129* *E123*
*:fu* *:function* *E128* *E129* *E123* *E1058* *E1068*
:fu[nction] List all functions and their arguments.
:fu[nction][!] {name} List function {name}, annotated with line numbers
@@ -206,7 +206,7 @@ See |:verbose-cmd| for more information.
*function-argument* *a:var*
An argument can be defined by giving its name. In the function this can then
be used as "a:name" ("a:" for argument).
*a:0* *a:1* *a:000* *E740* *...*
*a:0* *a:1* *a:000* *E740* *E1132* *...*
Up to 20 arguments can be given, separated by commas. After the named
arguments an argument "..." can be specified, which means that more arguments
may optionally be following. In the function the extra arguments can be used
+1 -1
View File
@@ -20,7 +20,7 @@ CTRL-L Clears and redraws the screen. The redraw may happen
|:nohlsearch| and updates diffs |:diffupdate|.
|default-mappings|
*:mod* *:mode*
*:mod* *:mode* *E359*
:mod[e] Clears and redraws the screen.
See also |nvim__redraw()|.
+8 -3
View File
@@ -14,7 +14,12 @@ Using expressions is introduced in chapter 41 of the user manual |usr_41.txt|.
1. Variables *variables*
1.1 Variable types ~
*E712* *E896* *E897* *E899*
*E712* *E896* *E897* *E899* *E908* *E909*
*E928* *E964* *E966* *E1023* *E1098* *E1174* *E1175*
*E1203* *E1206* *E1210* *E1211* *E1212* *E1219* *E1220*
*E1222* *E1225* *E1226* *E1238* *E1250* *E1252* *E1256*
*E1265* *E1297* *E5010* *E5050* *E5060* *E5070* *E5071*
*E5299* *E5300* *E5401* *E5420* *E6000*
There are seven types of variables:
*Number* *Integer*
@@ -1573,7 +1578,7 @@ See below |internal-variables|.
------------------------------------------------------------------------------
function call *expr-function* *E116* *E118* *E119* *E120*
function call *expr-function* *E116* *E118* *E119* *E120* *E130*
function(expr1, ...) function call
See below |functions|.
@@ -3733,7 +3738,7 @@ the output of `scriptnames` this code can be used: >
unlet scriptnames_output
==============================================================================
The sandbox *eval-sandbox* *sandbox*
The sandbox *eval-sandbox* *sandbox* *E523*
The 'foldexpr', 'formatexpr', 'includeexpr', 'indentexpr', 'statusline' and
'foldtext' options may be evaluated in a sandbox. This means that you are
+241
View File
@@ -0,0 +1,241 @@
-- Checks mismatches between "EXX" error codes (E123, E1234) defined in C sources and those
-- documented in `runtime/doc/*.txt`.
--
-- Usage: nvim -l scripts/linterrcodes.lua
--- Error codes allowed to appear in more than one place. Value is the exact expected
--- occurrence count. A mismatch (actual > or < expected) is reported, to avoid
--- accidental duplicates from slipping in.
--- @type table<string, integer>
local dup_allowed = {
E109 = 2,
E1098 = 2,
E110 = 2,
E112 = 2,
E114 = 2,
E115 = 2,
E1159 = 2,
E116 = 2,
E121 = 2,
E1502 = 4,
E151 = 2,
E155 = 3,
E158 = 2,
E170 = 2,
E173 = 2,
E180 = 2,
E212 = 2,
E216 = 2,
E298 = 3,
E303 = 3,
E312 = 2,
E317 = 4,
E319 = 2,
E423 = 3,
E474 = 52,
E475 = 6,
E482 = 3,
E484 = 2,
E488 = 2,
E5000 = 2,
E5001 = 2,
E5002 = 2,
E5009 = 3,
E502 = 2,
E503 = 3,
E504 = 2,
E505 = 2,
E509 = 2,
E5101 = 2,
E5102 = 2,
E5108 = 4,
E5111 = 2,
E513 = 2,
E521 = 2,
E546 = 2,
E588 = 2,
E678 = 2,
E685 = 5,
E697 = 2,
E703 = 2,
E716 = 2,
E723 = 2,
E724 = 3,
E728 = 2,
E741 = 2,
E742 = 2,
E745 = 2,
E798 = 2,
E805 = 2,
E856 = 2,
E867 = 2,
E900 = 3,
E903 = 2,
E905 = 2,
E906 = 2,
E948 = 2,
E970 = 2,
E974 = 2,
E996 = 5,
}
--- Runs a command, returns stdout lines. Errors on non-zero exit.
--- @param cmd string[]
--- @return string[]
local function run(cmd)
local result = vim.system(cmd, { text = true }):wait()
if result.code ~= 0 then
error('command failed: ' .. table.concat(cmd, ' ') .. '\n' .. (result.stderr or ''))
end
return vim.split(result.stdout, '\n', { trimempty = true })
end
--- Extracts error codes from a line of C source, excluding hex literals (0xE000),
--- identifiers (FOO_E123), and inline comments (`//`).
--- @param line string
--- @return string[]
local function extract_codes(line)
local codes = {} --- @type string[]
local cmt = line:find('//')
for pos, code in line:gmatch('()(E%d%d%d%d?)') do
--- @cast pos integer
local in_comment = cmt and pos > cmt
-- Preceded by a word char means the `E` is part of something else.
local prev = pos > 1 and line:sub(pos - 1, pos - 1) or ''
local in_word = prev:match('[%w_]') ~= nil
if not in_comment and not in_word then
codes[#codes + 1] = code
end
end
return codes
end
--- @return table<string, true> Set of error codes documented in help docs.
local function collect_help_codes()
local lines = run({
'git',
'grep',
'-hE',
[[\*E[0-9]{3,4}\*]],
'--',
'runtime/doc/*.txt',
})
local codes = {} --- @type table<string, true>
for _, line in ipairs(lines) do
for code in line:gmatch('E%d%d%d%d?') do
codes[code] = true
end
end
return codes
end
--- @return table<string, string[]> Map of error code to its occurrences in C sources.
local function collect_c_codes()
local lines = run({
'git',
'grep',
'-nE',
'E[0-9]{3,4}',
'--',
'src/nvim/*.c',
'src/nvim/*.h',
})
local codes = {} --- @type table<string, string[]>
for _, line in ipairs(lines) do
for _, code in ipairs(extract_codes(line)) do
codes[code] = codes[code] or {}
table.insert(codes[code], line)
end
end
return codes
end
--- @param a string
--- @param b string
--- @return boolean
local function errcode_lt(a, b)
return tonumber(a:sub(2)) < tonumber(b:sub(2))
end
--- @param c_codes table<string, string[]>
--- @param help_codes table<string, true>
--- @return integer missing Number of codes missing from help docs.
--- @return integer dups Number of codes with unexpected duplicate usage.
local function report(c_codes, help_codes)
local missing = {} --- @type string[]
for code in pairs(c_codes) do
if not help_codes[code] then
missing[#missing + 1] = code
end
end
table.sort(missing, errcode_lt)
local dup_codes = {} --- @type string[]
for code, occurrences in pairs(c_codes) do
local allowed = dup_allowed[code]
if allowed then
-- Whitelisted: only flag if the actual count doesn't match the expected count.
if #occurrences ~= allowed then
dup_codes[#dup_codes + 1] = code
end
elseif #occurrences > 1 then
dup_codes[#dup_codes + 1] = code
end
end
table.sort(dup_codes, errcode_lt)
if #missing > 0 then
print('Error codes missing from help docs:')
for _, code in ipairs(missing) do
print(' ' .. code)
end
print('')
end
if #dup_codes > 0 then
print('Error codes used in more than one place:')
for _, code in ipairs(dup_codes) do
print(string.format(' %s (%d occurrences):', code, #c_codes[code]))
for _, loc in ipairs(c_codes[code]) do
print(' ' .. loc)
end
end
print('')
end
local max_code = 0
for code in pairs(c_codes) do
local n = tonumber(code:sub(2)) or 0
if n > max_code then
max_code = n
end
end
local n_errcodes = 0
for _ in pairs(c_codes) do
n_errcodes = n_errcodes + 1
end
print(
string.format(
'errcodes=%d dup-codes=%d missing-help=%d highest=E%d',
n_errcodes,
#dup_codes,
#missing,
max_code
)
)
return #missing, #dup_codes
end
local function main()
local help_codes = collect_help_codes()
local c_codes = collect_c_codes()
local missing, dups = report(c_codes, help_codes)
if missing > 0 or dups > 0 then
os.exit(1)
end
end
main()
+1
View File
@@ -8575,6 +8575,7 @@ local options = {
scope = { 'global' },
secure = true,
short_desc = N_('method(s) used to suggest spelling corrections'),
tags = { 'E5700' },
type = 'string',
varname = 'p_sps',
},