Skip to content

Commit bafa71b

Browse files
committed
fix: method-level generic return overwritten by receiver class generic
When a generic class method returns a method-level generic, the receiver block clone would overwrite Round 1's correct resolution with an unresolved node. Skip the clone if the return doc references generics not in the class genericMap. Discussion: #3438
1 parent a9aa09a commit bafa71b

3 files changed

Lines changed: 38 additions & 7 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* `FIX` Deduplicate documentation bindings for parameters
77
* `FIX` Correct `math.type` meta return annotation to use `nil` instead of the string literal `'nil'`
88
* `FIX` Fix initial `nameStyle.config` not getting loaded in the appropriate workspace.
9+
* `FIX` Fix method-level generic return type being overwritten by receiver class generic resolution [#3438](https://github.com/LuaLS/lua-language-server/discussions/3438)
910

1011
## 3.18.2
1112
* `CHG` `duplicate-set-field` diagnostic now supports linked suppression: when any occurrence of a duplicate field is suppressed with `---@diagnostic disable` or `---@diagnostic disable-next-line`, all warnings for that field name will be suppressed

script/vm/compiler.lua

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,13 +1752,25 @@ local function bindReturnOfFunction(source, mfunc, index, args)
17521752
if doc.type == 'doc.return' then
17531753
for _, rtn in ipairs(doc.returns) do
17541754
if rtn.returnIndex == index then
1755-
local newRtn = vm.cloneObject(rtn, genericMap)
1756-
if newRtn then
1757-
returnNode = vm.compileNode(newRtn)
1758-
for rnode in returnNode:eachObject() do
1759-
if rnode.type == 'generic' then
1760-
returnNode = rnode:resolve(guide.getUri(source), args)
1761-
break
1755+
-- Only clone if the return type only references class-level generics
1756+
-- (i.e. generics that exist in the genericMap from the receiver).
1757+
-- Method-level generics (e.g. V not in {T=string}) are
1758+
-- already resolved correctly in the first round above.
1759+
local onlyClassGenerics = true
1760+
guide.eachSourceType(rtn, 'doc.generic.name', function(src)
1761+
if not genericMap[src[1]] then
1762+
onlyClassGenerics = false
1763+
end
1764+
end)
1765+
if onlyClassGenerics then
1766+
local newRtn = vm.cloneObject(rtn, genericMap)
1767+
if newRtn then
1768+
returnNode = vm.compileNode(newRtn)
1769+
for rnode in returnNode:eachObject() do
1770+
if rnode.type == 'generic' then
1771+
returnNode = rnode:resolve(guide.getUri(source), args)
1772+
break
1773+
end
17621774
end
17631775
end
17641776
end

test/type_inference/common.lua

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5215,3 +5215,21 @@ local mylist
52155215
52165216
local <?result?> = mylist:identity()
52175217
]]
5218+
5219+
-- Discussion #3438: Method-level generic return type not clobbered by receiver block
5220+
-- When a generic class method has @return R where R is method-only (not in class genericMap),
5221+
-- the receiver block should not overwrite Round 1's correct resolution.
5222+
TEST 'integer' [[
5223+
---@class MyClass<T>
5224+
local MyClass = {}
5225+
5226+
---@generic R
5227+
---@param val R
5228+
---@return R
5229+
function MyClass:pass(val) return val end
5230+
5231+
---@type MyClass<string>
5232+
local myClass
5233+
5234+
local <?result?> = myClass:pass(42)
5235+
]]

0 commit comments

Comments
 (0)