diff --git a/changelog.md b/changelog.md index c9375a4cd..ea12c7f0b 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ * `FIX` Deduplicate documentation bindings for parameters * `FIX` Correct `math.type` meta return annotation to use `nil` instead of the string literal `'nil'` * `FIX` Fix initial `nameStyle.config` not getting loaded in the appropriate workspace. +* `FIX` Fix method-level generic return type being overwritten by receiver class generic resolution [#3438](https://github.com/LuaLS/lua-language-server/discussions/3438) ## 3.18.2 * `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 diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 5a673e5fa..f90ed7d9c 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1752,13 +1752,25 @@ local function bindReturnOfFunction(source, mfunc, index, args) if doc.type == 'doc.return' then for _, rtn in ipairs(doc.returns) do if rtn.returnIndex == index then - local newRtn = vm.cloneObject(rtn, genericMap) - if newRtn then - returnNode = vm.compileNode(newRtn) - for rnode in returnNode:eachObject() do - if rnode.type == 'generic' then - returnNode = rnode:resolve(guide.getUri(source), args) - break + -- Only clone if the return type only references class-level generics + -- (i.e. generics that exist in the genericMap from the receiver). + -- Method-level generics (e.g. V not in {T=string}) are + -- already resolved correctly in the first round above. + local onlyClassGenerics = true + guide.eachSourceType(rtn, 'doc.generic.name', function(src) + if not genericMap[src[1]] then + onlyClassGenerics = false + end + end) + if onlyClassGenerics then + local newRtn = vm.cloneObject(rtn, genericMap) + if newRtn then + returnNode = vm.compileNode(newRtn) + for rnode in returnNode:eachObject() do + if rnode.type == 'generic' then + returnNode = rnode:resolve(guide.getUri(source), args) + break + end end end end diff --git a/test/type_inference/common.lua b/test/type_inference/common.lua index bd2de1ea5..48453bcbb 100644 --- a/test/type_inference/common.lua +++ b/test/type_inference/common.lua @@ -5215,3 +5215,21 @@ local mylist local = mylist:identity() ]] + +-- Discussion #3438: Method-level generic return type not clobbered by receiver block +-- When a generic class method has @return R where R is method-only (not in class genericMap), +-- the receiver block should not overwrite Round 1's correct resolution. +TEST 'integer' [[ +---@class MyClass +local MyClass = {} + +---@generic R +---@param val R +---@return R +function MyClass:pass(val) return val end + +---@type MyClass +local myClass + +local = myClass:pass(42) +]]