From 033efbbd32fad882da67c0a1f658d1c12a8d515e Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 26 Apr 2026 12:36:07 +0200 Subject: [PATCH] build(docs): sort/lint class fields and keysets Problem: Fields (key names) in classes, keysets, and quasi-keysets are ordered randomly, which adds friction when reading docs. Solution: - Sort class fields and keysets when generating docs. - Add a lint check for quasi-keysets (keysets defined as unstructured markdown lists within a docstring). --- src/gen/gen_eval_files.lua | 1 + src/gen/gen_vimdoc.lua | 5 +++++ src/gen/lint.lua | 42 ++++++++++++++++++++++++++++++++++++++ src/gen/util.lua | 13 ++++++++++++ 4 files changed, 61 insertions(+) diff --git a/src/gen/gen_eval_files.lua b/src/gen/gen_eval_files.lua index 05f2d52fcf..e55c8aeca7 100755 --- a/src/gen/gen_eval_files.lua +++ b/src/gen/gen_eval_files.lua @@ -328,6 +328,7 @@ local function get_api_keysets_meta() lint.lint_names('src/nvim/api/keysets_defs.h', nil, keysets) for _, k in ipairs(keysets) do + util.sort_by_key(k.keys) local params = {} for _, key in ipairs(k.keys) do local pty = k.types[key] or 'any' diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua index 204187cb3e..6b237b6ef8 100755 --- a/src/gen/gen_vimdoc.lua +++ b/src/gen/gen_vimdoc.lua @@ -610,6 +610,8 @@ local function inline_type(obj, classes) end end + util.sort_by_key(cls.fields, 'name') + local desc_append = {} for _, f in ipairs(cls.fields) do if not f.access then @@ -715,6 +717,8 @@ local function render_class(class, classes, hidden_fields, cfg) end, fields) end + util.sort_by_key(fields, 'name') + local fields_txt = render_fields_or_params(fields, nil, classes, cfg) if not fields_txt:match('^%s*$') then table.insert(ret, '\n Fields: ~\n') @@ -1145,6 +1149,7 @@ local function gen_target(cfg) if not f:find('ui_events%.in%.h$') then -- TODO(justinmk): also lint UI events. lint.lint_names(f, funs, nil, classes) + lint.lint_quasi_keysets(f, funs) end local mod_cls_nm = find_module_class(classes, 'M') diff --git a/src/gen/lint.lua b/src/gen/lint.lua index fd8292f29d..ee7a61456f 100644 --- a/src/gen/lint.lua +++ b/src/gen/lint.lua @@ -226,4 +226,46 @@ function M.lint_names(source, api_funs, keysets, classes) end end +--- Checks that markdown list items (`- name :` or `- {name}`) in parameter/field +--- descriptions are in sorted order. +--- +--- This is for "quasi-keysets" defined as markdown lists in docstrings. Ideally they would be +--- defined as a luals `@class` or a API keyset, but sometimes that isn't possible. +--- +--- @param source string Source filename. +--- @param funs {name:string, params:{name:string, desc?:string}[]}[] +function M.lint_quasi_keysets(source, funs) + local errors = {} --- @type string[] + + for _, fun in ipairs(funs) do + for _, p in ipairs(fun.params or {}) do + if p.desc then + local prev = nil --- @type string? + for name in p.desc:gmatch('\n%- ([%w_]+)') do + -- Sort underscore-prefixed keys (e.g. "_cmdline_offset") at the end. + local prev_key = prev and prev:gsub('^_', '~') or nil + local name_key = name:gsub('^_', '~') + if prev_key and name_key < prev_key then + errors[#errors + 1] = fmt( + '%s: %s(): param "%s": key "%s" should come before "%s" (sort alphabetically)', + source, + fun.name, + p.name, + name, + prev + ) + end + prev = name + end + end + end + end + + if #errors > 0 then + error( + 'lint_quasi_keysets(): unsorted keys in doc comments:\n ' .. table.concat(errors, '\n ') + ) + end +end + return M diff --git a/src/gen/util.lua b/src/gen/util.lua index 139047f1fa..c1d8f9b8e7 100644 --- a/src/gen/util.lua +++ b/src/gen/util.lua @@ -404,4 +404,17 @@ function M.md_to_vimdoc(text, start_indent, indent, text_width, is_list) return s end +--- Sorts a list in-place, with underscore-prefixed names last (e.g. "_cmdline_offset"). +--- If `key` is given, sorts by `item[key]`; otherwise sorts by value. +--- +--- @param t any[] +--- @param key? string +function M.sort_by_key(t, key) + table.sort(t, function(a, b) + local a_val = key and a[key] or a + local b_val = key and b[key] or b + return a_val:gsub('^_', '~') < b_val:gsub('^_', '~') + end) +end + return M