在Lua中,有没有办法记录函数类型,包括其参数的描述?

问题描述 投票:0回答:1

我正在 NeoVim 配置中进行一些维护,几乎完全是用 Lua 编写的。
为了方便起见,我创建了一个有用工具的迷你库来简化此配置。
我有几个朋友密切关注我的 NeoVim 配置,甚至使用它,因此我喜欢对我的工具进行详细记录,以减轻他们的使用体验。
主要来自 TypeScript 背景,你可以想象我喜欢深入研究它。 🤭

在这个示例中,我有一个模块可以减少设置映射的麻烦,也使其更具可读性。

--- Set a keymap using Vim's API
---@param desc  string     The descritpion that will be shown for your mapping when calling :map
---@param mode  MapModeStr The mode that your keymap will work in
---@param lhs   string     A NeoVim keymap string
---@param rhs   MapRHS     The action yout keymap will trigger
---@param opts? MapOptions A table of options
local set = function(desc, mode, lhs, rhs, opts)
  opts = opts or {}
  local finalopts = vim.tbl_deep_extend("keep", { desc = desc }, opts)
  vim.keymap.set(mode, lhs, rhs, finalopts)
end

return {
  set = set,

  --- Set a keymap for Insert mode
  ---@param desc  string     The descritpion that will be shown for your mapping when calling :map
  ---@param lhs   string     A NeoVim keymap string
  ---@param rhs   MapRHS     The action yout keymap will trigger
  ---@param opts? MapOptions A table of options
  i = function(desc, lhs, rhs, opts) set(desc, "i", lhs, rhs, opts) end,

  --- Set a keymap for Normal mode
  ---@param desc  string     The descritpion that will be shown for your mapping when calling :map
  ---@param lhs   string     A NeoVim keymap string
  ---@param rhs   MapRHS     The action yout keymap will trigger
  ---@param opts? MapOptions A table of options
  n = function(desc, lhs, rhs, opts) set(desc, "n", lhs, rhs, opts) end,

  ---@param desc  string     The descritpion that will be shown for your mapping when calling :map
  ---@param lhs   string     A NeoVim keymap string
  ---@param rhs   MapRHS     The action yout keymap will trigger
  ---@param opts? MapOptions A table of options
  o = function(desc, lhs, rhs, opts) end,

  --- Set a keymap for Visual and Select mode
  ---@param desc  string     The descritpion that will be shown for your mapping when calling :map
  ---@param lhs   string     A NeoVim keymap string
  ---@param rhs   MapRHS     The action yout keymap will trigger
  ---@param opts? MapOptions A table of options
  x = function(desc, lhs, rhs, opts) set(desc, "x", lhs, rhs, opts) end,

  --- Set a keymap for Visual mode
  ---@param desc  string     The descritpion that will be shown for your mapping when calling :map
  ---@param lhs   string     A NeoVim keymap string
  ---@param rhs   MapRHS     The action yout keymap will trigger
  ---@param opts? MapOptions A table of options
  v = function(desc, lhs, rhs, opts) set(desc, "v", lhs, rhs, opts) end,
}

为了减少复制粘贴,我想使用我为此模块定义的其他类型在元文件中向用户公开的映射器函数进行“typedef”。我知道这是我能做的:

---@meta

...

---@alias MapperFunction fun(desc: string, lhs: string, rhs: MapRHS, opts: MapOptions?)

然后我可以简单地对它们进行

---@type MapperFunction
,但问题是我丢失了每个参数的作用的描述,我想避免这种情况。

我能想到的最好的办法就是一些辛辣的小咖喱(函数式编程太棒了!)。它有效,但我想知道是否有一种方法可以仅通过打字来完成。

一个恼人的副作用是我丢失了一些完成注释。

那么,有没有一种方法可以简单地在我的函数之上添加一个类型并保留所有 IntelliSense,或者我注定要使用这两种技术中的一种?

谢谢!

lua neovim typing luadoc
1个回答
0
投票

我不确定以下建议在您的特定环境中是否对您有用,但对于一般 Lua 程序员来说可能会很有趣。

它是一个“装饰器”,允许创建带有类型化参数和返回值的 Lua 函数。函数规范是一个字符串,例如:

[[
Squares its argument.
@param number x Number to square
@return number The squared number
]]

允许为空类型(以

?
开头);多种类型也是如此,例如
number|string

local unpack = unpack or table.unpack

-- Makes an array of possible types out of string '[?]type1|[?]type2|...|[?]typen'.
local function type_spec (spec)
    local variants = {}
    for type in spec:gmatch '[^|]+' do
        local nullable = false
        if type:sub (1, 1) == '?' then
            type = type:sub (2)
            nullable = true
        end
        variants[#variants + 1] = { type = type, nullable = nullable }
    end
    return variants
end

-- Checks if the type actual is among the expected types, or it is 'nil' and expected contains nullable type.
local function matches (actual, expected)
    for _, variant in ipairs (expected) do
        if not (actual == variant.type or actual == 'nil' and variant.nullable) then
            return false
        end
    end
    return true
end

-- Returns an or-separated list of types, with nullable types prefixed by '?'.
local function serialise_types (types)
    local serialised = {}
    for _, type in ipairs (types) do
        serialised[#serialised + 1] = (type.nullable and '?' or '') .. type.type
    end
    return table.concat (serialised, ' or ')
end

-- Checks an array of values against an array of expected type specifications.
-- Aborts with error, if any of values is of wrong type.
local function check_all (values, specs, returned)
    for no, spec in ipairs (specs) do
        local type, value = type (values[no]), values [no]
        if not matches (type, spec.types) then
            error (
                'Type mismatch. ' .. (returned and 'Returned value' or 'Parameter') .. ' '
             .. tostring (no) .. (spec.name and ' (' .. spec.name .. ')' or '') .. ': '
             .. serialise_types (spec.types) .. ' is expected, '
             .. 'but ' .. type .. ' ' .. tostring (value) .. ' is '
             .. (returned and 'returned' or 'supplied')
            )
        end
    end
end

-- Wraps func with a callable table with type checks for parameters and returns as defined by spec.
local function typed (spec, func)
    local params, returns, lines = {}, {}, {}
    for line in spec:gmatch '[^\n]+' do
        local type, name, desc = line:match '%s*@param%s+(%S+)%s+(%S+)(.*)'
        if type then
            params[#params + 1] = { types = type_spec (type) , desc, name }
            lines[#lines + 1] = '@param ' .. type .. ' ' .. name .. desc
        else
            local type, desc = line:match '%s*@return%s+(%S+)(.*)'
            if type then
                returns[#returns + 1] = { types = type_spec (type), desc }
                lines[#lines + 1] = '@return ' .. type .. desc              
            else
                lines[#lines + 1] = line
            end
        end
    end

    return setmetatable ({
        params = params, -- use f.params to get the parsed parameters specification.
        returns = returns, -- use f.returns to get the parsed returns specification.
        desc = table.concat (lines, '\n'), -- use f.desc to get normalised function specification as text.
        validate_params = function (...) -- use f.validate_params (...) to get true/false, [error text].
            return pcall (check_all, {...}, params)
        end
    }, {
        __call = function (self, ...) -- simply call f(...).
            check_all ({...}, params) -- check parameters against the specification.
            local returned = { func (...) } -- actually call the function.
            check_all (returned, returns, true) -- check the returned values against the specification.
            return unpack (returned) -- return the validated return values.
        end
    })
end

-- That's how funcitons are decorated to make them typed.
local squared = typed ([[
Squares its argument
@param number x Number to square
@return number The squared number
]],
    function (x) return x ^ 2 end
)

print ('Description: ', squared.desc)

for _, arg in ipairs { 2, 'two' } do
    print ('x = ', arg, 'squared (x) = ', squared (arg))
end
© www.soinside.com 2019 - 2024. All rights reserved.