From bc6d946cca422c770e792a62d7454387d79065e2 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 16 Apr 2026 10:48:11 -0400 Subject: [PATCH] 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. --- .github/workflows/docs.yml | 5 +- CMakeLists.txt | 5 + Makefile | 2 +- runtime/doc/api.txt | 2 +- runtime/doc/autocmd.txt | 6 +- runtime/doc/channel.txt | 2 +- runtime/doc/cmdline.txt | 2 +- runtime/doc/dev.txt | 62 ++++++---- runtime/doc/diff.txt | 2 +- runtime/doc/gui.txt | 2 +- runtime/doc/job_control.txt | 2 +- runtime/doc/lua.txt | 2 +- runtime/doc/mbyte.txt | 2 +- runtime/doc/message.txt | 7 +- runtime/doc/options.txt | 2 +- runtime/doc/pattern.txt | 4 +- runtime/doc/spell.txt | 2 +- runtime/doc/starting.txt | 4 +- runtime/doc/userfunc.txt | 4 +- runtime/doc/various.txt | 2 +- runtime/doc/vimeval.txt | 11 +- scripts/linterrcodes.lua | 241 ++++++++++++++++++++++++++++++++++++ src/nvim/options.lua | 1 + 23 files changed, 322 insertions(+), 52 deletions(-) create mode 100644 scripts/linterrcodes.lua diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 40787a1b8f..8f67f06ddd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,5 +27,8 @@ jobs: git diff --color --exit-code fi - - name: Validate docs + - name: lintdoc run: make lintdoc + + - name: linterrcodes + run: make linterrcodes diff --git a/CMakeLists.txt b/CMakeLists.txt index 84db914661..a7fcb9b399 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,6 +288,11 @@ add_custom_target(lintcommit COMMAND $ --clean -l ${PROJECT_SOURCE_DIR}/scripts/lintcommit.lua main) add_dependencies(lintcommit nvim_bin) +add_custom_target(linterrcodes + COMMAND $ --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) diff --git a/Makefile b/Makefile index 590bda7178..30f1e6285d 100644 --- a/Makefile +++ b/Makefile @@ -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 $@ diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 572d7d0ae0..095e03bd8a 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -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 diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 9b8cc654ac..16e790f02e 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -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 diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt index 67aba15984..5f52d01668 100644 --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -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* diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt index d057f8a471..a490744f1d 100644 --- a/runtime/doc/cmdline.txt +++ b/runtime/doc/cmdline.txt @@ -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 diff --git a/runtime/doc/dev.txt b/runtime/doc/dev.txt index 9f8a3579ff..6a96b3d8f7 100644 --- a/runtime/doc/dev.txt +++ b/runtime/doc/dev.txt @@ -78,10 +78,10 @@ include the kitchen sink... but it's good for plumbing." ============================================================================== -Developer guidelines *dev-guidelines* +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,13 +422,13 @@ 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. - Lua stdlib = high-level functionality that builds on top of the API. -NAMING GUIDELINES *dev-naming* +NAMING GUIDELINES *dev-naming* Naming is exceedingly important: the name of a thing is the primary interface for uses it, discusses it, searches for it, shares it... Consistent @@ -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,14 +676,15 @@ API client implementation guidelines ~ https://github.com/msgpack-rpc/msgpack-rpc -EXTERNAL UI *dev-ui* +------------------------------------------------------------------------------ +EXTERNAL UI *dev-ui* External UIs should be aware of the |api-contract|. In particular, future 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. diff --git a/runtime/doc/diff.txt b/runtime/doc/diff.txt index 1fd8452fad..035cf41e40 100644 --- a/runtime/doc/diff.txt +++ b/runtime/doc/diff.txt @@ -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, diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index a0dae701fe..f15ef83098 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -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, diff --git a/runtime/doc/job_control.txt b/runtime/doc/job_control.txt index 48ceb5d74a..139e0a5559 100644 --- a/runtime/doc/job_control.txt +++ b/runtime/doc/job_control.txt @@ -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. diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index f53831a2ee..75fe1e34c9 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -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 diff --git a/runtime/doc/mbyte.txt b/runtime/doc/mbyte.txt index 49e129f569..33858f57f6 100644 --- a/runtime/doc/mbyte.txt +++ b/runtime/doc/mbyte.txt @@ -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 diff --git a/runtime/doc/message.txt b/runtime/doc/message.txt index f25fe45633..926b5275a0 100644 --- a/runtime/doc/message.txt +++ b/runtime/doc/message.txt @@ -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 = { diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 835bce74f6..45830379c1 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -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| diff --git a/runtime/doc/pattern.txt b/runtime/doc/pattern.txt index 4142a8f3d2..69132b120c 100644 --- a/runtime/doc/pattern.txt +++ b/runtime/doc/pattern.txt @@ -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}[/] 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. diff --git a/runtime/doc/spell.txt b/runtime/doc/spell.txt index 807615cf73..f53fe1de2b 100644 --- a/runtime/doc/spell.txt +++ b/runtime/doc/spell.txt @@ -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. diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt index e1a7c7dc99..2f29ca2e1e 100644 --- a/runtime/doc/starting.txt +++ b/runtime/doc/starting.txt @@ -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. diff --git a/runtime/doc/userfunc.txt b/runtime/doc/userfunc.txt index 93fac1e204..a44605e277 100644 --- a/runtime/doc/userfunc.txt +++ b/runtime/doc/userfunc.txt @@ -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 diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt index 5fd55ed3d1..8008627511 100644 --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -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()|. diff --git a/runtime/doc/vimeval.txt b/runtime/doc/vimeval.txt index 393b5c6a27..a93e9edb63 100644 --- a/runtime/doc/vimeval.txt +++ b/runtime/doc/vimeval.txt @@ -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 diff --git a/scripts/linterrcodes.lua b/scripts/linterrcodes.lua new file mode 100644 index 0000000000..578ff16a01 --- /dev/null +++ b/scripts/linterrcodes.lua @@ -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 +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 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 + 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 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 + 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 +--- @param help_codes table +--- @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() diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 6478811473..1cf73ddf74 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -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', },