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
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased
<!-- Add all new changes here. They will be moved under a version at release -->
* `FIX` Generic class inheritance with type arguments now works correctly (e.g., `class Bar: Foo<integer>`) [#1929](https://github.com/LuaLS/lua-language-server/issues/1929)
* `FIX` Method return types on generic classes now resolve correctly (e.g., `Box<string>:getValue()` returns `string`) [#1863](https://github.com/LuaLS/lua-language-server/issues/1863)
* `FIX` Self-referential generic classes no longer cause infinite expansion in hover display [#1853](https://github.com/LuaLS/lua-language-server/issues/1853)
* `FIX` Generic type parameters now work in `@overload` annotations [#723](https://github.com/LuaLS/lua-language-server/issues/723)
* `NEW` Support `fun<T>` syntax for inline generic function types in `@field` and `@type` annotations [#1170](https://github.com/LuaLS/lua-language-server/issues/1170)
* `FIX` Methods with `@generic T` and `@param self T` now correctly resolve return type to the receiver's concrete type (e.g., `List<number>:identity()` returns `List<number>`) [#1000](https://github.com/LuaLS/lua-language-server/issues/1000)

## 3.16.4
`2025-12-25`
Expand Down
74 changes: 64 additions & 10 deletions script/core/diagnostics/param-type-mismatch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ local vm = require 'vm'
local await = require 'await'

---@param defNode vm.node
local function expandGenerics(defNode)
---@param classGenericMap table<string, vm.node>?
local function expandGenerics(defNode, classGenericMap)
---@type parser.object[]
local generics = {}
for dn in defNode:eachObject() do
Expand All @@ -20,27 +21,78 @@ local function expandGenerics(defNode)
end

for _, generic in ipairs(generics) do
local limits = generic.generic and generic.generic.extends
if limits then
defNode:merge(vm.compileNode(limits))
-- First check if this generic is a class generic that can be resolved
local genericName = generic[1]
if classGenericMap and genericName and classGenericMap[genericName] then
defNode:merge(classGenericMap[genericName])
else
local unknownType = vm.declareGlobal('type', 'unknown')
defNode:merge(unknownType)
-- Fall back to constraint or unknown
local limits = generic.generic and generic.generic.extends
if limits then
defNode:merge(vm.compileNode(limits))
else
local unknownType = vm.declareGlobal('type', 'unknown')
defNode:merge(unknownType)
end
end
end
end

---@param uri uri
---@param source parser.object
---@return table<string, vm.node>?
local function getReceiverGenericMap(uri, source)
local callNode = source.node
if not callNode then
return nil
end
-- Only resolve generics for method calls (obj:method()), not static calls (Class.method())
if callNode.type ~= 'getmethod' then
return nil
end
local receiver = callNode.node
if not receiver then
return nil
end
local receiverNode = vm.compileNode(receiver)
for rn in receiverNode:eachObject() do
if rn.type == 'doc.type.sign' and rn.signs and rn.node and rn.node[1] then
local classGlobal = vm.getGlobal('type', rn.node[1])
if classGlobal then
return vm.getClassGenericMap(uri, classGlobal, rn.signs)
end
end
end
return nil
end

---@param funcNode vm.node
---@param i integer
---@param classGenericMap table<string, vm.node>?
---@return vm.node?
local function getDefNode(funcNode, i)
local function getDefNode(funcNode, i, classGenericMap)
local defNode = vm.createNode()
for src in funcNode:eachObject() do
if src.type == 'function'
or src.type == 'doc.type.function' then
local param = src.args and src.args[i]
if param then
defNode:merge(vm.compileNode(param))
local paramNode = vm.compileNode(param)
-- Check for global type references that match class generic params
if classGenericMap then
local newNode = vm.createNode()
for pn in paramNode:eachObject() do
if pn.type == 'global' and pn.cate == 'type' and classGenericMap[pn.name] then
-- Replace the global type reference with the resolved type
newNode:merge(classGenericMap[pn.name])
else
newNode:merge(pn)
end
end
defNode:merge(newNode)
else
defNode:merge(paramNode)
end
if param[1] == '...' then
defNode:addOptional()
end
Expand All @@ -51,7 +103,7 @@ local function getDefNode(funcNode, i)
return nil
end

expandGenerics(defNode)
expandGenerics(defNode, classGenericMap)

return defNode
end
Expand Down Expand Up @@ -87,12 +139,14 @@ return function (uri, callback)
end
await.delay()
local funcNode = vm.compileNode(source.node)
-- Get the class generic map for method calls on generic class instances
local classGenericMap = getReceiverGenericMap(uri, source)
for i, arg in ipairs(source.args) do
local refNode = vm.compileNode(arg)
if not refNode then
goto CONTINUE
end
local defNode = getDefNode(funcNode, i)
local defNode = getDefNode(funcNode, i, classGenericMap)
if not defNode then
goto CONTINUE
end
Expand Down
97 changes: 97 additions & 0 deletions script/core/diagnostics/undefined-doc-name.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,100 @@ local guide = require 'parser.guide'
local lang = require 'language'
local vm = require 'vm'

--- Check if name is a generic parameter from a class context
---@param source parser.object The doc.type.name source
---@param name string The type name to check
---@param uri uri The file URI
---@return boolean
local function isClassGenericParam(source, name, uri)
-- Find containing doc node
local doc = guide.getParentTypes(source, {
['doc.return'] = true,
['doc.param'] = true,
['doc.type'] = true,
['doc.field'] = true,
['doc.overload'] = true,
['doc.vararg'] = true,
})
if not doc then
return false
end

-- Walk up to find a doc node with bindGroup (intermediate doc.type nodes don't have it)
while doc and not doc.bindGroup do
doc = doc.parent
end
if not doc then
return false
end

-- Check bindGroup for class/alias with matching generic sign
local bindGroup = doc.bindGroup
if bindGroup then
for _, other in ipairs(bindGroup) do
if (other.type == 'doc.class' or other.type == 'doc.alias') and other.signs then
for _, sign in ipairs(other.signs) do
if sign[1] == name then
return true
end
end
end
end
end

-- Check direct class reference (for doc.field, doc.overload, doc.operator)
if doc.class and doc.class.signs then
for _, sign in ipairs(doc.class.signs) do
if sign[1] == name then
return true
end
end
end

-- Check if bound to a method on a generic class
-- Find the function from any doc in the bindGroup
local func = nil
if bindGroup then
for _, other in ipairs(bindGroup) do
local bindSource = other.bindSource
if bindSource then
if bindSource.type == 'function' then
-- doc.return binds directly to function
func = bindSource
break
else
-- doc.param binds to local param, find containing function
func = guide.getParentFunction(bindSource)
if func then
break
end
end
end
end
end

-- If we found a function, check if it's a method on a generic class
if func and func.parent then
local parent = func.parent
if parent.type == 'setmethod' or parent.type == 'setfield' or parent.type == 'setindex' then
local classGlobal = vm.getDefinedClass(uri, parent.node)
if classGlobal then
for _, set in ipairs(classGlobal:getSets(uri)) do
if set.type == 'doc.class' and set.signs then
for _, sign in ipairs(set.signs) do
if sign[1] == name then
return true
end
end
end
end
end
end
end

return false
end

return function (uri, callback)
local state = files.getState(uri)
if not state then
Expand All @@ -25,6 +119,9 @@ return function (uri, callback)
if name == '...' or name == '_' or name == 'self' then
return
end
if isClassGenericParam(source, name, uri) then
return
end
if #vm.getDocSets(uri, name) > 0 then
return
end
Expand Down
2 changes: 1 addition & 1 deletion script/parser/guide.lua
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ local childMap = {
['doc.generic.object'] = {'generic', 'extends', 'comment'},
['doc.vararg'] = {'vararg', 'comment'},
['doc.type.array'] = {'node'},
['doc.type.function'] = {'#args', '#returns', 'comment'},
['doc.type.function'] = {'#args', '#returns', '#signs', 'comment'},
['doc.type.table'] = {'#fields', 'comment'},
['doc.type.literal'] = {'node'},
['doc.type.arg'] = {'name', 'extends'},
Expand Down
57 changes: 56 additions & 1 deletion script/parser/luadoc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@ local function parseTypeUnitFunction(parent)
args = {},
returns = {},
}
-- Parse optional generic params: fun<T, V>(...)
typeUnit.signs = parseSigns(typeUnit)
if not nextSymbolOrError('(') then
return nil
end
Expand Down Expand Up @@ -617,6 +619,51 @@ local function parseTypeUnitFunction(parent)
end
end
typeUnit.finish = getFinish()
-- Bind local generics from fun<T, V> to type names within this function
if typeUnit.signs then
local generics = {}
for _, sign in ipairs(typeUnit.signs) do
generics[sign[1]] = sign
end
local function bindTypeNames(obj)
if not obj then return end
if obj.type == 'doc.type.name' and generics[obj[1]] then
obj.type = 'doc.generic.name'
obj.generic = generics[obj[1]]
elseif obj.type == 'doc.type' and obj.types then
for _, t in ipairs(obj.types) do
bindTypeNames(t)
end
elseif obj.type == 'doc.type.array' then
bindTypeNames(obj.node)
elseif obj.type == 'doc.type.table' and obj.fields then
for _, field in ipairs(obj.fields) do
bindTypeNames(field.name)
bindTypeNames(field.extends)
end
elseif obj.type == 'doc.type.sign' then
bindTypeNames(obj.node)
if obj.signs then
for _, s in ipairs(obj.signs) do
bindTypeNames(s)
end
end
elseif obj.type == 'doc.type.function' then
for _, arg in ipairs(obj.args) do
bindTypeNames(arg.extends)
end
for _, ret in ipairs(obj.returns) do
bindTypeNames(ret)
end
end
end
for _, arg in ipairs(typeUnit.args) do
bindTypeNames(arg.extends)
end
for _, ret in ipairs(typeUnit.returns) do
bindTypeNames(ret)
end
end
return typeUnit
end

Expand Down Expand Up @@ -1030,6 +1077,12 @@ local docSwitch = util.switch()
}
return result
end
if extend.type == 'doc.extends.name' then
local signResult = parseTypeUnitSign(result, extend)
if signResult then
extend = signResult
end
end
result.extends[#result.extends+1] = extend
result.finish = getFinish()
if not checkToken('symbol', ',', 1) then
Expand Down Expand Up @@ -1850,7 +1903,9 @@ local function bindGeneric(binded)
or doc.type == 'doc.return'
or doc.type == 'doc.type'
or doc.type == 'doc.class'
or doc.type == 'doc.alias' then
or doc.type == 'doc.alias'
or doc.type == 'doc.field'
or doc.type == 'doc.overload' then
guide.eachSourceType(doc, 'doc.type.name', function (src)
local name = src[1]
if generics[name] then
Expand Down
Loading
Loading