Skip to content
Open
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
93 changes: 93 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,96 @@ 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
-- First, 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
func = bindSource
break
elseif bindSource.parent and bindSource.parent.type == 'function' then
func = bindSource.parent
break
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 +115,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