From 6a54396db46c97529ce89ffceb78a2842a7573c5 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 7 Sep 2026 17:04:29 +0100 Subject: [PATCH] build: enable more EmmyLua checks - call-non-callable - missing-fields - redefined-label - redefined-local - undefined-field - unreachable-code Promote redefined-local and unreachable-code to warnings. Fix DOS line-ending handling for multiline semantic tokens, and support intersection types in the help parser. AI-assisted --- .emmyrc.json | 10 ++--- runtime/doc/diagnostic.txt | 13 +++--- runtime/doc/lsp.txt | 3 +- runtime/doc/lua.txt | 25 +++++++---- runtime/ftplugin/help.lua | 3 +- runtime/lua/man.lua | 5 ++- runtime/lua/vim/_core/shared.lua | 41 +++++++++++-------- runtime/lua/vim/_core/system.lua | 2 + runtime/lua/vim/_core/table.lua | 2 + runtime/lua/vim/_core/ui2/messages.lua | 7 ++-- runtime/lua/vim/_watch.lua | 4 +- runtime/lua/vim/deprecated/health.lua | 2 +- runtime/lua/vim/diagnostic.lua | 3 +- runtime/lua/vim/diagnostic/_config.lua | 1 + runtime/lua/vim/diagnostic/_handlers.lua | 5 ++- runtime/lua/vim/health.lua | 15 +++---- runtime/lua/vim/iter.lua | 5 ++- runtime/lua/vim/lsp.lua | 12 ++++-- runtime/lua/vim/lsp/_capability.lua | 8 ++-- runtime/lua/vim/lsp/_folding_range.lua | 6 +-- runtime/lua/vim/lsp/_snippet_grammar.lua | 5 +-- runtime/lua/vim/lsp/_tagfunc.lua | 3 ++ runtime/lua/vim/lsp/buf.lua | 8 ++-- runtime/lua/vim/lsp/client.lua | 14 +++---- runtime/lua/vim/lsp/codelens.lua | 4 +- runtime/lua/vim/lsp/semantic_tokens.lua | 2 +- runtime/lua/vim/net.lua | 1 + runtime/lua/vim/pack.lua | 3 ++ runtime/lua/vim/pos.lua | 6 +-- runtime/lua/vim/provider/health.lua | 3 ++ runtime/lua/vim/range.lua | 3 +- runtime/lua/vim/re.lua | 1 + runtime/lua/vim/treesitter/_select.lua | 4 +- runtime/lua/vim/treesitter/highlighter.lua | 3 +- runtime/lua/vim/treesitter/languagetree.lua | 6 +-- runtime/lua/vim/version.lua | 3 ++ .../dist/opt/nvim.difftool/lua/difftool.lua | 2 +- .../pack/dist/opt/nvim.tohtml/lua/tohtml.lua | 11 +---- .../dist/opt/nvim.undotree/lua/undotree.lua | 29 ++++++++++--- src/gen/gen_vimdoc.lua | 10 ++++- src/gen/luacats_grammar.lua | 5 ++- .../script/luacats_grammar_spec.lua | 10 +++++ 42 files changed, 190 insertions(+), 118 deletions(-) diff --git a/.emmyrc.json b/.emmyrc.json index dbfbe96446..4c6df27e51 100644 --- a/.emmyrc.json +++ b/.emmyrc.json @@ -29,31 +29,27 @@ "annotation-usage-error", "assign-type-mismatch", "await-in-sync", - "call-non-callable", "code-style-check", "deprecated", "global-in-non-module", "incomplete-signature-doc", "invert-if", "iter-variable-reassign", - "missing-fields", "missing-global-doc", "need-check-nil", "non-literal-expressions-in-assert", "param-type-mismatch", "preferred-local-alias", - "redefined-label", - "redefined-local", "return-type-mismatch", - "undefined-field", "unknown-doc-tag", "unnecessary-assert", "unnecessary-if", - "unreachable-code", "unused" ], "severity": { - "duplicate-require": "warning" + "duplicate-require": "warning", + "redefined-local": "warning", + "unreachable-code": "warning" } }, "codeAction": { diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt index a21cba14a4..4163b26ebf 100644 --- a/runtime/doc/diagnostic.txt +++ b/runtime/doc/diagnostic.txt @@ -659,11 +659,11 @@ Lua module: vim.diagnostic *diagnostic-api* • {severity}? (`vim.diagnostic.SeverityFilter`) Only show signs for diagnostics matching the given severity |diagnostic-severity| - • {text}? (`table`) A table mapping - |diagnostic-severity| to the sign text to display in the - sign column and statusline. The default is to use `"E"`, - `"W"`, `"I"`, and `"H"` for errors, warnings, - information, and hints, respectively. Example: >lua + • {text}? (`table`) + A table mapping |diagnostic-severity| to the sign text to + display in the sign column and statusline. The default is + to use `"E"`, `"W"`, `"I"`, and `"H"` for errors, + warnings, information, and hints, respectively. Example: >lua vim.diagnostic.config({ signs = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } } }) @@ -803,6 +803,9 @@ config({opts}, {namespace}) *vim.diagnostic.config()* • {namespace} (`integer?`) Update the options for the given namespace. When omitted, update the global diagnostic options. + Overloads: ~ + • `fun(opts?: nil, namespace?: integer): vim.diagnostic.Opts` + Return: ~ (`vim.diagnostic.Opts?`) Current diagnostic config if {opts} is omitted. diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index e8fd100b80..854222650f 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1438,7 +1438,8 @@ tagfunc({pattern}, {flags}) *vim.lsp.tagfunc()* • {flags} (`string`) See |tag-function| Return: ~ - (`table[]`) tags A list of matching tags + (`table[]|vim.NIL`) tags A list of matching tags, or `vim.NIL` to use + the built-in tags. ============================================================================== diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index af28ea63a0..00f6aa93bf 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1828,7 +1828,12 @@ vim.list.bisect({t}, {val}, {opts}) *vim.list.bisect()* Use {bound} to determine whether to return the first or the last position, defaults to "lower", i.e., the first position. - NOTE: Behavior is undefined on unsorted lists! + With {opts.key}, {val} may differ from the list elements, provided the key + function accepts both. For a string key, that field must exist on both. A + partial record is sufficient if it contains all fields used by the key. + + NOTE: The values being compared must support `<`, and the list must be + sorted by those values (after applying {opts.key}, if provided). Example: >lua @@ -1858,7 +1863,7 @@ vim.list.bisect({t}, {val}, {opts}) *vim.list.bisect()* Since: 0.12.0 Parameters: ~ - • {t} (`any[]`) A comparable list. + • {t} (`any[]`) A sorted list. • {val} (`any`) The value to search. • {opts} (`table?`) A table with the following fields: • {bound}? (`'lower'|'upper'`, default: `'lower'`) Specifies @@ -1869,11 +1874,17 @@ vim.list.bisect({t}, {val}, {opts}) *vim.list.bisect()* keeps the list sorted.. • {hi}? (`integer`, default: `#t + 1`) End index of the list, exclusive. - • {key}? (`string|fun(val: any): any`) Optional, compare the - return value instead of the {val} itself if provided. If a - string, index each value by this field name. + • {key}? (`(string & keyof T)|fun(val: T): any`) Applied to + {val} and the list elements being compared. If a string, + index both by this field name. If a function, it must accept + both {val} and the list elements and return mutually + comparable keys. • {lo}? (`integer`, default: `1`) Start index of the list. + Overloads: ~ + • `fun(t: T[], val: Q, opts: vim.list.bisect.Opts & { key: fun(val: T|Q): any }): integer` + • `fun(t: T[], val: Q, opts: vim.list.bisect.Opts & { key: string & keyof T & keyof Q }): integer` + Return: ~ (`integer`) index serves as either the lower bound or the upper bound position. @@ -2252,11 +2263,11 @@ vim.tbl_map({fn}, {t}) *vim.tbl_map()* change). Parameters: ~ - • {fn} (`fun(value: T): any`) Function + • {fn} (`fun(value: T): R`) Function • {t} (`table`) Table Return: ~ - (`table`) Table of transformed values + (`table`) Table of transformed values vim.tbl_values({t}) *vim.tbl_values()* Return a list of all values used in a table. However, the order of the diff --git a/runtime/ftplugin/help.lua b/runtime/ftplugin/help.lua index cee56ff053..fe105e4959 100644 --- a/runtime/ftplugin/help.lua +++ b/runtime/ftplugin/help.lua @@ -96,7 +96,8 @@ local function runnables() if name == 'code' then local code = vim.treesitter.get_node_text(node, 0) - local lang_node = match[metadata[id].lang][1] --[[@as TSNode]] + local lang_id = metadata[id].lang --[[@as integer]] + local lang_node = match[lang_id][1] local lang = vim.treesitter.get_node_text(lang_node, 0) for i = start + 1, end_ do code_blocks[i] = { lang = lang, code = code } diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 18f27ba984..84dfd0e7f4 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -270,12 +270,13 @@ function M._match_manpage_path(paths, name, sect) end -- find any that match the specified name + --- @type string[] --- @param v string local namematches = vim.tbl_filter(function(v) local tail = vim.fs.basename(v) return tail:find(name, 1, true) ~= nil - end, paths) or {} - local sectmatches = {} + end, paths) + local sectmatches = {} --- @type string[] if #namematches > 0 and sect ~= '' then --- @param v string diff --git a/runtime/lua/vim/_core/shared.lua b/runtime/lua/vim/_core/shared.lua index 6e62db8015..d4c33d4b8a 100644 --- a/runtime/lua/vim/_core/shared.lua +++ b/runtime/lua/vim/_core/shared.lua @@ -272,10 +272,10 @@ end --- Applies function `fn` to all values of table `t`, in `pairs()` iteration order (which is not --- guaranteed to be stable, even when the data doesn't change). --- ----@generic T ----@param fn fun(value: T): any Function +---@generic T, R +---@param fn fun(value: T): R Function ---@param t table Table ----@return table : Table of transformed values +---@return table : Table of transformed values function vim.tbl_map(fn, t) vim.validate('fn', fn, 'callable') vim.validate('t', t, 'table') @@ -457,7 +457,7 @@ function vim.list.unique(t, key) return t end ----@class vim.list.bisect.Opts +---@class vim.list.bisect.Opts ---@inlinedoc --- --- Start index of the list. @@ -468,9 +468,10 @@ end --- (default: `#t + 1`) ---@field hi? integer --- ---- Optional, compare the return value instead of the {val} itself if provided. ---- If a string, index each value by this field name. ----@field key? string|fun(val: any): any +--- Applied to {val} and the list elements being compared. +--- If a string, index both by this field name. If a function, it must accept both +--- {val} and the list elements and return mutually comparable keys. +---@field key? (string & keyof T)|fun(val: T): any --- --- Specifies the search variant. --- - "lower": returns the first position @@ -480,12 +481,12 @@ end --- (default: `'lower'`) ---@field bound? 'lower' | 'upper' ----@generic T +---@generic T, Q ---@param t T[] ----@param val T +---@param val Q ---@param lo integer ---@param hi integer ----@param key_fn fun(val: any): any +---@param key_fn fun(val: T|Q): any ---@return integer i in range such that `t[j]` < {val} for all j < i, --- and `t[j]` >= {val} for all j >= i, --- or return {hi} if no such index is found. @@ -503,12 +504,12 @@ local function lower_bound(t, val, lo, hi, key_fn) return lo end ----@generic T +---@generic T, Q ---@param t T[] ----@param val T +---@param val Q ---@param lo integer ---@param hi integer ----@param key_fn fun(val: any): any +---@param key_fn fun(val: T|Q): any ---@return integer i in range such that `t[j]` <= {val} for all j < i, --- and `t[j]` > {val} for all j >= i, --- or return {hi} if no such index is found. @@ -532,7 +533,12 @@ end --- Use {bound} to determine whether to return the first or the last position, --- defaults to "lower", i.e., the first position. --- ---- NOTE: Behavior is undefined on unsorted lists! +--- With {opts.key}, {val} may differ from the list elements, provided the key +--- function accepts both. For a string key, that field must exist on both. +--- A partial record is sufficient if it contains all fields used by the key. +--- +--- NOTE: The values being compared must support `<`, and the list must be sorted +--- by those values (after applying {opts.key}, if provided). --- --- Example: --- ```lua @@ -560,10 +566,12 @@ end --- ``` ---@since 14 ---@generic T ----@param t T[] A comparable list. +---@param t T[] A sorted list. ---@param val T The value to search. ----@param opts? vim.list.bisect.Opts +---@param opts? vim.list.bisect.Opts ---@return integer index serves as either the lower bound or the upper bound position. +---@overload fun(t: T[], val: Q, opts: vim.list.bisect.Opts & { key: fun(val: T|Q): any }): integer +---@overload fun(t: T[], val: Q, opts: vim.list.bisect.Opts & { key: string & keyof T & keyof Q }): integer function vim.list.bisect(t, val, opts) vim.validate('t', t, 'table') vim.validate('opts', opts, 'table', true) @@ -1131,6 +1139,7 @@ do end elseif vim.is_callable(validator) then -- Check user-provided validation function + ---@cast validator fun(v: any): boolean, string? local valid, opt_msg = validator(val) if not valid then local err_msg = ('%s: expected %s, got %s'):format( diff --git a/runtime/lua/vim/_core/system.lua b/runtime/lua/vim/_core/system.lua index 3ff4bcdc2f..d804b04dfe 100644 --- a/runtime/lua/vim/_core/system.lua +++ b/runtime/lua/vim/_core/system.lua @@ -237,6 +237,7 @@ local function setup_output(output, text) end end + --- @diagnostic disable-next-line:missing-fields luvit/luv#827 local pipe_fd = assert(uv.pipe({ nonblock = true }, {})) local pipe = assert(uv.new_pipe(false)) pipe:open(pipe_fd.read) @@ -268,6 +269,7 @@ local function setup_input(input) towrite = input end + --- @diagnostic disable-next-line:missing-fields luvit/luv#827 local pipe_fd = assert(uv.pipe({}, { nonblock = true })) local pipe = assert(uv.new_pipe(false)) pipe:open(pipe_fd.write) diff --git a/runtime/lua/vim/_core/table.lua b/runtime/lua/vim/_core/table.lua index 58833b1c8d..d039a25e41 100644 --- a/runtime/lua/vim/_core/table.lua +++ b/runtime/lua/vim/_core/table.lua @@ -21,6 +21,8 @@ if not has_clear then end end +---@cast new fun(narr: integer, nrec: integer): table +---@cast clear fun(tab: table) M.new = new M.clear = clear diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 33048ac9c1..b6084daffe 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -737,13 +737,14 @@ end ---@param focus? boolean Enter the pager: it was explicitly requested. function M.set_pos(tgt, focus) for t, win in pairs(ui.wins) do - local cfg = (t == tgt or (tgt == nil and t ~= 'cmd')) + local current_cfg = (t == tgt or (tgt == nil and t ~= 'cmd')) and api.nvim_win_is_valid(win) and api.nvim_win_get_config(win) - if cfg and (tgt or not cfg.hide) then + if current_cfg and (tgt or not current_cfg.hide) then local texth = api.nvim_win_text_height(win, { max_height = o.lines }) local top = { mopt.msgsep, 'MsgSeparator' } - cfg = { hide = false, relative = 'laststatus', col = 10000 } ---@type table + ---@type vim.api.keyset.win_config + local cfg = { hide = false, relative = 'laststatus', col = 10000 } cfg.row, cfg.height, cfg.border = win_row_height_border(t, texth.all) cfg.border = cfg.border and t ~= 'msg' and { '', top, '', '', '', '', '', '' } or nil cfg.mouse = tgt == 'cmd' or t == 'msg' or nil diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index b2798b8cd6..34490437b8 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -245,9 +245,9 @@ function M.watchdirs(path, opts, callback) for name, type in vim.fs.dir(path, { depth = max_depth, - skip = function(name) + skip = function(dir) return not opts.exclude_pattern - or opts.exclude_pattern:match(vim.fs.joinpath(path, name)) == nil + or opts.exclude_pattern:match(vim.fs.joinpath(path, dir)) == nil end, }) do diff --git a/runtime/lua/vim/deprecated/health.lua b/runtime/lua/vim/deprecated/health.lua index 297587b248..26dc77011b 100644 --- a/runtime/lua/vim/deprecated/health.lua +++ b/runtime/lua/vim/deprecated/health.lua @@ -1,7 +1,7 @@ local M = {} local health = vim.health -local deprecated = {} ---@type [string, table, string][] +local deprecated = {} ---@type table function M.check() if next(deprecated) == nil then diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index cbd7e8f5d1..a439482dbc 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -309,7 +309,7 @@ local M = vim._defer_require('vim.diagnostic', { --- signs = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } } --- }) --- ``` ---- @field text? table +--- @field text? table --- --- A table mapping |diagnostic-severity| to the highlight group used for the --- line number where the sign is placed. @@ -441,6 +441,7 @@ local all_namespaces = {} ---@param namespace integer? Update the options for the given namespace. --- When omitted, update the global diagnostic options. ---@return vim.diagnostic.Opts? : Current diagnostic config if {opts} is omitted. +---@overload fun(opts?: nil, namespace?: integer): vim.diagnostic.Opts function M.config(opts, namespace) return M._config.config(opts, namespace) end diff --git a/runtime/lua/vim/diagnostic/_config.lua b/runtime/lua/vim/diagnostic/_config.lua index d3bd5ed5c1..a3fa60494e 100644 --- a/runtime/lua/vim/diagnostic/_config.lua +++ b/runtime/lua/vim/diagnostic/_config.lua @@ -95,6 +95,7 @@ end --- @param namespace integer? Update the options for the given namespace. --- When omitted, update the global diagnostic options. --- @return vim.diagnostic.Opts? : Current diagnostic config if {opts} is omitted. +--- @overload fun(opts?: nil, namespace?: integer): vim.diagnostic.Opts function M.config(opts, namespace) vim.validate('opts', opts, 'table', true) vim.validate('namespace', namespace, 'number', true) diff --git a/runtime/lua/vim/diagnostic/_handlers.lua b/runtime/lua/vim/diagnostic/_handlers.lua index 36233d9b85..d15b4f21de 100644 --- a/runtime/lua/vim/diagnostic/_handlers.lua +++ b/runtime/lua/vim/diagnostic/_handlers.lua @@ -219,8 +219,11 @@ function M.signs.show(namespace, bufnr, diagnostics, opts) for _, diagnostic0 in ipairs(diagnostics) do if diagnostic0.lnum <= line_count then + local severity_name = ( + severity --[[@as table]] + )[diagnostic0.severity] api.nvim_buf_set_extmark(bufnr, ns.user_data.sign_ns, diagnostic0.lnum, 0, { - sign_text = text[diagnostic0.severity] or text[severity[diagnostic0.severity]] or 'U', + sign_text = text[diagnostic0.severity] or text[severity_name] or 'U', sign_hl_group = sign_highlight_map[diagnostic0.severity], number_hl_group = numhl[diagnostic0.severity], line_hl_group = linehl[diagnostic0.severity], diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua index 8cfc4ab79d..a10dc7340b 100644 --- a/runtime/lua/vim/health.lua +++ b/runtime/lua/vim/health.lua @@ -135,11 +135,11 @@ local function filepath_to_healthcheck(path) else local rtp_lua = vim .iter(vim.api.nvim_get_runtime_file('lua/', true)) - :map(function(rtp_lua) - return vim.fs.abspath(vim.fs.normalize(rtp_lua)) + :map(function(dir) + return vim.fs.abspath(vim.fs.normalize(dir)) end) - :find(function(rtp_lua) - return vim.fs.relpath(rtp_lua, path) + :find(function(dir) + return vim.fs.relpath(dir, path) end) -- "/path/to/rtp/lua/foo/bar/health.lua" => "foo/bar/health.lua" -- "/another/rtp/lua/baz/health/init.lua" => "baz/health/init.lua" @@ -314,6 +314,8 @@ function M.error(msg, ...) check_summary['error'] = check_summary['error'] + 1 end +---@param path string +---@return string local path2name = function(path) if vim.fs.ext(path) == 'lua' then -- Lua: transform "../lua/vim/lsp/health.lua" into "vim.lsp" @@ -340,13 +342,12 @@ end local PATTERNS = { '/autoload/health/*.vim', '/lua/**/**/health.lua', '/lua/**/**/health/init.lua' } --- :checkhealth completion function used by cmdexpand.c get_healthcheck_names() M._complete = function() - local unique = vim ---@type table - ---@param pattern string + ---@type table + local unique = vim .iter(vim.tbl_map(function(pattern) return vim.tbl_map(path2name, vim.api.nvim_get_runtime_file(pattern, true)) end, PATTERNS)) :flatten() - ---@param t table :fold({}, function(t, name) t[name] = true -- Remove duplicates return t diff --git a/runtime/lua/vim/iter.lua b/runtime/lua/vim/iter.lua index 136941e233..f81c43b3bf 100644 --- a/runtime/lua/vim/iter.lua +++ b/runtime/lua/vim/iter.lua @@ -1313,8 +1313,11 @@ function IterArray.new(t) }, IterArray) end -return setmetatable(M, { +setmetatable(M, { __call = function(_, ...) return Iter.new(...) end, }) + +-- Return M separately to work around EmmyLuaLs/emmylua-analyzer-rust#1240. +return M diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 2cd6f312b0..9f69d0c1dc 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -284,8 +284,9 @@ end --- --- @param name string --- @param cfg vim.lsp.Config +--- @type vim.lsp.config --- @diagnostic disable-next-line:assign-type-mismatch -function lsp.config(name, cfg) +lsp.config = function(name, cfg) local _, _ = name, cfg -- ignore unused -- dummy proto for docs end @@ -321,6 +322,7 @@ end --- @class vim.lsp.config --- @field [string] vim.lsp.Config? --- @field package _configs table +--- @overload fun(name: string, cfg: vim.lsp.Config) lsp.config = setmetatable({ _configs = {} }, { --- @param self vim.lsp.config --- @param name string @@ -332,7 +334,7 @@ lsp.config = setmetatable({ _configs = {} }, { if not rconfig.resolved_config then if name == '*' then - rconfig.resolved_config = lsp.config._configs['*'] or {} + rconfig.resolved_config = self._configs['*'] or {} return rconfig.resolved_config end @@ -355,7 +357,7 @@ lsp.config = setmetatable({ _configs = {} }, { rconfig.resolved_config = vim.tbl_deep_extend( 'force', - lsp.config._configs['*'] or {}, + self._configs['*'] or {}, rtp_config or {}, self._configs[name] or {} ) @@ -1449,8 +1451,10 @@ end ---@param pattern string Pattern used to find a workspace symbol ---@param flags string See |tag-function| --- ----@return table[] tags A list of matching tags +---@return table[]|vim.NIL tags A list of matching tags, or `vim.NIL` to use the built-in tags. function lsp.tagfunc(pattern, flags) + -- EmmyLua incorrectly treats function exports referenced by @module as non-callable. + --- @diagnostic disable-next-line:call-non-callable EmmyLuaLs/emmylua-analyzer-rust#1238 return vim.lsp._tagfunc(pattern, flags) end diff --git a/runtime/lua/vim/lsp/_capability.lua b/runtime/lua/vim/lsp/_capability.lua index 22d5f0b560..977c1ab355 100644 --- a/runtime/lua/vim/lsp/_capability.lua +++ b/runtime/lua/vim/lsp/_capability.lua @@ -48,9 +48,11 @@ local buf_capabilities = {} local M = {} M.__index = M +---@generic T: vim.lsp.Capability +---@param self T ---@param bufnr integer ----@return self -function M:new(bufnr) +---@return T +function M.new(self, bufnr) -- `self` in the `new()` function refers to the concrete type (i.e., the metatable). -- `Class` may be a subtype of `Capability`, as it supports inheritance. ---@type vim.lsp.Capability @@ -63,7 +65,7 @@ function M:new(bufnr) all_capabilities[Class.name] = Class end - ---@type vim.lsp.Capability + ---@type T self = setmetatable({}, Class) self.bufnr = bufnr self.augroup = api.nvim_create_augroup(string.format('nvim.lsp.%s:%s', self.name, bufnr), { diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua index bf97a7a8fe..63369764f9 100644 --- a/runtime/lua/vim/lsp/_folding_range.lua +++ b/runtime/lua/vim/lsp/_folding_range.lua @@ -387,9 +387,9 @@ function M.foldtext(lnum) line = state.row_text[row] or line local ok, parser = pcall(function() - local parser = vim.treesitter.get_string_parser(line, lang) - parser:parse(true) - return parser + local string_parser = vim.treesitter.get_string_parser(line, lang) + string_parser:parse(true) + return string_parser end) if not ok then return line diff --git a/runtime/lua/vim/lsp/_snippet_grammar.lua b/runtime/lua/vim/lsp/_snippet_grammar.lua index 5424c41955..4d70d91830 100644 --- a/runtime/lua/vim/lsp/_snippet_grammar.lua +++ b/runtime/lua/vim/lsp/_snippet_grammar.lua @@ -89,12 +89,11 @@ M.NodeType = Type --- @class vim.snippet.FormatData: { capture: number, modifier?: string, if_text?: string, else_text?: string } --- @class vim.snippet.SnippetData: { children: vim.snippet.Node[] } ---- @type vim.snippet.Node local Node = {} +--- @param self vim.snippet.Node --- @return string ---- @diagnostic disable-next-line: inject-field -function Node:__tostring() +function Node.__tostring(self) local node_text = {} local type, data = self.type, self.data if type == Type.Snippet then diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua index a71c831199..ee462b8aa4 100644 --- a/runtime/lua/vim/lsp/_tagfunc.lua +++ b/runtime/lua/vim/lsp/_tagfunc.lua @@ -84,6 +84,9 @@ local function query_workspace_symbols(pattern) return results end +---@param pattern string +---@param flags string +---@return table[]|vim.NIL local function tagfunc(pattern, flags) -- avoid definition/symbol queries for insert completion if string.match(flags, 'i') then diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 7a041a7dab..0eca0bc420 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -796,7 +796,7 @@ function M.rename(new_name, opts) if client:supports_method('textDocument/prepareRename') then local params = util.make_position_params(win, client.offset_encoding) - ---@param result? lsp.Range|{ range: lsp.Range, placeholder: string } + ---@param result? lsp.PrepareRenameResult client:request('textDocument/prepareRename', params, function(err, result) if err or result == nil then if next(clients, idx) then @@ -816,10 +816,8 @@ function M.rename(new_name, opts) local range ---@type vim.Range? if result.start then - ---@cast result lsp.Range range = vim.range.lsp(bufnr, result, client.offset_encoding) elseif result.range then - ---@cast result { range: lsp.Range, placeholder: string } range = vim.range.lsp(bufnr, result.range, client.offset_encoding) end if range then @@ -841,8 +839,6 @@ function M.rename(new_name, opts) prompt_opts.default = result.placeholder elseif result.start then prompt_opts.default = get_text_at_range(result, client.offset_encoding) - elseif result.range then - prompt_opts.default = get_text_at_range(result.range, client.offset_encoding) else prompt_opts.default = cword end @@ -1273,6 +1269,8 @@ local function on_code_action_results(results, opts) return end + -- Work around incorrect union narrowing: EmmyLuaLs/emmylua-analyzer-rust#1239. + ---@cast action lsp.CodeAction if action.disabled then vim.notify(action.disabled.reason, vim.log.levels.ERROR) return diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 0ce4087f77..984447720c 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -417,7 +417,7 @@ function Client.create(config) local name = get_name(id, config) --- @type vim.lsp.Client - local self = { + local self = setmetatable({ id = id, config = config, handlers = config.handlers or {}, @@ -455,7 +455,7 @@ function Client.create(config) --- @deprecated use client.progress instead messages = { name = name, messages = {}, progress = {}, status = {} }, - } + }, Client) self.capabilities = vim.tbl_deep_extend('force', lsp.protocol.make_client_capabilities(), self.capabilities or {}) @@ -516,8 +516,6 @@ function Client.create(config) }) end - setmetatable(self, Client) - method_wrapper(self, Client, 'request') method_wrapper(self, Client, 'request_sync') method_wrapper(self, Client, 'notify') @@ -768,19 +766,19 @@ function Client:request(method, params, handler, bufnr) local request_registered = false -- NOTE: rpc.request might call an in-process (Lua) server, thus may be synchronous. - local success, request_id = self.rpc.request(method, params, function(err, result, request_id) + local success, request_id = self.rpc.request(method, params, function(err, result, id) handler(err, result, { method = method, client_id = self.id, - request_id = request_id, + request_id = id, bufnr = bufnr, params = params, version = version, }) - end, function(request_id) + end, function(id) -- Called when the server sends a response to the request (including cancelled acknowledgment). if request_registered then - self:_process_request(request_id, 'complete') + self:_process_request(id, 'complete') end already_responded = true end) diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 17f40fa298..ebcdef0b1e 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -10,12 +10,12 @@ local Capability = require('vim.lsp._capability') ---@class (private) vim.lsp.codelens.RowLenses ---@field lenses lsp.CodeLens[] ---@field version? integer `TextDocument` version most recently applied to this row. ---- + ---@class (private) vim.lsp.codelens.ClientState ---@field row_lenses table ---@field namespace integer ---@field version? integer `TextDocument` version current state corresponds to. ---- + ---@class (private) vim.lsp.codelens.Provider : vim.lsp.Capability ---@field active table --- diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index e23abafb97..9951d39f25 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -163,7 +163,7 @@ local function tokens_to_ranges(data, bufnr, client, request, ranges) if last_insert_idx < #ranges then local needs_insert = true - local idx = vim.list.bisect(ranges, { line = range.line }, { + local idx = vim.list.bisect(ranges, range, { lo = last_insert_idx, key = function(highlight) return highlight.line diff --git a/runtime/lua/vim/net.lua b/runtime/lua/vim/net.lua index ff3a1558f1..96e67c1eef 100644 --- a/runtime/lua/vim/net.lua +++ b/runtime/lua/vim/net.lua @@ -107,6 +107,7 @@ function M.request(method, url, opts, on_response) return (b == nil and true) or (type(b) == 'string' and not b:match('^@')) end, true, 'body should be string and not start with @') vim.validate('on_response', on_response, 'function', true) + ---@cast on_response vim.net.request.ResponseFunc? local retry = opts.retry or 3 diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index 91126b9fb8..754224ec31 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -953,6 +953,7 @@ local function pack_add(plug, load) active_plugins[plug.path] = { plug = plug, id = n_active_plugins } if vim.is_callable(load) then + ---@cast load -boolean load({ spec = vim.deepcopy(plug.spec), path = plug.path }) return end @@ -1389,6 +1390,7 @@ end --- @class vim.pack.keyset.update --- @inlinedoc +--- @field package _ex? boolean --- @field force? boolean Whether to skip confirmation and make updates immediately. Default `false`. --- --- @field offline? boolean Whether to skip downloading new updates. Default: `false`. @@ -1520,6 +1522,7 @@ end --- @class vim.pack.keyset.del --- @inlinedoc +--- @field package _ex? boolean --- @field force? boolean Whether to allow deleting an active plugin. Default `false`. --- Remove plugins from disk diff --git a/runtime/lua/vim/pos.lua b/runtime/lua/vim/pos.lua index bb2e2a0597..edb0f801e5 100644 --- a/runtime/lua/vim/pos.lua +++ b/runtime/lua/vim/pos.lua @@ -345,8 +345,8 @@ function M.offset(buf, offset) local lnum = vim.list.bisect( setmetatable({}, { - __index = function(_, lnum) - return api.nvim_buf_get_offset(buf, lnum - 1) + __index = function(_, idx) + return api.nvim_buf_get_offset(buf, idx - 1) end, }), offset, @@ -364,6 +364,6 @@ setmetatable(M, { return M.new(...) end, }) ----@cast M +fun(buf: integer, row: integer, col: integer): vim.Pos +---@cast M vim.Pos & fun(buf: integer, row: integer, col: integer): vim.Pos return M diff --git a/runtime/lua/vim/provider/health.lua b/runtime/lua/vim/provider/health.lua index 54bafcf9d6..fc26efa6ae 100644 --- a/runtime/lua/vim/provider/health.lua +++ b/runtime/lua/vim/provider/health.lua @@ -3,6 +3,9 @@ local iswin = vim.fn.has('win32') == 1 local M = {} +---@param cmd string[] +---@return boolean +---@return string? local function cmd_ok(cmd) local result = vim.system(cmd, { text = true }):wait() return result.code == 0, result.stdout diff --git a/runtime/lua/vim/range.lua b/runtime/lua/vim/range.lua index 0689b15d81..9f11f30d32 100644 --- a/runtime/lua/vim/range.lua +++ b/runtime/lua/vim/range.lua @@ -465,7 +465,6 @@ setmetatable(M, { return M.new(...) end, }) ----@cast M +fun(start: vim.Pos, end_: vim.Pos): vim.Range ----@cast M +fun(buf: integer, start_row: integer, start_col: integer, end_row: integer, end_col: integer): vim.Range +---@cast M vim.Range & (fun(start: vim.Pos, end_: vim.Pos): vim.Range) & (fun(buf: integer, start_row: integer, start_col: integer, end_row: integer, end_col: integer): vim.Range) return M diff --git a/runtime/lua/vim/re.lua b/runtime/lua/vim/re.lua index 5677230946..f36cfc4d83 100644 --- a/runtime/lua/vim/re.lua +++ b/runtime/lua/vim/re.lua @@ -214,6 +214,7 @@ local exp = m.P{ "Exp", ((m.V"Definition" / firstdef) * (m.V"Definition" % adddef)^0) / mm.P } +--- @type vim.lpeg.Pattern local pattern = S * m.Cg(m.Cc(false), "G") * exp / mm.P * (-any + patt_error) diff --git a/runtime/lua/vim/treesitter/_select.lua b/runtime/lua/vim/treesitter/_select.lua index e77e51a0c5..7d9408b7b4 100644 --- a/runtime/lua/vim/treesitter/_select.lua +++ b/runtime/lua/vim/treesitter/_select.lua @@ -31,7 +31,7 @@ local history = { --- @field ltree vim.treesitter.LanguageTree --- @field region Range4 -local M = {} +local M = { TEST_SWITCH_PRIORITY = false } --- @param node vim.treesitter.select.node --- @return string @@ -96,10 +96,8 @@ end --- @param ltree vim.treesitter.LanguageTree --- @return vim.treesitter.select.node.top local function create_top_node(tree, region, ltree) - --- @type vim.treesitter.select.node.top local self = { node = tree:root(), - top = {} --[[@as any]], ltree = ltree, region = region, } diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index f7e22eb5ff..04e0723b5b 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -298,7 +298,7 @@ end --- @param metadata vim.treesitter.query.TSMetadata --- @return string? local function get_url(match, bufnr, capture, metadata) - ---@type string|number|nil + ---@type string|integer|nil local url = metadata[capture] and metadata[capture].url if not url or type(url) == 'string' then @@ -369,7 +369,6 @@ local function on_range_impl( self:for_each_highlight_state(function(state) subtree_counter = subtree_counter + 1 local root_node = state.tstree:root() - ---@type { [1]: integer, [2]: integer, [3]: integer, [4]: integer } local root_range = { root_node:range() } if diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 7b287f26c3..a956fc155f 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -140,7 +140,7 @@ function LanguageTree.new(source, lang, opts) local injections = opts.injections or {} --- @type vim.treesitter.LanguageTree - local self = { + local self = setmetatable({ _source = source, _lang = lang, _children = {}, @@ -159,9 +159,7 @@ function LanguageTree.new(source, lang, opts) _cb_queues = {}, _callbacks = {}, _callbacks_rec = {}, - } - - setmetatable(self, LanguageTree) + }, LanguageTree) if vim.g.__ts_debug and type(vim.g.__ts_debug) == 'number' then self:_set_logger() diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua index c3406d8c1b..ec567df747 100644 --- a/runtime/lua/vim/version.lua +++ b/runtime/lua/vim/version.lua @@ -53,6 +53,8 @@ --- 1.2 - 2.3.0 is 1.2.0 - 2.3.0 --- ``` +---@class (internal) vim.VersionModule +---@operator call: vim.Version local M = {} ---@nodoc @@ -101,6 +103,7 @@ local function cmp_prerel(prerel1, prerel2) end end +---@param key string|integer function Version:__index(key) return type(key) == 'number' and ({ self.major, self.minor, self.patch })[key] or Version[key] end diff --git a/runtime/pack/dist/opt/nvim.difftool/lua/difftool.lua b/runtime/pack/dist/opt/nvim.difftool/lua/difftool.lua index 7fb6e70b6b..c841d5360d 100644 --- a/runtime/pack/dist/opt/nvim.difftool/lua/difftool.lua +++ b/runtime/pack/dist/opt/nvim.difftool/lua/difftool.lua @@ -261,7 +261,7 @@ local function diff_dirs_builtin(left_dir, right_dir, opt) -- Detect possible renames if opt.rename.detect then for left_rel, left_path in pairs(left_only) do - ---@type {similarity: number, path: string?, rel: string} + ---@type {similarity: number, path: string?, rel: string?} local best_match = { similarity = opt.rename.similarity, path = nil } for right_rel, right_path in pairs(right_only) do diff --git a/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua b/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua index 7daa62597b..09c9a24fa0 100644 --- a/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua +++ b/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua @@ -1337,16 +1337,7 @@ local styletable_funcs = { local function state_generate_style(state) vim._with({ win = state.winid }, function() for _, fn in ipairs(styletable_funcs) do - --- @type string? - local cond - if type(fn) == 'table' then - cond = fn[2] --[[@as string]] - --- @type function - fn = fn[1] - end - if not cond or cond(state) then - fn(state) - end + fn(state) end end) end diff --git a/runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua b/runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua index 658a90337d..ca83691177 100644 --- a/runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua +++ b/runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua @@ -49,12 +49,27 @@ local function treefy(ent, _tree, _last) return tree end ---- @class (private) vim.undotree.graph_line ---- @field kind 'node'|'remove'|'branch'|'remove+branch'|'nochange_remove' +--- @class (private) vim.undotree.graph_line.base --- @field index integer --- @field node_count integer ---- @field node integer|integer[] ---- @field index2 integer? -- for branch-index in `remove+branch` + +--- @class (private) vim.undotree.graph_line.node: vim.undotree.graph_line.base +--- @field kind 'node'|'remove'|'nochange_remove' +--- @field node integer + +--- @class (private) vim.undotree.graph_line.branch: vim.undotree.graph_line.base +--- @field kind 'branch' +--- @field node integer[] + +--- @class (private) vim.undotree.graph_line.remove_branch: vim.undotree.graph_line.base +--- @field kind 'remove+branch' +--- @field node integer +--- @field index2 integer + +--- @alias vim.undotree.graph_line +--- | vim.undotree.graph_line.node +--- | vim.undotree.graph_line.branch +--- | vim.undotree.graph_line.remove_branch --- @param tree vim.undotree.tree --- @return vim.undotree.graph_line[] @@ -181,13 +196,15 @@ local function buf_apply_graph_lines(tree, graph_lines, buf, meta, find_seq) --- @type string? local line if v.kind == 'node' then + -- Work around tagged union narrowing: EmmyLuaLs/emmylua-analyzer-rust#1241. + local seq = v.node --[[@as integer]] line = ('| '):rep(v.index - 1) .. '*' .. (' |'):rep(v.node_count - v.index) .. ' ' - .. v.node + .. seq .. ' (' - .. undo_fmt_time(tree[v.node].time) + .. undo_fmt_time(tree[seq].time) .. ')' elseif v.kind == 'remove' then line = ('| '):rep(v.index - 1) .. (' /'):rep(v.node_count - v.index) diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua index 355109cb07..71a89de00e 100755 --- a/src/gen/gen_vimdoc.lua +++ b/src/gen/gen_vimdoc.lua @@ -257,6 +257,11 @@ local config = { fun.table = nil end + -- Render the callable version module as ordinary module functions. + if fun.class == 'vim.VersionModule' then + fun.classvar = nil + end + if fun.classvar or vim.startswith(fun.name, 'vim.') or fun.module == 'vim.iter' then return end @@ -345,6 +350,9 @@ local config = { }, fn_xform = function(fun) fun.name = fun.name:gsub('result%.', '') + if fun.module == 'vim.lsp' and fun.name == 'config' then + fun.table = nil + end if fun.module == 'vim.lsp.protocol' then fun.classvar = nil end @@ -674,7 +682,7 @@ local function get_class(ty, classes) return end - local cty = ty:gsub('%s*|%s*nil', '?'):gsub('?$', ''):gsub('%[%]$', '') + local cty = ty:gsub('%s*|%s*nil', '?'):gsub('?$', ''):gsub('%[%]$', ''):gsub('%b<>$', '') return classes[cty] end diff --git a/src/gen/luacats_grammar.lua b/src/gen/luacats_grammar.lua index 02d086435f..31616a1805 100644 --- a/src/gen/luacats_grammar.lua +++ b/src/gen/luacats_grammar.lua @@ -145,14 +145,15 @@ local typedef = P({ 'typedef', typedef = C(v.type), - type = v.ty * rep_array_opt_postfix * rep(Pf('|') * v.ty * rep_array_opt_postfix), + type = v.ty * rep_array_opt_postfix * rep(Sf('|&') * v.ty * rep_array_opt_postfix), ty = v.composite + paren(v.typedef), composite = (v.types * array_postfix) + (v.types * opt_postfix) + (P(ty_ident) * P('...')) -- Generic vararg + v.types, - types = v.fun + v.generics + v.kv_table + v.tuple + v.dict + v.table_literal + ty_prims, + types = v.keyof + v.fun + v.generics + v.kv_table + v.tuple + v.dict + v.table_literal + ty_prims, + keyof = P('keyof') * ws * v.ty, tuple = Pf('[') * comma1(v.type) * Plf(']'), dict = Pf('{') * comma1(Pf('[') * v.type * Pf(']') * colon * v.type) * Plf('}'), kv_table = Pf('table') * Pf('<') * v.type * Pf(',') * v.type * Plf('>'), diff --git a/test/functional/script/luacats_grammar_spec.lua b/test/functional/script/luacats_grammar_spec.lua index ccb5bbbeb2..4e7534e7a0 100644 --- a/test/functional/script/luacats_grammar_spec.lua +++ b/test/functional/script/luacats_grammar_spec.lua @@ -34,6 +34,14 @@ describe('luacats grammar', function() desc = 'this is a description', }) + test( + '@overload fun(opts: vim.list.bisect.Opts & { key: string & keyof T & keyof Q }): integer', + { + kind = 'overload', + type = 'fun(opts: vim.list.bisect.Opts & { key: string & keyof T & keyof Q }): integer', + } + ) + test('@param hello vim.type?|string? this is a description', { kind = 'param', name = 'hello', @@ -244,6 +252,8 @@ describe('luacats grammar', function() { 'number[][][]' }, { 'number[][]?' }, { 'string|integer[][]?' }, + { 'vim.type & { key: string|function }' }, + { '(vim.type & { key: string })|nil' }, -- tuples { '[string]' },