Files
neovim/scripts/linterrcodes.lua
T

243 lines
5.3 KiB
Lua

-- 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,
E1510 = 2,
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 = 5,
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()