From 3572319b4cb1a4163624a5fe328886f1928dbc4a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 18 Oct 2024 11:33:12 +0100 Subject: [PATCH] feat(vim.validate): improve fast form and deprecate spec form Problem: `vim.validate()` takes two forms when it only needs one. Solution: - Teach the fast form all the features of the spec form. - Deprecate the spec form. - General optimizations for both forms. - Add a `message` argument which can be used alongside or in place of the `optional` argument. --- runtime/doc/deprecated.txt | 1 + runtime/doc/lua.txt | 76 +++--- runtime/doc/news.txt | 2 + runtime/lua/vim/_editor.lua | 12 +- runtime/lua/vim/_watch.lua | 8 +- runtime/lua/vim/diagnostic.lua | 56 ++--- runtime/lua/vim/fs.lua | 2 +- runtime/lua/vim/func/_memoize.lua | 6 +- runtime/lua/vim/hl.lua | 14 +- runtime/lua/vim/keymap.lua | 18 +- runtime/lua/vim/lsp/client.lua | 34 ++- runtime/lua/vim/secure.lua | 16 +- runtime/lua/vim/shared.lua | 295 ++++++++++++------------ runtime/lua/vim/termcap.lua | 6 +- runtime/lua/vim/treesitter.lua | 8 +- runtime/lua/vim/treesitter/language.lua | 6 +- runtime/lua/vim/ui.lua | 6 +- scripts/bump_deps.lua | 6 +- scripts/gen_help_html.lua | 52 ++--- test/functional/lua/vim_spec.lua | 105 +++++++-- 20 files changed, 355 insertions(+), 374 deletions(-) diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index 7a3e2664c2..28771dbd28 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -22,6 +22,7 @@ API LUA - vim.region() Use |getregionpos()| instead. - *vim.highlight* Renamed to |vim.hl|. +- vim.validate(opts: table) Use form 1. See |vim.validate()|. DIAGNOSTICS - *vim.diagnostic.goto_next()* Use |vim.diagnostic.jump()| with `{count=1, float=true}` instead. diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 280e9ebf79..2f8488a128 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2398,31 +2398,29 @@ vim.trim({s}) *vim.trim()* • |lua-patterns| • https://www.lua.org/pil/20.2.html -vim.validate({opt}) *vim.validate()* + *vim.validate()* +vim.validate({name}, {value}, {validator}, {optional}, {message}) Validate function arguments. This function has two valid forms: - 1. vim.validate(name: str, value: any, type: string, optional?: bool) - 2. vim.validate(spec: table) - - Form 1 validates that argument {name} with value {value} has the type - {type}. {type} must be a value returned by |lua-type()|. If {optional} is - true, then {value} may be null. This form is significantly faster and - should be preferred for simple cases. - - Example: >lua - function vim.startswith(s, prefix) + 1. `vim.validate(name, value, validator[, optional][, message])` + Validates that argument {name} with value {value} satisfies + {validator}. If {optional} is given and is `true`, then {value} may be + `nil`. If {message} is given, then it is used as the expected type in + the error message. + Example: >lua + function vim.startswith(s, prefix) vim.validate('s', s, 'string') vim.validate('prefix', prefix, 'string') ... end < - - Form 2 validates a parameter specification (types and values). Specs are - evaluated in alphanumeric order, until the first failure. - - Usage example: >lua - function user.new(name, age, hobbies) + 2. `vim.validate(spec)` (deprecated) where `spec` is of type + `table)` + Validates a argument specification. Specs are evaluated in alphanumeric + order, until the first failure. + Example: >lua + function user.new(name, age, hobbies) vim.validate{ name={name, 'string'}, age={age, 'number'}, @@ -2433,40 +2431,44 @@ vim.validate({opt}) *vim.validate()* < Examples with explicit argument values (can be run directly): >lua - vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}} + vim.validate('arg1', {'foo'}, 'table') + --> NOP (success) + vim.validate('arg2', 'foo', 'string') --> NOP (success) - vim.validate{arg1={1, 'table'}} + vim.validate('arg1', 1, 'table') --> error('arg1: expected table, got number') - vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}} + vim.validate('arg1', 3, function(a) return (a % 2) == 0 end, 'even number') --> error('arg1: expected even number, got 3') < If multiple types are valid they can be given as a list. >lua - vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}} + vim.validate('arg1', {'foo'}, {'table', 'string'}) + vim.validate('arg2', 'foo', {'table', 'string'}) -- NOP (success) - vim.validate{arg1={1, {'string', 'table'}}} + vim.validate('arg1', 1, {'string', 'table'}) -- error('arg1: expected string|table, got number') < + Note: ~ + • `validator` set to a value returned by |lua-type()| provides the best + performance. + Parameters: ~ - • {opt} (`table`) Names of parameters to validate. Each key is a - parameter name; each value is a tuple in one of these forms: - 1. (arg_value, type_name, optional) - • arg_value: argument value - • type_name: string|table type name, one of: ("table", "t", - "string", "s", "number", "n", "boolean", "b", "function", - "f", "nil", "thread", "userdata") or list of them. - • optional: (optional) boolean, if true, `nil` is valid - 2. (arg_value, fn, msg) - • arg_value: argument value - • fn: any function accepting one argument, returns true if - and only if the argument is valid. Can optionally return - an additional informative error message as the second - returned value. - • msg: (optional) error string if validation fails + • {name} (`string`) Argument name + • {value} (`string`) Argument value + • {validator} (`vim.validate.Validator`) + • (`string|string[]`): Any value that can be returned + from |lua-type()| in addition to `'callable'`: + `'boolean'`, `'callable'`, `'function'`, `'nil'`, + `'number'`, `'string'`, `'table'`, `'thread'`, + `'userdata'`. + • (`fun(val:any): boolean, string?`) A function that + returns a boolean and an optional string message. + • {optional} (`boolean?`) Argument is optional (may be omitted) + • {message} (`string?`) message when validation fails ============================================================================== diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 9045f9f669..adba659851 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -200,6 +200,8 @@ LSP LUA • |vim.fs.rm()| can delete files and directories. +• |vim.validate()| now has a new signature which uses less tables, + is more peformant and easier to read. OPTIONS diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 7aba7498f9..58283ac64b 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -467,13 +467,11 @@ vim.cmd = setmetatable({}, { -- These are the vim.env/v/g/o/bo/wo variable magic accessors. do - local validate = vim.validate - --- @param scope string --- @param handle? false|integer --- @return vim.var_accessor local function make_dict_accessor(scope, handle) - validate('scope', scope, 'string') + vim.validate('scope', scope, 'string') local mt = {} function mt:__newindex(k, v) return vim._setvar(scope, handle or 0, k, v) @@ -589,7 +587,7 @@ end ---@param timeout integer Number of milliseconds to wait before calling `fn` ---@return table timer luv timer object function vim.defer_fn(fn, timeout) - vim.validate({ fn = { fn, 'c', true } }) + vim.validate('fn', fn, 'callable', true) local timer = assert(vim.uv.new_timer()) timer:start( timeout, @@ -680,10 +678,8 @@ function vim.on_key(fn, ns_id) return vim.tbl_count(on_key_cbs) end - vim.validate({ - fn = { fn, 'c', true }, - ns_id = { ns_id, 'n', true }, - }) + vim.validate('fn', fn, 'callable', true) + vim.validate('ns_id', ns_id, 'number', true) if ns_id == nil or ns_id == 0 then ns_id = vim.api.nvim_create_namespace('') diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 7a306d1123..13894c6147 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -59,9 +59,9 @@ end --- @param callback vim._watch.Callback Callback for new events --- @return fun() cancel Stops the watcher function M.watch(path, opts, callback) - vim.validate('path', path, 'string', false) + vim.validate('path', path, 'string') vim.validate('opts', opts, 'table', true) - vim.validate('callback', callback, 'function', false) + vim.validate('callback', callback, 'function') opts = opts or {} @@ -125,9 +125,9 @@ end --- @param callback vim._watch.Callback Callback for new events --- @return fun() cancel Stops the watcher function M.watchdirs(path, opts, callback) - vim.validate('path', path, 'string', false) + vim.validate('path', path, 'string') vim.validate('opts', opts, 'table', true) - vim.validate('callback', callback, 'function', false) + vim.validate('callback', callback, 'function') opts = opts or {} local debounce = opts.debounce or 500 diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 3570956efd..392db5b800 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -478,7 +478,7 @@ end --- @return vim.Diagnostic[] local function reformat_diagnostics(format, diagnostics) vim.validate('format', format, 'function') - vim.validate({ diagnostics = { diagnostics, vim.islist, 'a list of diagnostics' } }) + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') local formatted = vim.deepcopy(diagnostics, true) for _, diagnostic in ipairs(formatted) do @@ -1056,7 +1056,7 @@ end function M.set(namespace, bufnr, diagnostics, opts) vim.validate('namespace', namespace, 'number') vim.validate('bufnr', bufnr, 'number') - vim.validate({ diagnostics = { diagnostics, vim.islist, 'a list of diagnostics' } }) + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) @@ -1336,7 +1336,7 @@ M.handlers.signs = { show = function(namespace, bufnr, diagnostics, opts) vim.validate('namespace', namespace, 'number') vim.validate('bufnr', bufnr, 'number') - vim.validate({ diagnostics = { diagnostics, vim.islist, 'a list of diagnostics' } }) + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) @@ -1457,7 +1457,7 @@ M.handlers.underline = { show = function(namespace, bufnr, diagnostics, opts) vim.validate('namespace', namespace, 'number') vim.validate('bufnr', bufnr, 'number') - vim.validate({ diagnostics = { diagnostics, vim.islist, 'a list of diagnostics' } }) + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) @@ -1524,7 +1524,7 @@ M.handlers.virtual_text = { show = function(namespace, bufnr, diagnostics, opts) vim.validate('namespace', namespace, 'number') vim.validate('bufnr', bufnr, 'number') - vim.validate({ diagnostics = { diagnostics, vim.islist, 'a list of diagnostics' } }) + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) @@ -1709,15 +1709,7 @@ end function M.show(namespace, bufnr, diagnostics, opts) vim.validate('namespace', namespace, 'number', true) vim.validate('bufnr', bufnr, 'number', true) - vim.validate({ - diagnostics = { - diagnostics, - function(v) - return v == nil or vim.islist(v) - end, - 'a list of diagnostics', - }, - }) + vim.validate('diagnostics', diagnostics, vim.islist, true, 'a list of diagnostics') vim.validate('opts', opts, 'table', true) if not bufnr or not namespace then @@ -1869,13 +1861,7 @@ function M.open_float(opts, ...) local highlights = {} --- @type table[] local header = if_nil(opts.header, 'Diagnostics:') if header then - vim.validate({ - header = { - header, - { 'string', 'table' }, - "'string' or 'table'", - }, - }) + vim.validate('header', header, { 'string', 'table' }, "'string' or 'table'") if type(header) == 'table' then -- Don't insert any lines for an empty string if string.len(if_nil(header[1], '')) > 0 then @@ -1903,13 +1889,12 @@ function M.open_float(opts, ...) local prefix, prefix_hl_group --- @type string?, string? if prefix_opt then - vim.validate({ - prefix = { - prefix_opt, - { 'string', 'table', 'function' }, - "'string' or 'table' or 'function'", - }, - }) + vim.validate( + 'prefix', + prefix_opt, + { 'string', 'table', 'function' }, + "'string' or 'table' or 'function'" + ) if type(prefix_opt) == 'string' then prefix, prefix_hl_group = prefix_opt, 'NormalFloat' elseif type(prefix_opt) == 'table' then @@ -1923,13 +1908,12 @@ function M.open_float(opts, ...) local suffix, suffix_hl_group --- @type string?, string? if suffix_opt then - vim.validate({ - suffix = { - suffix_opt, - { 'string', 'table', 'function' }, - "'string' or 'table' or 'function'", - }, - }) + vim.validate( + 'suffix', + suffix_opt, + { 'string', 'table', 'function' }, + "'string' or 'table' or 'function'" + ) if type(suffix_opt) == 'string' then suffix, suffix_hl_group = suffix_opt, 'NormalFloat' elseif type(suffix_opt) == 'table' then @@ -2239,7 +2223,7 @@ local errlist_type_map = { ---@param diagnostics vim.Diagnostic[] ---@return table[] : Quickfix list items |setqflist-what| function M.toqflist(diagnostics) - vim.validate({ diagnostics = { diagnostics, vim.islist, 'a list of diagnostics' } }) + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') local list = {} --- @type table[] for _, v in ipairs(diagnostics) do diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index c23cd5af1c..d91eeaf02f 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -229,7 +229,7 @@ end ---@return (string[]) # Normalized paths |vim.fs.normalize()| of all matching items function M.find(names, opts) opts = opts or {} - vim.validate({ names = { names, { 'string', 'table', 'function' } } }) + vim.validate('names', names, { 'string', 'table', 'function' }) vim.validate('path', opts.path, 'string', true) vim.validate('upward', opts.upward, 'boolean', true) vim.validate('stop', opts.stop, 'string', true) diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua index 65210351bf..6e557905a7 100644 --- a/runtime/lua/vim/func/_memoize.lua +++ b/runtime/lua/vim/func/_memoize.lua @@ -39,10 +39,8 @@ end --- @param strong? boolean --- @return F return function(hash, fn, strong) - vim.validate({ - hash = { hash, { 'number', 'string', 'function' } }, - fn = { fn, 'function' }, - }) + vim.validate('hash', hash, { 'number', 'string', 'function' }) + vim.validate('fn', fn, 'function') ---@type table> local cache = {} diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua index 49bbaa3cc6..099efa3c61 100644 --- a/runtime/lua/vim/hl.lua +++ b/runtime/lua/vim/hl.lua @@ -135,19 +135,7 @@ local yank_cancel --- @type fun()? --- - event event structure (default vim.v.event) --- - priority integer priority (default |vim.hl.priorities|`.user`) function M.on_yank(opts) - vim.validate({ - opts = { - opts, - function(t) - if t == nil then - return true - else - return type(t) == 'table' - end - end, - 'a table or nil to configure options (see `:h vim.hl.on_yank`)', - }, - }) + vim.validate('opts', opts, 'table', true) opts = opts or {} local event = opts.event or vim.v.event local on_macro = opts.on_macro or false diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua index 50ca0d2d0e..4c19435ef8 100644 --- a/runtime/lua/vim/keymap.lua +++ b/runtime/lua/vim/keymap.lua @@ -42,12 +42,10 @@ local keymap = {} ---@see |mapcheck()| ---@see |mapset()| function keymap.set(mode, lhs, rhs, opts) - vim.validate({ - mode = { mode, { 's', 't' } }, - lhs = { lhs, 's' }, - rhs = { rhs, { 's', 'f' } }, - opts = { opts, 't', true }, - }) + vim.validate('mode', mode, { 'string', 'table' }) + vim.validate('lhs', lhs, 'string') + vim.validate('rhs', rhs, { 'string', 'function' }) + vim.validate('opts', opts, 'table', true) opts = vim.deepcopy(opts or {}, true) @@ -107,11 +105,9 @@ end ---@param opts? vim.keymap.del.Opts ---@see |vim.keymap.set()| function keymap.del(modes, lhs, opts) - vim.validate({ - mode = { modes, { 's', 't' } }, - lhs = { lhs, 's' }, - opts = { opts, 't', true }, - }) + vim.validate('mode', modes, { 'string', 'table' }) + vim.validate('lhs', lhs, 'string') + vim.validate('opts', opts, 'table', true) opts = opts or {} modes = type(modes) == 'string' and { modes } or modes diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index d5fc5b8908..d9d6b851d0 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -349,24 +349,22 @@ end --- @param config vim.lsp.ClientConfig local function validate_config(config) validate('config', config, 'table') - validate({ - handlers = { config.handlers, 't', true }, - capabilities = { config.capabilities, 't', true }, - cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' }, - cmd_env = { config.cmd_env, 't', true }, - detached = { config.detached, 'b', true }, - name = { config.name, 's', true }, - on_error = { config.on_error, 'f', true }, - on_exit = { config.on_exit, { 'f', 't' }, true }, - on_init = { config.on_init, { 'f', 't' }, true }, - on_attach = { config.on_attach, { 'f', 't' }, true }, - settings = { config.settings, 't', true }, - commands = { config.commands, 't', true }, - before_init = { config.before_init, { 'f', 't' }, true }, - offset_encoding = { config.offset_encoding, 's', true }, - flags = { config.flags, 't', true }, - get_language_id = { config.get_language_id, 'f', true }, - }) + validate('handlers', config.handlers, 'table', true) + validate('capabilities', config.capabilities, 'table', true) + validate('cmd_cwd', config.cmd_cwd, optional_validator(is_dir), 'directory') + validate('cmd_env', config.cmd_env, 'table', true) + validate('detached', config.detached, 'boolean', true) + validate('name', config.name, 'string', true) + validate('on_error', config.on_error, 'function', true) + validate('on_exit', config.on_exit, { 'function', 'table' }, true) + validate('on_init', config.on_init, { 'function', 'table' }, true) + validate('on_attach', config.on_attach, { 'function', 'table' }, true) + validate('settings', config.settings, 'table', true) + validate('commands', config.commands, 'table', true) + validate('before_init', config.before_init, { 'function', 'table' }, true) + validate('offset_encoding', config.offset_encoding, 'string', true) + validate('flags', config.flags, 'table', true) + validate('get_language_id', config.get_language_id, 'function', true) assert( ( diff --git a/runtime/lua/vim/secure.lua b/runtime/lua/vim/secure.lua index 0d3a271ac7..7b1d071270 100644 --- a/runtime/lua/vim/secure.lua +++ b/runtime/lua/vim/secure.lua @@ -132,17 +132,11 @@ end ---@return boolean success true if operation was successful ---@return string msg full path if operation was successful, else error message function M.trust(opts) - vim.validate({ - path = { opts.path, 's', true }, - bufnr = { opts.bufnr, 'n', true }, - action = { - opts.action, - function(m) - return m == 'allow' or m == 'deny' or m == 'remove' - end, - [["allow" or "deny" or "remove"]], - }, - }) + vim.validate('path', opts.path, 'string', true) + vim.validate('bufnr', opts.bufnr, 'number', true) + vim.validate('action', opts.action, function(m) + return m == 'allow' or m == 'deny' or m == 'remove' + end, [["allow" or "deny" or "remove"]]) ---@cast opts vim.trust.opts local path = opts.path diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index fd6a37001d..94a194cde9 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -251,7 +251,8 @@ end ---@param t table Table ---@return table : Table of transformed values function vim.tbl_map(func, t) - vim.validate({ func = { func, 'c' }, t = { t, 't' } }) + vim.validate('func', func, 'callable') + vim.validate('t', t, 'table') --- @cast t table local rettab = {} --- @type table @@ -268,7 +269,8 @@ end ---@param t table (table) Table ---@return T[] : Table of filtered values function vim.tbl_filter(func, t) - vim.validate({ func = { func, 'c' }, t = { t, 't' } }) + vim.validate('func', func, 'callable') + vim.validate('t', t, 'table') --- @cast t table local rettab = {} --- @type table @@ -311,7 +313,7 @@ function vim.tbl_contains(t, value, opts) local pred --- @type fun(v: any): boolean? if opts and opts.predicate then - vim.validate({ value = { value, 'c' } }) + vim.validate('value', value, 'callable') pred = value else pred = function(v) @@ -779,237 +781,226 @@ function vim.endswith(s, suffix) end do - --- @alias vim.validate.LuaType - --- | 'nil' - --- | 'number' - --- | 'string' - --- | 'boolean' - --- | 'table' - --- | 'function' - --- | 'thread' - --- | 'userdata' - --- - --- @alias vim.validate.Type vim.validate.LuaType | 't' | 's' | 'n' | 'f' | 'c' + --- @alias vim.validate.Validator + --- | type|'callable' + --- | (type|'callable')[] + --- | fun(v:any):boolean, string? - local type_names = { - ['table'] = 'table', - t = 'table', - ['string'] = 'string', - s = 'string', - ['number'] = 'number', - n = 'number', - ['boolean'] = 'boolean', + local type_aliases = { b = 'boolean', - ['function'] = 'function', - f = 'function', - ['callable'] = 'callable', c = 'callable', - ['nil'] = 'nil', - ['thread'] = 'thread', - ['userdata'] = 'userdata', + f = 'function', + n = 'number', + s = 'string', + t = 'table', } --- @nodoc - --- @class vim.validate.Spec [any, string|string[], boolean] + --- @class vim.validate.Spec --- @field [1] any Argument value - --- @field [2] vim.validate.Type|vim.validate.Type[]|fun(v:any):boolean, string? Type name, or callable - --- @field [3]? boolean + --- @field [2] vim.validate.Validator Argument validator + --- @field [3]? boolean|string Optional flag or error message - local function _is_type(val, t) + local function is_type(val, t) return type(val) == t or (t == 'callable' and vim.is_callable(val)) end --- @param param_name string - --- @param spec vim.validate.Spec + --- @param val any + --- @param validator vim.validate.Validator + --- @param message? string + --- @param allow_alias? boolean Allow short type names: 'n', 's', 't', 'b', 'f', 'c' --- @return string? - local function is_param_valid(param_name, spec) - if type(spec) ~= 'table' then - return string.format('opt[%s]: expected table, got %s', param_name, type(spec)) - end + local function is_valid(param_name, val, validator, message, allow_alias) + if type(validator) == 'string' then + local expected = allow_alias and type_aliases[validator] or validator - local val = spec[1] -- Argument value - local types = spec[2] -- Type name, or callable - local optional = (true == spec[3]) + if not expected then + return string.format('invalid type name: %s', validator) + end - if type(types) == 'string' then - types = { types } - end - - if vim.is_callable(types) then + if not is_type(val, expected) then + return string.format('%s: expected %s, got %s', param_name, expected, type(val)) + end + elseif vim.is_callable(validator) then -- Check user-provided validation function - local valid, optional_message = types(val) + local valid, opt_msg = validator(val) if not valid then - local error_message = - string.format('%s: expected %s, got %s', param_name, (spec[3] or '?'), tostring(val)) - if optional_message ~= nil then - error_message = string.format('%s. Info: %s', error_message, optional_message) + local err_msg = + string.format('%s: expected %s, got %s', param_name, message or '?', tostring(val)) + + if opt_msg then + err_msg = string.format('%s. Info: %s', err_msg, opt_msg) end - return error_message + return err_msg end - elseif type(types) == 'table' then - local success = false - for i, t in ipairs(types) do - local t_name = type_names[t] - if not t_name then + elseif type(validator) == 'table' then + for _, t in ipairs(validator) do + local expected = allow_alias and type_aliases[t] or t + if not expected then return string.format('invalid type name: %s', t) end - types[i] = t_name - if (optional and val == nil) or _is_type(val, t_name) then - success = true - break + if is_type(val, expected) then + return -- success end end - if not success then - return string.format( - '%s: expected %s, got %s', - param_name, - table.concat(types, '|'), - type(val) - ) + + -- Normalize validator types for error message + if allow_alias then + for i, t in ipairs(validator) do + validator[i] = type_aliases[t] or t + end end + + return string.format( + '%s: expected %s, got %s', + param_name, + table.concat(validator, '|'), + type(val) + ) else - return string.format('invalid type name: %s', tostring(types)) + return string.format('invalid validator: %s', tostring(validator)) end end - --- @param opt table - --- @return boolean, string? - local function is_valid(opt) - if type(opt) ~= 'table' then - return false, string.format('opt: expected table, got %s', type(opt)) - end - + --- @param opt table + --- @return string? + local function validate_spec(opt) local report --- @type table? for param_name, spec in pairs(opt) do - local msg = is_param_valid(param_name, spec) - if msg then + local err_msg --- @type string? + if type(spec) ~= 'table' then + err_msg = string.format('opt[%s]: expected table, got %s', param_name, type(spec)) + else + local value, validator = spec[1], spec[2] + local msg = type(spec[3]) == 'string' and spec[3] or nil --[[@as string?]] + local optional = spec[3] == true + if not (optional and value == nil) then + err_msg = is_valid(param_name, value, validator, msg, true) + end + end + + if err_msg then report = report or {} - report[param_name] = msg + report[param_name] = err_msg end end if report then for _, msg in vim.spairs(report) do -- luacheck: ignore - return false, msg + return msg end end - - return true end --- Validate function arguments. --- --- This function has two valid forms: --- - --- 1. vim.validate(name: str, value: any, type: string, optional?: bool) - --- 2. vim.validate(spec: table) + --- 1. `vim.validate(name, value, validator[, optional][, message])` --- - --- Form 1 validates that argument {name} with value {value} has the type - --- {type}. {type} must be a value returned by |lua-type()|. If {optional} is - --- true, then {value} may be null. This form is significantly faster and - --- should be preferred for simple cases. + --- Validates that argument {name} with value {value} satisfies + --- {validator}. If {optional} is given and is `true`, then {value} may be + --- `nil`. If {message} is given, then it is used as the expected type in the + --- error message. --- - --- Example: + --- Example: --- - --- ```lua - --- function vim.startswith(s, prefix) - --- vim.validate('s', s, 'string') - --- vim.validate('prefix', prefix, 'string') - --- ... - --- end - --- ``` + --- ```lua + --- function vim.startswith(s, prefix) + --- vim.validate('s', s, 'string') + --- vim.validate('prefix', prefix, 'string') + --- ... + --- end + --- ``` --- - --- Form 2 validates a parameter specification (types and values). Specs are - --- evaluated in alphanumeric order, until the first failure. + --- 2. `vim.validate(spec)` (deprecated) + --- where `spec` is of type + --- `table)` --- - --- Usage example: + --- Validates a argument specification. + --- Specs are evaluated in alphanumeric order, until the first failure. --- - --- ```lua - --- function user.new(name, age, hobbies) - --- vim.validate{ - --- name={name, 'string'}, - --- age={age, 'number'}, - --- hobbies={hobbies, 'table'}, - --- } - --- ... - --- end - --- ``` + --- Example: + --- + --- ```lua + --- function user.new(name, age, hobbies) + --- vim.validate{ + --- name={name, 'string'}, + --- age={age, 'number'}, + --- hobbies={hobbies, 'table'}, + --- } + --- ... + --- end + --- ``` --- --- Examples with explicit argument values (can be run directly): --- --- ```lua - --- vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}} + --- vim.validate('arg1', {'foo'}, 'table') + --- --> NOP (success) + --- vim.validate('arg2', 'foo', 'string') --- --> NOP (success) --- - --- vim.validate{arg1={1, 'table'}} + --- vim.validate('arg1', 1, 'table') --- --> error('arg1: expected table, got number') --- - --- vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}} + --- vim.validate('arg1', 3, function(a) return (a % 2) == 0 end, 'even number') --- --> error('arg1: expected even number, got 3') --- ``` --- --- If multiple types are valid they can be given as a list. --- --- ```lua - --- vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}} + --- vim.validate('arg1', {'foo'}, {'table', 'string'}) + --- vim.validate('arg2', 'foo', {'table', 'string'}) --- -- NOP (success) --- - --- vim.validate{arg1={1, {'string', 'table'}}} + --- vim.validate('arg1', 1, {'string', 'table'}) --- -- error('arg1: expected string|table, got number') --- ``` --- - ---@param opt table (table) Names of parameters to validate. Each key is a parameter - --- name; each value is a tuple in one of these forms: - --- 1. (arg_value, type_name, optional) - --- - arg_value: argument value - --- - type_name: string|table type name, one of: ("table", "t", "string", - --- "s", "number", "n", "boolean", "b", "function", "f", "nil", - --- "thread", "userdata") or list of them. - --- - optional: (optional) boolean, if true, `nil` is valid - --- 2. (arg_value, fn, msg) - --- - arg_value: argument value - --- - fn: any function accepting one argument, returns true if and - --- only if the argument is valid. Can optionally return an additional - --- informative error message as the second returned value. - --- - msg: (optional) error string if validation fails - --- @overload fun(name: string, val: any, expected: vim.validate.LuaType, optional?: boolean) - function vim.validate(opt, ...) - local ok = false - local err_msg ---@type string? - local narg = select('#', ...) - if narg == 0 then - ok, err_msg = is_valid(opt) - elseif narg >= 2 then - -- Overloaded signature for fast/simple cases - local name = opt --[[@as string]] - local v, expected, optional = ... ---@type string, string, boolean? - local actual = type(v) - - ok = (actual == expected) or (v == nil and optional == true) + --- @note `validator` set to a value returned by |lua-type()| provides the + --- best performance. + --- + --- @param name string Argument name + --- @param value string Argument value + --- @param validator vim.validate.Validator + --- - (`string|string[]`): Any value that can be returned from |lua-type()| in addition to + --- `'callable'`: `'boolean'`, `'callable'`, `'function'`, `'nil'`, `'number'`, `'string'`, `'table'`, + --- `'thread'`, `'userdata'`. + --- - (`fun(val:any): boolean, string?`) A function that returns a boolean and an optional + --- string message. + --- @param optional? boolean Argument is optional (may be omitted) + --- @param message? string message when validation fails + --- @overload fun(name: string, val: any, validator: vim.validate.Validator, message: string) + --- @overload fun(spec: table) + function vim.validate(name, value, validator, optional, message) + local err_msg --- @type string? + if validator then -- Form 1 + -- Check validator as a string first to optimize the common case. + local ok = (type(value) == validator) or (value == nil and optional == true) if not ok then - if not jit and (actual ~= 'string' or actual ~= 'number') then - -- PUC-Lua can only handle string and number for %s in string.format() - v = vim.inspect(v) - end - err_msg = ('%s: expected %s, got %s%s'):format( - name, - expected, - actual, - v and (' (%s)'):format(v) or '' - ) + local msg = type(optional) == 'string' and optional or message --[[@as string?]] + -- Check more complicated validators + err_msg = is_valid(name, value, validator, msg, false) end + elseif type(name) == 'table' then -- Form 2 + vim.deprecate('vim.validate', 'vim.validate(name, value, validator, optional_or_msg)', '1.0') + err_msg = validate_spec(name) else error('invalid arguments') end - if not ok then + if err_msg then error(err_msg, 2) end end end + --- Returns true if object `f` can be called as a function. --- ---@param f any Any object diff --git a/runtime/lua/vim/termcap.lua b/runtime/lua/vim/termcap.lua index 1da2e71839..2c36561587 100644 --- a/runtime/lua/vim/termcap.lua +++ b/runtime/lua/vim/termcap.lua @@ -17,10 +17,8 @@ local M = {} --- otherwise. {seq} is the control sequence for the capability if found, or nil for --- boolean capabilities. function M.query(caps, cb) - vim.validate({ - caps = { caps, { 'string', 'table' } }, - cb = { cb, 'f' }, - }) + vim.validate('caps', caps, { 'string', 'table' }) + vim.validate('cb', cb, 'function') if type(caps) ~= 'table' then caps = { caps } diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index c24a732fe3..9f6dc53932 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -241,11 +241,9 @@ end --- ---@return boolean True if the {node} contains the {range} function M.node_contains(node, range) - vim.validate({ - -- allow a table so nodes can be mocked - node = { node, { 'userdata', 'table' } }, - range = { range, M._range.validate, 'integer list with 4 or 6 elements' }, - }) + -- allow a table so nodes can be mocked + vim.validate('node', node, { 'userdata', 'table' }) + vim.validate('range', range, M._range.validate, 'integer list with 4 or 6 elements') return M._range.contains({ node:range() }, range) end diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index ce12ca433a..446051dfd7 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -154,10 +154,8 @@ end --- @param lang string Name of parser --- @param filetype string|string[] Filetype(s) to associate with lang function M.register(lang, filetype) - vim.validate({ - lang = { lang, 'string' }, - filetype = { filetype, { 'string', 'table' } }, - }) + vim.validate('lang', lang, 'string') + vim.validate('filetype', filetype, { 'string', 'table' }) for _, f in ipairs(ensure_list(filetype)) do if f ~= '' then diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index f66b34ac44..b2e972d07a 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -37,8 +37,8 @@ local M = {} --- `idx` is the 1-based index of `item` within `items`. --- `nil` if the user aborted the dialog. function M.select(items, opts, on_choice) - vim.validate('items', items, 'table', false) - vim.validate('on_choice', on_choice, 'function', false) + vim.validate('items', items, 'table') + vim.validate('on_choice', on_choice, 'function') opts = opts or {} local choices = { opts.prompt or 'Select one of:' } local format_item = opts.format_item or tostring @@ -85,7 +85,7 @@ end --- `nil` if the user aborted the dialog. function M.input(opts, on_confirm) vim.validate('opts', opts, 'table', true) - vim.validate('on_confirm', on_confirm, 'function', false) + vim.validate('on_confirm', on_confirm, 'function') opts = (opts and not vim.tbl_isempty(opts)) and opts or vim.empty_dict() diff --git a/scripts/bump_deps.lua b/scripts/bump_deps.lua index e332ef475f..ad71da5150 100755 --- a/scripts/bump_deps.lua +++ b/scripts/bump_deps.lua @@ -325,10 +325,8 @@ function M.commit(dependency_name, commit) end function M.version(dependency_name, version) - vim.validate { - dependency_name = { dependency_name, 's' }, - version = { version, 's' }, - } + vim.validate('dependency_name', dependency_name, 'string') + vim.validate('version', version, 'string') local dependency = assert(get_dependency(dependency_name)) verify_cmakelists_committed() local commit_sha = get_gh_commit_sha(dependency.repo, version) diff --git a/scripts/gen_help_html.lua b/scripts/gen_help_html.lua index b5dbce38f6..21d865309d 100644 --- a/scripts/gen_help_html.lua +++ b/scripts/gen_help_html.lua @@ -1289,25 +1289,15 @@ end --- --- @return nvim.gen_help_html.gen_result result function M.gen(help_dir, to_dir, include, commit, parser_path) - vim.validate { - help_dir = { - help_dir, - function(d) - return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 - end, - 'valid directory', - }, - to_dir = { to_dir, 's' }, - include = { include, 't', true }, - commit = { commit, 's', true }, - parser_path = { - parser_path, - function(f) - return f == nil or vim.fn.filereadable(vim.fs.normalize(f)) == 1 - end, - 'valid vimdoc.{so,dll} filepath', - }, - } + vim.validate('help_dir', help_dir, function(d) + return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 + end, 'valid directory') + vim.validate('to_dir', to_dir, 'string') + vim.validate('include', include, 'table', true) + vim.validate('commit', commit, 'sring', true) + vim.validate('parser_path', parser_path, function(f) + return vim.fn.filereadable(vim.fs.normalize(f)) == 1 + end, true, 'valid vimdoc.{so,dll} filepath') local err_count = 0 local redirects_count = 0 @@ -1410,23 +1400,13 @@ end --- --- @return nvim.gen_help_html.validate_result result function M.validate(help_dir, include, parser_path) - vim.validate { - help_dir = { - help_dir, - function(d) - return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 - end, - 'valid directory', - }, - include = { include, 't', true }, - parser_path = { - parser_path, - function(f) - return f == nil or vim.fn.filereadable(vim.fs.normalize(f)) == 1 - end, - 'valid vimdoc.{so,dll} filepath', - }, - } + vim.validate('help_dir', help_dir, function(d) + return vim.fn.isdirectory(vim.fs.normalize(d)) == 1 + end, 'valid directory') + vim.validate('include', include, 'table', true) + vim.validate('parser_path', parser_path, function(f) + return vim.fn.filereadable(vim.fs.normalize(f)) == 1 + end, true, 'valid vimdoc.{so,dll} filepath') local err_count = 0 ---@type integer local files_to_errors = {} ---@type table ensure_runtimepath() diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index cba6c89c16..b32712860a 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -1358,7 +1358,79 @@ describe('lua stdlib', function() eq('{"a": {}, "b": []}', exec_lua([[ return vim.fn.json_encode({a=vim.empty_dict(), b={}}) ]])) end) - it('vim.validate', function() + it('vim.validate (fast form)', function() + exec_lua("vim.validate('arg1', {}, 'table')") + exec_lua("vim.validate('arg1', nil, 'table', true)") + exec_lua("vim.validate('arg1', { foo='foo' }, 'table')") + exec_lua("vim.validate('arg1', { 'foo' }, 'table')") + exec_lua("vim.validate('arg1', 'foo', 'string')") + exec_lua("vim.validate('arg1', nil, 'string', true)") + exec_lua("vim.validate('arg1', 1, 'number')") + exec_lua("vim.validate('arg1', 0, 'number')") + exec_lua("vim.validate('arg1', 0.1, 'number')") + exec_lua("vim.validate('arg1', nil, 'number', true)") + exec_lua("vim.validate('arg1', true, 'boolean')") + exec_lua("vim.validate('arg1', false, 'boolean')") + exec_lua("vim.validate('arg1', nil, 'boolean', true)") + exec_lua("vim.validate('arg1', function()end, 'function')") + exec_lua("vim.validate('arg1', nil, 'function', true)") + exec_lua("vim.validate('arg1', nil, 'nil')") + exec_lua("vim.validate('arg1', nil, 'nil', true)") + exec_lua("vim.validate('arg1', coroutine.create(function()end), 'thread')") + exec_lua("vim.validate('arg1', nil, 'thread', true)") + exec_lua("vim.validate('arg1', 2, function(a) return (a % 2) == 0 end, 'even number')") + exec_lua("vim.validate('arg1', 5, {'number', 'string'})") + exec_lua("vim.validate('arg2', 'foo', {'number', 'string'})") + + matches('arg1: expected number, got nil', pcall_err(vim.validate, 'arg1', nil, 'number')) + matches('arg1: expected string, got nil', pcall_err(vim.validate, 'arg1', nil, 'string')) + matches('arg1: expected table, got nil', pcall_err(vim.validate, 'arg1', nil, 'table')) + matches('arg1: expected function, got nil', pcall_err(vim.validate, 'arg1', nil, 'function')) + matches('arg1: expected string, got number', pcall_err(vim.validate, 'arg1', 5, 'string')) + matches('arg1: expected table, got number', pcall_err(vim.validate, 'arg1', 5, 'table')) + matches('arg1: expected function, got number', pcall_err(vim.validate, 'arg1', 5, 'function')) + matches('arg1: expected number, got string', pcall_err(vim.validate, 'arg1', '5', 'number')) + matches('arg1: expected x, got number', pcall_err(exec_lua, "vim.validate('arg1', 1, 'x')")) + matches('invalid validator: 1', pcall_err(exec_lua, "vim.validate('arg1', 1, 1)")) + matches('invalid arguments', pcall_err(exec_lua, "vim.validate('arg1', { 1 })")) + + -- Validated parameters are required by default. + matches( + 'arg1: expected string, got nil', + pcall_err(exec_lua, "vim.validate('arg1', nil, 'string')") + ) + -- Explicitly required. + matches( + 'arg1: expected string, got nil', + pcall_err(exec_lua, "vim.validate('arg1', nil, 'string', false)") + ) + + matches( + 'arg1: expected table, got number', + pcall_err(exec_lua, "vim.validate('arg1', 1, 'table')") + ) + + matches( + 'arg1: expected even number, got 3', + pcall_err(exec_lua, "vim.validate('arg1', 3, function(a) return a == 1 end, 'even number')") + ) + matches( + 'arg1: expected %?, got 3', + pcall_err(exec_lua, "vim.validate('arg1', 3, function(a) return a == 1 end)") + ) + matches( + 'arg1: expected number|string, got nil', + pcall_err(exec_lua, "vim.validate('arg1', nil, {'number', 'string'})") + ) + + -- Pass an additional message back. + matches( + 'arg1: expected %?, got 3. Info: TEST_MSG', + pcall_err(exec_lua, "vim.validate('arg1', 3, function(a) return a == 1, 'TEST_MSG' end)") + ) + end) + + it('vim.validate (spec form)', function() exec_lua("vim.validate{arg1={{}, 'table' }}") exec_lua("vim.validate{arg1={{}, 't' }}") exec_lua("vim.validate{arg1={nil, 't', true }}") @@ -1387,29 +1459,11 @@ describe('lua stdlib', function() exec_lua("vim.validate{arg1={{}, 't' }, arg2={ 'foo', 's' }}") exec_lua("vim.validate{arg1={2, function(a) return (a % 2) == 0 end, 'even number' }}") exec_lua("vim.validate{arg1={5, {'n', 's'} }, arg2={ 'foo', {'n', 's'} }}") - vim.validate('arg1', 5, 'number') - vim.validate('arg1', '5', 'string') - vim.validate('arg1', { 5 }, 'table') - vim.validate('arg1', function() - return 5 - end, 'function') - vim.validate('arg1', nil, 'number', true) - vim.validate('arg1', nil, 'string', true) - vim.validate('arg1', nil, 'table', true) - vim.validate('arg1', nil, 'function', true) - matches('arg1: expected number, got nil', pcall_err(vim.validate, 'arg1', nil, 'number')) - matches('arg1: expected string, got nil', pcall_err(vim.validate, 'arg1', nil, 'string')) - matches('arg1: expected table, got nil', pcall_err(vim.validate, 'arg1', nil, 'table')) - matches('arg1: expected function, got nil', pcall_err(vim.validate, 'arg1', nil, 'function')) - matches('arg1: expected string, got number', pcall_err(vim.validate, 'arg1', 5, 'string')) - matches('arg1: expected table, got number', pcall_err(vim.validate, 'arg1', 5, 'table')) - matches('arg1: expected function, got number', pcall_err(vim.validate, 'arg1', 5, 'function')) - matches('arg1: expected number, got string', pcall_err(vim.validate, 'arg1', '5', 'number')) matches('expected table, got number', pcall_err(exec_lua, "vim.validate{ 1, 'x' }")) - matches('invalid type name: x', pcall_err(exec_lua, "vim.validate{ arg1={ 1, 'x' }}")) - matches('invalid type name: 1', pcall_err(exec_lua, 'vim.validate{ arg1={ 1, 1 }}')) - matches('invalid type name: nil', pcall_err(exec_lua, 'vim.validate{ arg1={ 1 }}')) + matches('arg1: expected x, got number', pcall_err(exec_lua, "vim.validate{ arg1={ 1, 'x' }}")) + matches('invalid validator: 1', pcall_err(exec_lua, 'vim.validate{ arg1={ 1, 1 }}')) + matches('invalid validator: nil', pcall_err(exec_lua, 'vim.validate{ arg1={ 1 }}')) -- Validated parameters are required by default. matches( @@ -4094,10 +4148,15 @@ describe('vim.keymap', function() ) matches( - 'opts: expected table, got function', + 'rhs: expected string|function, got number', pcall_err(exec_lua, [[vim.keymap.set({}, 'x', 42, function() end)]]) ) + matches( + 'opts: expected table, got function', + pcall_err(exec_lua, [[vim.keymap.set({}, 'x', 'x', function() end)]]) + ) + matches( 'rhs: expected string|function, got number', pcall_err(exec_lua, [[vim.keymap.set('z', 'x', 42)]])