Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## Unreleased
<!-- Add all new changes here. They will be moved under a version at release -->
* `NEW` Narrow overload candidates by the types of the preceding arguments, for completion and `param-type-mismatch`
```lua
---@class A.Component
---@class B.Component

---@overload fun(c: A.Component, field: "hp"|"max_hp")
---@overload fun(c: B.Component, field: "mana"|"cooldown")
local function setValue(...) end

---@type A.Component
local a

setValue(a, "mana") --> now warns (`param-type-mismatch`); completion inside the quotes only suggests "hp" and "max_hp"
```
* `NEW` Support type inference for `@field` and `@type` function declarations in method overrides [#3367](https://github.com/LuaLS/lua-language-server/issues/3367)
* `FIX` Deduplicate documentation bindings for parameters
* `FIX` Correct `math.type` meta return annotation to use `nil` instead of the string literal `'nil'`
Expand Down
48 changes: 37 additions & 11 deletions script/core/diagnostics/param-type-mismatch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,41 @@ local function getReceiverGenericMap(uri, source)
return nil
end

---@param uri uri
---@param funcNode vm.node
---@param callArgs parser.object[]
---@param i integer
---@param classGenericMap table<string, vm.node>?
---@return vm.node?
local function getDefNode(funcNode, i, classGenericMap)
local defNode = vm.createNode()
---@return parser.object[]
local function getCheckableFunctions(uri, funcNode, callArgs, i)
local funcs = {}
for src in funcNode:eachObject() do
if src.type == 'function'
or src.type == 'doc.type.function' then
funcs[#funcs+1] = src
end
end
if #funcs > 1 then
local matched = {}
for _, src in ipairs(funcs) do
if vm.isPriorArgsMatched(uri, src, callArgs, i) then
matched[#matched+1] = src
end
end
if #matched > 0 then
funcs = matched
end
end
return funcs
end
Comment on lines +69 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update getCheckableFunctions to accept the call AST node (source) instead of callArgs to support the parameter alignment fix in vm.isPriorArgsMatched.

---@param uri uri
---@param funcNode vm.node
---@param call parser.object
---@param i integer
---@return parser.object[]
local function getCheckableFunctions(uri, funcNode, call, i)
    local funcs = {}
    for src in funcNode:eachObject() do
        if src.type == 'function'
        or src.type == 'doc.type.function' then
            funcs[#funcs+1] = src
        end
    end
    if #funcs > 1 then
        local matched = {}
        for _, src in ipairs(funcs) do
            if vm.isPriorArgsMatched(uri, src, call, i) then
                matched[#matched+1] = src
            end
        end
        if #matched > 0 then
            funcs = matched
        end
    end
    return funcs
end


---@param funcs parser.object[]
---@param i integer
---@param classGenericMap table<string, vm.node>?
---@return vm.node?
local function getDefNode(funcs, i, classGenericMap)
local defNode = vm.createNode()
for _, src in ipairs(funcs) do
if src.args then
local param = src.args and src.args[i]
if param then
local paramNode = vm.compileNode(param)
Expand Down Expand Up @@ -108,14 +134,13 @@ local function getDefNode(funcNode, i, classGenericMap)
return defNode
end

---@param funcNode vm.node
---@param funcs parser.object[]
---@param i integer
---@return vm.node
local function getRawDefNode(funcNode, i)
local function getRawDefNode(funcs, i)
local defNode = vm.createNode()
for f in funcNode:eachObject() do
if f.type == 'function'
or f.type == 'doc.type.function' then
for _, f in ipairs(funcs) do
if f.args then
local param = f.args and f.args[i]
if param then
defNode:merge(vm.compileNode(param))
Expand Down Expand Up @@ -146,7 +171,8 @@ return function (uri, callback)
if not refNode then
goto CONTINUE
end
local defNode = getDefNode(funcNode, i, classGenericMap)
local funcs = getCheckableFunctions(uri, funcNode, source.args, i)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Pass source (the call AST node) instead of source.args to getCheckableFunctions.

            local funcs = getCheckableFunctions(uri, funcNode, source, i)

local defNode = getDefNode(funcs, i, classGenericMap)
if not defNode then
goto CONTINUE
end
Expand All @@ -159,7 +185,7 @@ return function (uri, callback)
end
local errs = {}
if not vm.canCastType(uri, defNode, refNode, errs) then
local rawDefNode = getRawDefNode(funcNode, i)
local rawDefNode = getRawDefNode(funcs, i)
assert(errs)
callback {
start = arg.start,
Expand Down
22 changes: 20 additions & 2 deletions script/vm/compiler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1208,23 +1208,41 @@ local function compileCallArgNode(arg, call, callNode, fixIndex, myIndex)
end
end

local docFuncs = {}
for n in callNode:eachObject() do
if n.type == 'function' then
---@cast n parser.object
dealFunction(n)
elseif n.type == 'doc.type.function' then
---@cast n parser.object
dealDocFunc(n)
docFuncs[#docFuncs+1] = n
elseif n.type == 'global' and n.cate == 'type' then
---@cast n vm.global
local overloads = vm.getOverloadsByTypeName(n.name, guide.getUri(arg))
if overloads then
for _, func in ipairs(overloads) do
dealDocFunc(func)
docFuncs[#docFuncs+1] = func
end
end
end
end

if #docFuncs > 1 and call.args then
local uri = guide.getUri(arg)
local matched = {}
for _, n in ipairs(docFuncs) do
if vm.isPriorArgsMatched(uri, n, call.args, myIndex, fixIndex) then
matched[#matched+1] = n
end
end
if #matched > 0 then
docFuncs = matched
end
end
Comment on lines +1230 to +1241

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Pass call instead of call.args to vm.isPriorArgsMatched to support the parameter alignment fix.

    if #docFuncs > 1 and call.args then
        local uri = guide.getUri(arg)
        local matched = {}
        for _, n in ipairs(docFuncs) do
            if vm.isPriorArgsMatched(uri, n, call, myIndex, fixIndex) then
                matched[#matched+1] = n
            end
        end
        if #matched > 0 then
            docFuncs = matched
        end
    end


for _, n in ipairs(docFuncs) do
dealDocFunc(n)
end
end

---@param arg parser.object
Expand Down
38 changes: 38 additions & 0 deletions script/vm/function.lua
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,44 @@ local function isAllParamMatched(uri, args, params)
return true
end

---@param param parser.object
---@return boolean
local function isVarargParam(param)
return param.type == '...'
or (param.name and param.name[1] == '...')
end

---@param uri uri
---@param func parser.object -- `function` or `doc.type.function`
---@param callArgs parser.object[]
---@param myIndex integer
---@param fixIndex? integer
---@return boolean
function vm.isPriorArgsMatched(uri, func, callArgs, myIndex, fixIndex)
fixIndex = fixIndex or 0
local params = func.args
if not params then
return true
end
for i = 1, myIndex - 1 do
local callArg = callArgs[i + fixIndex]
local param = params[i]
if not callArg or not param then
break
end
if callArg.type ~= '...'
and param.type ~= 'self'
and not isVarargParam(param) then
local defNode = vm.compileNode(param)
local refNode = vm.compileNode(callArg)
if not vm.canCastType(uri, defNode, refNode) then
return false
end
end
end
return true
end
Comment on lines +363 to +392

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a misalignment bug when matching prior arguments for method calls (using :).

In Lua, a method definition like function class:method(arg1, arg2) implicitly has self as its first parameter, so func.args is { self, arg1, arg2 }. However, a method call like obj:method(arg1, arg2) only has explicit arguments in call.args, i.e., { arg1, arg2 }.

When checking prior arguments for arg2 (myIndex = 2), the loop compares callArgs[1] (arg1) against params[1] (self). Since params[1] is self, it is skipped, meaning arg1 is never validated. For calls with 3 or more arguments, this misalignment causes arguments to be compared against the wrong parameters (e.g., callArgs[2] compared against params[2] which is arg1's parameter instead of arg2's), leading to incorrect overload filtering and false-positive diagnostics.

We can fix this by passing the call AST node instead of callArgs and checking if call.node.type == 'getmethod' to offset the parameter index by 1 when self is present.

---@param uri uri
---@param func parser.object -- `function` or `doc.type.function`
---@param call parser.object
---@param myIndex integer
---@param fixIndex? integer
---@return boolean
function vm.isPriorArgsMatched(uri, func, call, myIndex, fixIndex)
    local callArgs = call.args
    if not callArgs then
        return true
    end
    fixIndex = fixIndex or 0
    local params = func.args
    if not params then
        return true
    end
    local paramOffset = 0
    if call.node and call.node.type == 'getmethod' then
        if params[1] and params[1].type == 'self' then
            paramOffset = 1
        end
    end
    for i = 1, myIndex - 1 do
        local callArg = callArgs[i + fixIndex]
        local param   = params[i + paramOffset]
        if not callArg or not param then
            break
        end
        if  callArg.type ~= '...'
        and param.type ~= 'self'
        and not isVarargParam(param) then
            local defNode = vm.compileNode(param)
            local refNode = vm.compileNode(callArg)
            if not vm.canCastType(uri, defNode, refNode) then
                return false
            end
        end
    end
    return true
end


---@param uri uri
---@param args parser.object[]
---@param func parser.object
Expand Down
26 changes: 26 additions & 0 deletions test/completion/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4613,3 +4613,29 @@ print(a:<??>)
kind = define.CompletionItemKind.Method,
},
}

TEST [[
---@class A.Component
---@class B.Component

---@overload fun(c: A.Component, field: "hp"|"max_hp", value: any)
---@overload fun(c: B.Component, field: "mana"|"cooldown", value: any)
local function setValue(...) end

---@type A.Component
local a

setValue(a, '<??>')
]]
{
{
label = "'hp'",
kind = define.CompletionItemKind.EnumMember,
textEdit = EXISTS,
},
{
label = "'max_hp'",
kind = define.CompletionItemKind.EnumMember,
textEdit = EXISTS,
},
}
30 changes: 30 additions & 0 deletions test/diagnostics/param-type-mismatch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,33 @@ f(x)
]]

config.set(nil, 'Lua.type.checkTableShape', false)

TEST [[
---@class A.Component
---@class B.Component

---@overload fun(c: A.Component, field: "hp"|"max_hp")
---@overload fun(c: B.Component, field: "mana"|"cooldown")
local function setValue(...) end

---@type A.Component
local a

setValue(a, 'hp')
setValue(a, <!'mana'!>)
]]

TEST [[
---@class A.Component
---@class B.Component

---@overload fun(c: A.Component, field: "hp")
---@overload fun(c: B.Component, field: "mana")
local function setValue(...) end

---@type A.Component|B.Component
local c

setValue(c, 'hp')
setValue(c, 'mana')
]]
Loading