From bafa71b14c67bdfb9e560bca9a6cdc9188b58820 Mon Sep 17 00:00:00 2001 From: Tom Lau Date: Sun, 26 Jul 2026 15:44:40 +0800 Subject: [PATCH] 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 --- changelog.md | 1 + script/vm/compiler.lua | 26 +++++++++++++++++++------- test/type_inference/common.lua | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) 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) +]]