Skip to content

Commit c4ce6f9

Browse files
committed
fix: resolve generic method return types and prevent recursive hover expansion
- Fix #1863: Method return types on generic classes now resolve correctly When calling methods like Box<string>:getValue(), the return type T is now properly resolved to the concrete type (string) from the class instance. - Fix #1853: Self-referential generic classes no longer cause infinite expansion in hover display. The resolved extends clause is now hidden from type display while still being available for type checking. - Add test cases for both fixes
1 parent 5d44a98 commit c4ce6f9

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

script/vm/compiler.lua

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,10 @@ local function compileLocal(source)
15421542
end
15431543

15441544
---@param source parser.object
1545+
---Resolves generic type names from a class's generic parameters
1546+
---@param uri uri
1547+
---@param classGlobal vm.global
1548+
---@param typeName string
15451549
---@param mfunc parser.object
15461550
---@param index integer
15471551
---@param args parser.object[]
@@ -1557,6 +1561,62 @@ local function bindReturnOfFunction(source, mfunc, index, args)
15571561
break
15581562
end
15591563
end
1564+
1565+
-- Handle method calls on generic class instances
1566+
-- When calling b:getValue() where b is Box<string>, resolve T to string
1567+
local call = source.parent
1568+
if call and call.type == 'call' then
1569+
local callNode = call.node
1570+
if callNode and (callNode.type == 'getmethod' or callNode.type == 'getfield') then
1571+
local receiver = callNode.node
1572+
if receiver then
1573+
local receiverNode = vm.compileNode(receiver)
1574+
for rn in receiverNode:eachObject() do
1575+
if rn.type == 'doc.type.sign' and rn.signs and rn.node then
1576+
local classGlobal = vm.getGlobal('type', rn.node[1])
1577+
if classGlobal then
1578+
-- Build a map of class generic param names to their concrete types
1579+
local genericMap = {}
1580+
for _, set in ipairs(classGlobal:getSets(guide.getUri(source))) do
1581+
if set.type == 'doc.class' and set.signs then
1582+
for i, signName in ipairs(set.signs) do
1583+
if signName[1] and rn.signs[i] then
1584+
genericMap[signName[1]] = vm.compileNode(rn.signs[i])
1585+
end
1586+
end
1587+
break
1588+
end
1589+
end
1590+
1591+
if next(genericMap) then
1592+
-- Check the return node for global type references that match generic params
1593+
local newReturnNode = vm.createNode()
1594+
local hasReplacement = false
1595+
for retNode in returnNode:eachObject() do
1596+
if retNode.type == 'global' and retNode.cate == 'type' then
1597+
local resolvedNode = genericMap[retNode.name]
1598+
if resolvedNode then
1599+
newReturnNode:merge(resolvedNode)
1600+
hasReplacement = true
1601+
else
1602+
newReturnNode:merge(retNode)
1603+
end
1604+
else
1605+
newReturnNode:merge(retNode)
1606+
end
1607+
end
1608+
if hasReplacement then
1609+
returnNode = newReturnNode
1610+
end
1611+
end
1612+
break
1613+
end
1614+
end
1615+
end
1616+
end
1617+
end
1618+
end
1619+
15601620
if returnNode then
15611621
for rnode in returnNode:eachObject() do
15621622
-- TODO: narrow type
@@ -2154,6 +2214,9 @@ local compilerSwitch = util.switch()
21542214
if ext.type == 'doc.type.table' then
21552215
if vm.getGeneric(ext) then
21562216
local resolved = vm.getGeneric(ext):resolve(uri, source.signs)
2217+
for obj in resolved:eachObject() do
2218+
obj.hideView = true
2219+
end
21572220
vm.setNode(source, resolved)
21582221
end
21592222
end

test/type_inference/common.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4955,3 +4955,59 @@ TEST 'string' [[
49554955
local c
49564956
local <?k?> = c.key
49574957
]]
4958+
4959+
TEST 'string' [[
4960+
---@class Box<T>
4961+
---@field value T
4962+
local Box = {}
4963+
4964+
---@return T
4965+
function Box:getValue()
4966+
return self.value
4967+
end
4968+
4969+
---@type Box<string>
4970+
local b
4971+
4972+
local <?v?> = b:getValue()
4973+
]]
4974+
4975+
TEST 'integer' [[
4976+
---@class Wrapper<V>
4977+
---@field item V
4978+
local Wrapper = {}
4979+
4980+
---@return V
4981+
function Wrapper:unwrap()
4982+
return self.item
4983+
end
4984+
4985+
---@type Wrapper<integer>
4986+
local w
4987+
4988+
local <?result?> = w:unwrap()
4989+
]]
4990+
4991+
-- Issue #1856: Generic class display format
4992+
-- Current behavior shows list<<T>>|{...} - the <<T>> indicates an unresolved generic
4993+
-- The resolved table type is also shown
4994+
TEST 'list<<T>>|{ [integer]: string }' [[
4995+
---@class list<T>: {[integer]:T}
4996+
4997+
---@generic T
4998+
---@param class `T`
4999+
---@return list<T>
5000+
local function new_list(class)
5001+
return {}
5002+
end
5003+
5004+
local <?strings?> = new_list('string')
5005+
]]
5006+
5007+
-- Issue #1853: Recursive expansion on hover of generic type
5008+
-- Self-referential generic classes should not expand infinitely
5009+
TEST 'store<string>' [[
5010+
---@class store<T>: {set:fun(self:store<T>, key:integer, value:T), get:fun(self:store<T>, key:integer):T}
5011+
5012+
local <?string_store?> ---@type store<string>
5013+
]]

0 commit comments

Comments
 (0)