aboutsummaryrefslogtreecommitdiff
path: root/src/gen/gen_vimdoc.lua
diff options
context:
space:
mode:
authorLewis Russell <lewis6991@gmail.com>2026-09-30 14:41:16 +0100
committerLewis Russell <me@lewisr.dev>2026-09-30 17:56:01 +0100
commit364b7a3402634251a766c4a3a81117c296839e12 (patch)
treeecab60376dd1f008b2594ffe3f03ee4bc8fde865 /src/gen/gen_vimdoc.lua
parent9f7fb8b27dc03daf21ff4524b982e1d6e982ca5e (diff)
downloadrneovim-364b7a3402634251a766c4a3a81117c296839e12.tar.gz
rneovim-364b7a3402634251a766c4a3a81117c296839e12.tar.bz2
rneovim-364b7a3402634251a766c4a3a81117c296839e12.zip
ci(lua): enable annotation and style checks
Problem: - annotation-usage-error and code-style-check are disabled. - vim.inspect relies on a dummy self-assignment for docgen, and its help omits parameter and option details. - LuaLS fallback types obscure iterator inference. - Docgen hardcodes public function names and appends separately sorted files, leaving shared helpers under the inspector heading. Solution: - Enable annotation-usage-error and code-style-check, and fix misplaced annotations. - Document vim.inspect's callable module directly, removing the dummy assignment. Describe its parameters, option defaults and process callback. - Remove LuaLS iterator fallbacks, correct iterator signatures and check values that the improved inference identifies as optional. - Parse callable modules, require bindings and parent module references. Derive public names and inherited function docs from these links, including missing help for vim.async.semaphore() and vim.version(). - Export vim.lsp.tagfunc directly with lazy loading and document its implementation instead of maintaining a wrapper. - Group files in section_order, replacing append_only and merge_files. Merge declarations before sorting each section, placing shared helpers in the main vim section and keeping vim.inspect in alphabetical order. - Add basic parser coverage for callable modules and module links. AI-assisted
Diffstat (limited to 'src/gen/gen_vimdoc.lua')
-rwxr-xr-xsrc/gen/gen_vimdoc.lua211
1 files changed, 137 insertions, 74 deletions
diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua
index 58c3fc687a..b17c70c470 100755
--- a/src/gen/gen_vimdoc.lua
+++ b/src/gen/gen_vimdoc.lua
@@ -38,7 +38,9 @@ local INDENTATION = 4
--- Generated documentation target, e.g. api.txt
--- @field filename string
---
---- @field section_order string[]
+--- Ordered filenames or groups of filenames to merge into one section.
+--- For a group, the first filename determines the section name and help tag.
+--- @field section_order (string|string[])[]
---
--- List of files/directories for doxygen to read, relative to `base_dir`.
--- @field files string[]
@@ -59,11 +61,6 @@ local INDENTATION = 4
---
--- Per-function helptag.
--- @field fn_helptag_fmt? fun(fun: nvim.gen_vimdoc.HelptagTarget): string
----
---- @field append_only? string[]
----
---- Merge parsed files into the first section instead of rendering one section per file.
---- @field merge_files? boolean
---@alias nvim.gen_vimdoc.HelptagTarget
---| nvim.luacats.parser.fun
@@ -179,9 +176,8 @@ local config = {
-- Sections at the top, in a specific order:
'builtin.lua',
'options.lua',
- 'editor.lua',
+ { 'editor.lua', 'inspect.lua', 'shared.lua' },
'_inspector.lua',
- 'shared.lua',
-- Sections in alphanumeric order:
'base64.lua',
@@ -233,6 +229,7 @@ local config = {
'runtime/lua/vim/fs.lua',
'runtime/lua/vim/glob.lua',
'runtime/lua/vim/hl.lua',
+ 'runtime/lua/vim/inspect.lua',
'runtime/lua/vim/iter.lua',
'runtime/lua/vim/keymap.lua',
'runtime/lua/vim/loader.lua',
@@ -253,7 +250,7 @@ local config = {
fun.module = 'vim'
end
- if fun.module == 'vim' and contains(fun.name, { 'cmd', 'inspect' }) then
+ if fun.module == 'vim' and fun.name == 'cmd' then
fun.table = nil
end
@@ -312,15 +309,12 @@ local config = {
return fn_helptag_fmt_common(fun)
end,
- append_only = {
- 'shared.lua',
- },
},
lsp = {
filename = 'lsp.txt',
section_order = {
-- Sections at the top, in a specific order:
- 'lsp.lua',
+ { 'lsp.lua', 'tagfunc.lua' },
-- Sections in alphanumeric order:
'buf.lua',
@@ -338,7 +332,6 @@ local config = {
'on_type_formatting.lua',
'rpc.lua',
'semantic_tokens.lua',
- 'tagfunc.lua',
-- Sections at the end, in a specific order:
'util.lua',
@@ -373,9 +366,8 @@ local config = {
async = {
filename = 'lua-async.txt',
section_order = {
- 'async.lua',
+ { 'async.lua', '_core.lua', '_semaphore.lua' },
},
- merge_files = true,
files = {
'runtime/lua/vim/async.lua',
'runtime/lua/vim/async/_core.lua',
@@ -388,12 +380,6 @@ local config = {
return { 'lua-async', 'vim.async' }
end,
fn_xform = function(fun)
- if fun.module == 'vim.async._core' or fun.module == 'vim.async._semaphore' then
- fun.module = 'vim.async'
- end
- if fun.name == 'new_semaphore' then
- fun.name = 'semaphore'
- end
if fun.classvar == 'M' then
fun.classvar = nil
end
@@ -425,10 +411,8 @@ local config = {
'highlighter.lua',
'language.lua',
'languagetree.lua',
- 'query.lua',
- 'tsquery.lua',
+ { 'query.lua', 'tsquery.lua' },
},
- append_only = { 'tsquery.lua' },
files = {
'runtime/lua/vim/treesitter/_meta/',
'runtime/lua/vim/treesitter.lua',
@@ -1171,21 +1155,16 @@ local function make_section(filename, cfg, briefs, funs_txt, classes_txt)
end
--- @param section nvim.gen_vimdoc.Section
---- @param add_header? boolean
-local function render_section(section, add_header)
+local function render_section(section)
local doc = {} --- @type string[]
if not section.title then
error(('section.title is nil, check section_fmt(). section: %s'):format(vim.inspect(section)))
end
- if add_header ~= false then
- vim.list_extend(doc, {
- string.rep('=', TEXT_WIDTH),
- '\n',
- align_tags(TEXT_WIDTH)(section.title .. ' ' .. section.help_tag),
- })
- end
+ doc[#doc + 1] = string.rep('=', TEXT_WIDTH)
+ .. '\n'
+ .. align_tags(TEXT_WIDTH)(section.title .. ' ' .. section.help_tag)
if next(section.briefs) then
local briefs_txt = {} --- @type string[]
@@ -1193,23 +1172,21 @@ local function render_section(section, add_header)
briefs_txt[#briefs_txt + 1] = md_to_vimdoc(b, 0, 0, TEXT_WIDTH)
end
- local sdoc = '\n\n' .. table.concat(briefs_txt, '\n')
+ local sdoc = table.concat(briefs_txt, '\n')
if sdoc:find('[^%s]') then
doc[#doc + 1] = sdoc
end
end
if section.classes_txt ~= '' then
- table.insert(doc, '\n\n')
table.insert(doc, (section.classes_txt:gsub('\n+$', '\n')))
end
if section.funs_txt ~= '' then
- table.insert(doc, '\n\n')
table.insert(doc, section.funs_txt)
end
- return table.concat(doc)
+ return table.concat(doc, '\n\n')
end
local parsers = {
@@ -1243,11 +1220,95 @@ local function find_module_class(classes, modvar)
end
end
+--- Add callable exports and inherited module functions under their public names.
+--- E.g. `M.semaphore = require('vim.async._semaphore')` in vim.async exposes
+--- that module's returned `new_semaphore` function as `vim.async.semaphore()`.
+--- A callable table uses its module name: vim.inspect's `__call` => `vim.inspect()`.
+--- @param modules table<string,nvim.luacats.parser.module>
+local function expand_module_docs(modules)
+ local exports = {} --- @type table<nvim.luacats.parser.module,string[]>
+ for _, module in pairs(modules) do
+ local documented = {} --- @type table<string,true>
+ for _, fun in ipairs(module.funs) do
+ if fun.modvar == module.modvar and (not fun.classvar or fun.classvar == module.modvar) then
+ documented[fun.name] = true
+ end
+ end
+ for name, imported in pairs(module.imports) do
+ local public_name = module.name .. '.' .. name
+ if not documented[name] and not ('.' .. public_name):find('%._') then
+ exports[imported] = exports[imported] or {}
+ table.insert(exports[imported], public_name)
+ end
+ end
+ end
+
+ for name, module in pairs(modules) do
+ -- E.g. new_semaphore becomes vim.async.semaphore through a require() binding.
+ -- Otherwise, use the public module path (vim.inspect for its __call).
+ if module.callable and (exports[module] or not ('.' .. name):find('%._')) then
+ for _, public_name in ipairs(exports[module] or { name }) do
+ local fun = vim.deepcopy(module.callable)
+ fun.module, fun.name = public_name:match('^(.*)%.([^.]+)$')
+ fun.name = fun.name or public_name
+ module.funs[#module.funs + 1] = fun
+ end
+ end
+ end
+
+ -- `@class vim.async: vim.async._core` makes `vim.async._core.run()` => `vim.async.run()`.
+ -- Copy inherited functions under the public module name, keeping nearer declarations.
+ local hidden = {} --- @type table<nvim.luacats.parser.fun,true>
+ for _, module in pairs(modules) do
+ if module.class and not ('.' .. module.name):find('%._') then
+ local names = {} --- @type table<string,true>
+ local seen = {} --- @type table<nvim.luacats.parser.module,true>
+ local source = module
+ while source and not seen[source] do
+ seen[source] = true
+ for _, fun in ipairs(source.funs) do
+ if fun.class == source.class.name and fun.member_sep == '.' then
+ if
+ source ~= module
+ and not names[fun.name]
+ and not fun.nodoc
+ and not fun.access
+ and fun.name:sub(1, 1) ~= '_'
+ then
+ local inherited = vim.deepcopy(fun)
+ inherited.module, inherited.class = module.name, module.class.name
+ inherited.modvar, inherited.classvar = module.modvar, module.modvar
+ module.funs[#module.funs + 1] = inherited
+ end
+ if source ~= module and ('.' .. source.name):find('%._') then
+ hidden[fun] = true
+ end
+ -- Include functions already inherited by the source module.
+ names[fun.name] = true
+ end
+ end
+ -- A nearer declaration shadows ancestors, including non-function fields.
+ for _, field in ipairs(source.class.fields) do
+ names[field.name] = true
+ end
+ for name in pairs(source.assignments) do
+ names[name] = true
+ end
+ source = source.parent
+ end
+ end
+ end
+ -- Delay hiding until all modules have inherited the original documentation.
+ for fun in pairs(hidden) do
+ fun.nodoc = true
+ end
+end
+
--- @param cfg nvim.gen_vimdoc.Config
local function gen_target(cfg)
cfg.fn_helptag_fmt = cfg.fn_helptag_fmt or fn_helptag_fmt_common
print('Target:', cfg.filename)
- local sections = {} --- @type table<string,nvim.gen_vimdoc.Section>
+ local sections = {} --- @type nvim.gen_vimdoc.Section[]
expand_files(cfg.files)
@@ -1257,20 +1318,28 @@ local function gen_target(cfg)
--- @type table<string,nvim.luacats.parser.class>
local all_classes = {}
- --- First pass so we can collect all classes
+ --- @type table<string,nvim.luacats.parser.module>
+ local modules = {}
+
+ --- First pass so we can collect all classes and module exports.
for _, f in vim.spairs(cfg.files) do
local ext = f:match('%.([^.]+)$')
local parser = parsers[ext]
if parser then
- local classes, funs, briefs = parser(f)
+ local classes, funs, briefs, _, module = parser(f)
file_results[f] = { classes, funs, briefs }
all_classes = vim.tbl_extend('error', all_classes, classes)
+ if module then
+ modules[module.name] = module
+ end
end
end
- local merged_classes = {} --- @type table<string,nvim.luacats.parser.class>
- local merged_funs = {} --- @type nvim.luacats.parser.fun[]
- local merged_briefs = {} --- @type string[]
+ luacats_parser.resolve_modules(modules)
+ expand_module_docs(modules)
+
+ --- @type table<string,[table<string,nvim.luacats.parser.class>, nvim.luacats.parser.fun[], string[]]>
+ local files_by_name = {}
for f, r in vim.spairs(file_results) do
local classes, funs, briefs = r[1], r[2], r[3]
@@ -1291,43 +1360,37 @@ local function gen_target(cfg)
print(' Processing file:', f)
- if cfg.merge_files then
- merged_classes = vim.tbl_extend('error', merged_classes, classes)
- vim.list_extend(merged_funs, funs)
- vim.list_extend(merged_briefs, briefs)
- else
- -- FIXME: Using f_base will confuse `_meta/protocol.lua` with `protocol.lua`
- local f_base = vim.fs.basename(f)
- sections[f_base] = make_section(
- f_base,
- cfg,
- render_briefs(briefs, cfg),
- render_funs(funs, all_classes, cfg),
- render_classes(classes, funs, cfg)
- )
- end
+ -- FIXME: Using basenames will confuse `_meta/protocol.lua` with `protocol.lua`
+ files_by_name[vim.fs.basename(f)] = r
end
- if cfg.merge_files then
- local section_file = cfg.section_order[1]
- sections[section_file] = make_section(
- section_file,
+ for _, entry in ipairs(cfg.section_order) do
+ local files = type(entry) == 'table' and entry or { entry }
+ local classes = {} --- @type table<string,nvim.luacats.parser.class>
+ local funs = {} --- @type nvim.luacats.parser.fun[]
+ local briefs = {} --- @type string[]
+ for _, f in ipairs(files) do
+ local r = files_by_name[f]
+ if r then
+ classes = vim.tbl_extend('error', classes, r[1])
+ vim.list_extend(funs, r[2])
+ vim.list_extend(briefs, r[3])
+ end
+ end
+ sections[#sections + 1] = make_section(
+ files[1],
cfg,
- render_briefs(merged_briefs, cfg),
- render_funs(merged_funs, all_classes, cfg),
- render_classes(merged_classes, merged_funs, cfg)
+ render_briefs(briefs, cfg),
+ render_funs(funs, all_classes, cfg),
+ render_classes(classes, funs, cfg)
)
end
- local first_section_tag = sections[cfg.section_order[1]].help_tag
+ local first_section_tag = sections[1].help_tag
local docs = {} --- @type string[]
- for _, f in ipairs(cfg.section_order) do
- local section = sections[f]
- if section then
- print(fmt(" Rendering section: '%s'", section.title))
- local add_sep_and_header = not vim.tbl_contains(cfg.append_only or {}, f)
- docs[#docs + 1] = render_section(section, add_sep_and_header)
- end
+ for _, section in ipairs(sections) do
+ print(fmt(" Rendering section: '%s'", section.title))
+ docs[#docs + 1] = render_section(section)
end
table.insert(