local fmt = string.format local M = {} --- Banned nouns. See `:help dev-name-common`. --- Apply to parameter names, keyset keys, and function name parts. local banned_nouns = { buffer = 'buf', bufnr = 'buf', -- channel = 'chan', command = 'cmd', directory = 'dir', highlight = 'hl', position = 'pos', process = 'proc', window = 'win', winnr = 'win', } --- Banned verbs. See `:help dev-name-common`. --- Apply to function name parts. local banned_verbs = { contains = 'has', delete = 'del', disable = 'enable', exit = 'cancel', -- or "stop" -- format = 'fmt', -- hide = '?', -- show = '?', list = 'get', notify = 'print', -- or "echo" pretty = 'fmt', remove = 'del', toggle = 'enable', } --- Whitelist of legacy function names that violate the naming conventions. --- Do not add new entries here. New functions must follow `:help dev-naming`. --- --- @type table> local legacy_names = { ['src/nvim/api/buffer.c'] = { nvim_buf_delete = true, }, ['src/nvim/api/window.c'] = { nvim_win_get_position = true, }, ['src/nvim/api/tabpage.c'] = { nvim_tabpage_list_wins = true, }, ['src/nvim/api/ui.c'] = { remote_ui_highlight_set = true, }, ['src/nvim/api/vim.c'] = { nvim_list_bufs = true, nvim_list_chans = true, nvim_list_runtime_paths = true, nvim_list_tabpages = true, nvim_list_uis = true, nvim_list_wins = true, }, ['runtime/lua/vim/uri.lua'] = { uri_from_bufnr = true, uri_to_bufnr = true, }, ['runtime/lua/vim/_core/shared.lua'] = { _ensure_list = true, _list_insert = true, _list_remove = true, _resolve_bufnr = true, list_contains = true, tbl_contains = true, }, ['runtime/lua/vim/lsp.lua'] = { buf_notify = true, }, ['runtime/lua/vim/lsp/client.lua'] = { _on_exit = true, _process_request = true, _process_static_registrations = true, _remove_workspace_folder = true, }, ['runtime/lua/vim/lsp/util.lua'] = { buf_highlight_references = true, make_position_params = true, }, ['runtime/lua/vim/lsp/rpc.lua'] = { _notify = true, }, ['runtime/lua/vim/treesitter.lua'] = { node_contains = true, }, ['runtime/lua/vim/treesitter/highlighter.lua'] = { for_each_highlight_state = true, prepare_highlight_states = true, }, ['runtime/lua/vim/treesitter/query.lua'] = { _process_patterns = true, }, ['src/nvim/api/command.c'] = { create_user_command = true, nvim_buf_create_user_command = true, nvim_buf_del_user_command = true, nvim_create_user_command = true, nvim_del_user_command = true, }, ['src/nvim/api/vimscript.c'] = { nvim_command = true, }, } --- Whitelist of legacy Lua class fields that violate the naming conventions. --- Do not add new entries here. New classes must follow `:help dev-naming`. --- --- @type table> local legacy_fields = { ['vim.Diagnostic'] = { bufnr = true, }, ['vim.lsp.Capability'] = { bufnr = true, }, ['vim.lsp.inlay_hint.enable.Filter'] = { bufnr = true, }, ['vim.lsp.inlay_hint.get.Filter'] = { bufnr = true, }, ['vim.lsp.inlay_hint.get.ret'] = { bufnr = true, }, ['TS.Heading'] = { bufnr = true, -- Passed to setloclist(). }, ['vim.treesitter.get_node.Opts'] = { bufnr = true, }, ['vim.treesitter.dev.inspect_tree.Opts'] = { bufnr = true, command = true, }, ['vim.undotree.opts'] = { bufnr = true, command = true, }, } --- Whitelist of legacy keyset keys that violate the naming conventions. --- Do not add new entries here. New keysets must follow `:help dev-naming`. --- --- @type table> local legacy_keys = { create_autocmd = { command = true, }, } --- Enforces naming conventions (`:help dev-naming`). --- --- - For API functions: --- - Checks positional parameter names of non-deprecated functions. --- The `legacy_names` whitelist does NOT apply to params: misnamed positional --- parameters are never allowed. --- - Checks function name parts (split by `_`) for banned nouns/verbs. --- - For `keysets_defs.h` keys: --- - Checks key names. Because we don't (currently) have a way to mark keyset names as --- "deprecated", a banned key is allowed (only) if its replacement also exists. --- - For Lua classes (opts dicts, etc.): --- - Checks field names. A banned field is allowed only if its replacement also exists --- (same compat rule as keysets). --- --- @param source string Source filename or module name. --- @param api_funs {name:string, params:{name:string}[], deprecated?:true, deprecated_since?:integer}[]? Parsed API functions. --- @param keysets {name: string, keys: string[], types: table}[]? API keyset metadata. --- @param classes table? Parsed Lua classes. function M.lint_names(source, api_funs, keysets, classes) local errors = {} --- @type string[] if api_funs then local src_legacy = legacy_names[source] or {} for _, fun in ipairs(api_funs) do if fun.name and fun.params and not fun.deprecated and not fun.deprecated_since then -- Positional parameter names: always checked (no legacy allowed). -- Exception: "bufnr" is allowed as a param name. for _, p in ipairs(fun.params) do local want_name = banned_nouns[p.name] if want_name and p.name ~= 'bufnr' then local msg = '%s: %s(): param "%s" should be renamed to "%s"' errors[#errors + 1] = fmt(msg, source, fun.name, p.name, want_name) end end -- Function name parts: check for banned nouns/verbs. -- Skip the `nvim_` prefix; start from the second part. -- Legacy-whitelisted names are skipped. if not src_legacy[fun.name] then local parts = vim.split(fun.name, '_', { plain = true }) for i = 2, #parts do local part = parts[i] local want_noun = banned_nouns[part] if want_noun then local msg = '%s: %s(): name contains banned noun "%s", use "%s"' errors[#errors + 1] = fmt(msg, source, fun.name, part, want_noun) end local want_verb = banned_verbs[part] if want_verb then local msg = '%s: %s(): name contains banned verb "%s", use "%s"' errors[#errors + 1] = fmt(msg, source, fun.name, part, want_verb) end end end end end end if keysets then for _, k in ipairs(keysets) do local keyset = {} --- @type table for _, key in ipairs(k.keys) do keyset[key] = true end local ks_legacy = legacy_keys[k.name] or {} for _, key in ipairs(k.keys) do local want_name = banned_nouns[key] if want_name and not keyset[want_name] and not ks_legacy[key] then -- Banned key without its replacement: not a compat key, just stale. local msg = '%s: keyset "%s": key "%s" should be renamed to "%s"' errors[#errors + 1] = fmt(msg, source, k.name, key, want_name) end end end end if classes then for cls_name, cls in pairs(classes) do if cls.fields and not cls.nodoc and not cls.access then local field_set = {} --- @type table for _, f in ipairs(cls.fields) do field_set[f.name] = true end local cls_legacy = legacy_fields[cls_name] or {} for _, f in ipairs(cls.fields) do if not f.access then local want_name = banned_nouns[f.name] if want_name and not field_set[want_name] and not cls_legacy[f.name] then -- Banned field without its replacement: not a compat field, just stale. local msg = '%s: class "%s": field "%s" should be renamed to "%s"' errors[#errors + 1] = fmt(msg, source, cls_name, f.name, want_name) end end end end end end if #errors > 0 then table.sort(errors) error('lint_names(): found banned parameter/key names:\n ' .. table.concat(errors, '\n ')) 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? -- Capture the keyname with optional punctuation (`, "), so the sort includes surrounding -- chars: backtick items group together, double-quote group together, etc. (Example: |Client:stop()|) 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('^([`"]?)_', '%1~') or nil local name_key = name:gsub('^([`"]?)_', '%1~') 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