Skip to content

Commit 2bb62ab

Browse files
committed
fix: resolve generics in overloads, fun<T> syntax, and @param self T patterns
- Add support for @Generic T in @overload annotations (#723) - Parse fun<T>(x: T): T syntax in @field and @type annotations (#1170) - Fix methods with @Generic T and @param self T to resolve return type to receiver's concrete type (e.g., List<number>:identity() returns List<number> instead of unknown) (#1000) - Add comprehensive test cases for all generic patterns
1 parent 1d32d41 commit 2bb62ab

File tree

5 files changed

+287
-9
lines changed

5 files changed

+287
-9
lines changed

changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* `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)
66
* `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)
77
* `FIX` Self-referential generic classes no longer cause infinite expansion in hover display [#1853](https://github.com/LuaLS/lua-language-server/issues/1853)
8+
* `FIX` Generic type parameters now work in `@overload` annotations [#723](https://github.com/LuaLS/lua-language-server/issues/723)
9+
* `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)
10+
* `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)
811

912
## 3.16.4
1013
`2025-12-25`

script/parser/guide.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ local childMap = {
177177
['doc.generic.object'] = {'generic', 'extends', 'comment'},
178178
['doc.vararg'] = {'vararg', 'comment'},
179179
['doc.type.array'] = {'node'},
180-
['doc.type.function'] = {'#args', '#returns', 'comment'},
180+
['doc.type.function'] = {'#args', '#returns', '#signs', 'comment'},
181181
['doc.type.table'] = {'#fields', 'comment'},
182182
['doc.type.literal'] = {'node'},
183183
['doc.type.arg'] = {'name', 'extends'},

script/parser/luadoc.lua

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,8 @@ local function parseTypeUnitFunction(parent)
523523
args = {},
524524
returns = {},
525525
}
526+
-- Parse optional generic params: fun<T, V>(...)
527+
typeUnit.signs = parseSigns(typeUnit)
526528
if not nextSymbolOrError('(') then
527529
return nil
528530
end
@@ -617,6 +619,51 @@ local function parseTypeUnitFunction(parent)
617619
end
618620
end
619621
typeUnit.finish = getFinish()
622+
-- Bind local generics from fun<T, V> to type names within this function
623+
if typeUnit.signs then
624+
local generics = {}
625+
for _, sign in ipairs(typeUnit.signs) do
626+
generics[sign[1]] = sign
627+
end
628+
local function bindTypeNames(obj)
629+
if not obj then return end
630+
if obj.type == 'doc.type.name' and generics[obj[1]] then
631+
obj.type = 'doc.generic.name'
632+
obj.generic = generics[obj[1]]
633+
elseif obj.type == 'doc.type' and obj.types then
634+
for _, t in ipairs(obj.types) do
635+
bindTypeNames(t)
636+
end
637+
elseif obj.type == 'doc.type.array' then
638+
bindTypeNames(obj.node)
639+
elseif obj.type == 'doc.type.table' and obj.fields then
640+
for _, field in ipairs(obj.fields) do
641+
bindTypeNames(field.name)
642+
bindTypeNames(field.extends)
643+
end
644+
elseif obj.type == 'doc.type.sign' then
645+
bindTypeNames(obj.node)
646+
if obj.signs then
647+
for _, s in ipairs(obj.signs) do
648+
bindTypeNames(s)
649+
end
650+
end
651+
elseif obj.type == 'doc.type.function' then
652+
for _, arg in ipairs(obj.args) do
653+
bindTypeNames(arg.extends)
654+
end
655+
for _, ret in ipairs(obj.returns) do
656+
bindTypeNames(ret)
657+
end
658+
end
659+
end
660+
for _, arg in ipairs(typeUnit.args) do
661+
bindTypeNames(arg.extends)
662+
end
663+
for _, ret in ipairs(typeUnit.returns) do
664+
bindTypeNames(ret)
665+
end
666+
end
620667
return typeUnit
621668
end
622669

@@ -1857,7 +1904,8 @@ local function bindGeneric(binded)
18571904
or doc.type == 'doc.type'
18581905
or doc.type == 'doc.class'
18591906
or doc.type == 'doc.alias'
1860-
or doc.type == 'doc.field' then
1907+
or doc.type == 'doc.field'
1908+
or doc.type == 'doc.overload' then
18611909
guide.eachSourceType(doc, 'doc.type.name', function (src)
18621910
local name = src[1]
18631911
if generics[name] then

script/vm/compiler.lua

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,18 +1550,120 @@ local function bindReturnOfFunction(source, mfunc, index, args)
15501550
if not returnObject then
15511551
return
15521552
end
1553+
1554+
local resolveArgs = args
1555+
if source.func and source.func.type == 'getmethod' then
1556+
local receiver = source.func.node
1557+
if receiver then
1558+
resolveArgs = { receiver }
1559+
if args then
1560+
for i = 2, #args do
1561+
resolveArgs[#resolveArgs + 1] = args[i]
1562+
end
1563+
end
1564+
end
1565+
end
1566+
15531567
local returnNode = vm.compileNode(returnObject)
1568+
1569+
local selfGenericResolved = nil
1570+
if source.func and source.func.type == 'getmethod' and mfunc.type == 'function' and mfunc.bindDocs then
1571+
local receiver = source.func.node
1572+
if receiver then
1573+
local receiverNode = vm.compileNode(receiver)
1574+
local selfGenericName = nil
1575+
for _, doc in ipairs(mfunc.bindDocs) do
1576+
if doc.type == 'doc.param' and doc.param and doc.param[1] == 'self' then
1577+
if doc.extends then
1578+
for _, typeUnit in ipairs(doc.extends.types or {}) do
1579+
if typeUnit.type == 'doc.generic.name' then
1580+
selfGenericName = typeUnit[1]
1581+
break
1582+
end
1583+
end
1584+
end
1585+
break
1586+
end
1587+
end
1588+
if selfGenericName then
1589+
local filteredNode = vm.createNode()
1590+
for item in receiverNode:eachObject() do
1591+
if item.type == 'doc.type.sign'
1592+
or (item.type == 'global' and item.cate == 'type')
1593+
or item.type == 'doc.type.table'
1594+
or item.type == 'doc.type.array' then
1595+
filteredNode:merge(item)
1596+
end
1597+
end
1598+
if not filteredNode:isEmpty() then
1599+
selfGenericResolved = { [selfGenericName] = filteredNode }
1600+
else
1601+
selfGenericResolved = { [selfGenericName] = receiverNode }
1602+
end
1603+
end
1604+
end
1605+
end
1606+
15541607
for rnode in returnNode:eachObject() do
15551608
if rnode.type == 'generic' then
1556-
returnNode = rnode:resolve(guide.getUri(source), args)
1609+
if selfGenericResolved and rnode.sign then
1610+
local resolved = rnode.sign:resolve(guide.getUri(source), resolveArgs) or {}
1611+
for k, v in pairs(selfGenericResolved) do
1612+
resolved[k] = v
1613+
end
1614+
local protoNode = vm.compileNode(rnode.proto)
1615+
local result = vm.createNode()
1616+
for nd in protoNode:eachObject() do
1617+
if nd.type == 'global' or nd.type == 'variable' then
1618+
result:merge(nd)
1619+
else
1620+
local clonedObject = vm.cloneObject(nd, resolved)
1621+
if clonedObject then
1622+
result:merge(vm.compileNode(clonedObject))
1623+
end
1624+
end
1625+
end
1626+
if protoNode:isOptional() then
1627+
result:addOptional()
1628+
end
1629+
returnNode = result
1630+
else
1631+
returnNode = rnode:resolve(guide.getUri(source), resolveArgs)
1632+
end
15571633
break
15581634
end
15591635
end
15601636

1561-
-- Handle method calls on generic class instances
1562-
-- When calling b:getValue() where b is Box<string>, resolve T to string
1637+
if mfunc.type == 'function' then
1638+
local hasUnresolvedGeneric = false
1639+
for rnode in returnNode:eachObject() do
1640+
if rnode.type == 'doc.generic.name' and not rnode._resolved then
1641+
hasUnresolvedGeneric = true
1642+
break
1643+
end
1644+
end
1645+
if hasUnresolvedGeneric then
1646+
local sign = vm.getSign(mfunc)
1647+
if sign and resolveArgs and #resolveArgs > 0 then
1648+
local resolved = sign:resolve(guide.getUri(source), resolveArgs)
1649+
if resolved and next(resolved) then
1650+
local newReturnNode = vm.createNode()
1651+
for rnode in returnNode:eachObject() do
1652+
local cloned = vm.cloneObject(rnode, resolved)
1653+
if cloned then
1654+
newReturnNode:merge(vm.compileNode(cloned))
1655+
else
1656+
newReturnNode:merge(rnode)
1657+
end
1658+
end
1659+
returnNode = newReturnNode
1660+
end
1661+
end
1662+
end
1663+
end
1664+
15631665
local call = source.parent
1564-
if call and call.type == 'call' then
1666+
if not selfGenericResolved and call and call.type == 'call' then
15651667
local callNode = call.node
15661668
if callNode and (callNode.type == 'getmethod' or callNode.type == 'getfield') then
15671669
local receiver = callNode.node
@@ -1571,7 +1673,6 @@ local function bindReturnOfFunction(source, mfunc, index, args)
15711673
if rn.type == 'doc.type.sign' and rn.signs and rn.node and rn.node[1] then
15721674
local classGlobal = vm.getGlobal('type', rn.node[1])
15731675
if classGlobal then
1574-
-- Build a map of class generic param names to their concrete types
15751676
local genericMap = {}
15761677
for _, set in ipairs(classGlobal:getSets(guide.getUri(source))) do
15771678
if set.type == 'doc.class' and set.signs then
@@ -1616,7 +1717,6 @@ local function bindReturnOfFunction(source, mfunc, index, args)
16161717

16171718
if returnNode then
16181719
for rnode in returnNode:eachObject() do
1619-
-- TODO: narrow type
16201720
if rnode.type ~= 'doc.generic.name' then
16211721
vm.setNode(source, rnode)
16221722
end
@@ -2191,7 +2291,11 @@ local compilerSwitch = util.switch()
21912291
end)
21922292
: case 'doc.generic.name'
21932293
: call(function (source)
2194-
vm.setNode(source, source)
2294+
if source._resolved then
2295+
vm.setNode(source, source._resolved)
2296+
else
2297+
vm.setNode(source, source)
2298+
end
21952299
end)
21962300
: case 'doc.type.sign'
21972301
: call(function (source)

test/type_inference/common.lua

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5046,3 +5046,126 @@ local f
50465046
50475047
local <?wrapped?> = f:wrap()
50485048
]]
5049+
5050+
-- Issue #723: Generics in @overload
5051+
-- @generic should work with @overload annotations
5052+
TEST 'string' [[
5053+
---@generic T
5054+
---@param x T
5055+
---@return T
5056+
---@overload fun(x: T): T
5057+
local function identity(x)
5058+
return x
5059+
end
5060+
5061+
local <?result?> = identity("hello")
5062+
]]
5063+
5064+
-- Issue #723: Multiple generics in @overload
5065+
TEST 'integer' [[
5066+
---@generic K, V
5067+
---@param k K
5068+
---@param v V
5069+
---@return V
5070+
---@overload fun(k: K, v: V): V
5071+
local function getValue(k, v)
5072+
return v
5073+
end
5074+
5075+
local <?result?> = getValue("key", 42)
5076+
]]
5077+
5078+
-- Issue #1170: Generics in function type format (fun<T>)
5079+
TEST 'string' [[
5080+
---@type fun<T>(x: T): T
5081+
local identity
5082+
5083+
local <?result?> = identity("hello")
5084+
]]
5085+
5086+
-- Issue #1170: Multiple generics in function type
5087+
TEST 'boolean' [[
5088+
---@type fun<K, V>(k: K, v: V): V
5089+
local getSecond
5090+
5091+
local <?result?> = getSecond("key", true)
5092+
]]
5093+
5094+
-- Issue #1170: Generic function in @field
5095+
TEST 'integer' [[
5096+
---@class Mapper
5097+
---@field transform fun<T, U>(input: T, fn: fun(x: T): U): U
5098+
5099+
---@type Mapper
5100+
local m
5101+
5102+
local <?result?> = m.transform("hello", function(x) return #x end)
5103+
]]
5104+
5105+
-- Issue #1532: Promise-like method chaining
5106+
-- Method returning self-type should preserve generic param through chain
5107+
TEST 'string' [[
5108+
---@class Promise<T>
5109+
---@field value T
5110+
local Promise = {}
5111+
5112+
---@return Promise<T>
5113+
function Promise:next(fn)
5114+
return self
5115+
end
5116+
5117+
---@return T
5118+
function Promise:await()
5119+
return self.value
5120+
end
5121+
5122+
---@type Promise<string>
5123+
local p
5124+
5125+
local <?result?> = p:next(function() end):await()
5126+
]]
5127+
5128+
-- Issue #1532: Multiple chained methods
5129+
TEST 'number' [[
5130+
---@class Chain<V>
5131+
local Chain = {}
5132+
5133+
---@return Chain<V>
5134+
function Chain:map(fn)
5135+
return self
5136+
end
5137+
5138+
---@return Chain<V>
5139+
function Chain:filter(fn)
5140+
return self
5141+
end
5142+
5143+
---@return V
5144+
function Chain:first()
5145+
return nil
5146+
end
5147+
5148+
---@type Chain<number>
5149+
local c
5150+
5151+
local <?result?> = c:map(function() end):filter(function() end):first()
5152+
]]
5153+
5154+
-- Issue #1000: Generic self parameter should resolve to concrete type
5155+
-- When @generic T and @param self T, calling on List<number> should return List<number>
5156+
TEST 'List<number>' [[
5157+
---@class List<V>
5158+
local List = {}
5159+
5160+
---@generic T
5161+
---@param self T
5162+
---@return T
5163+
function List:identity()
5164+
return self
5165+
end
5166+
5167+
---@type List<number>
5168+
local mylist
5169+
5170+
local <?result?> = mylist:identity()
5171+
]]

0 commit comments

Comments
 (0)