From 94e3e89da22987936c40b31b1a0a527c45fd432d Mon Sep 17 00:00:00 2001 From: Nathan <47441022+NathanSnail@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:28:29 +1300 Subject: [PATCH 1/2] fix: generic classes with generic methods --- script/vm/compiler.lua | 39 ++++++++---- script/vm/generic.lua | 50 ++++++++++++--- test/type_inference/common.lua | 108 +++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 21 deletions(-) diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 20ab3ac41..2d4561636 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1834,26 +1834,43 @@ 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 - -- 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 + -- Check if rtn references any class generics from the receiver. + local hasClassGenerics = false + local function checkGeneric(src) + if genericMap[src[1]] then + hasClassGenerics = true end - end) - if onlyClassGenerics then + end + guide.eachSourceType(rtn, 'doc.generic.name', checkGeneric) + if not hasClassGenerics then + guide.eachSourceType(rtn, 'doc.type.name', checkGeneric) + end + if hasClassGenerics 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) + returnNode = rnode:resolve(guide.getUri(source), resolveArgs) break end end + local sign = vm.getSign(mfunc) + if sign and resolveArgs and #resolveArgs > 0 then + local resolved = sign:resolve(guide.getUri(source), resolveArgs) + if resolved and next(resolved) then + local newReturnNode = vm.createNode() + for rnode in returnNode:eachObject() do + local cloned = vm.cloneObject(rnode, resolved) + if cloned then + newReturnNode:merge(vm.compileNode(cloned)) + else + newReturnNode:merge(rnode) + end + end + returnNode = newReturnNode + end + end end end break diff --git a/script/vm/generic.lua b/script/vm/generic.lua index 0b171622c..b6c095245 100644 --- a/script/vm/generic.lua +++ b/script/vm/generic.lua @@ -23,15 +23,35 @@ local function cloneObject(source, resolved) if source.type == 'doc.generic.name' then local key = source[1] local newName = { - type = source.type, - start = source.start, - finish = source.finish, - parent = source.parent, - [1] = source[1], + type = source.type, + start = source.start, + finish = source.finish, + parent = source.parent, + literal = source.literal, + generic = source.generic, + [1] = source[1], } if resolved[key] then vm.setNode(newName, resolved[key], true) newName._resolved = resolved[key] + elseif source._resolved then + local resolvedNode = vm.createNode() + local anyResolved = false + for rn in source._resolved:eachObject() do + if rn.type == 'doc.generic.name' and resolved[rn[1]] then + resolvedNode:merge(resolved[rn[1]]) + anyResolved = true + else + resolvedNode:merge(rn) + end + end + if anyResolved then + vm.setNode(newName, resolvedNode, true) + newName._resolved = resolvedNode + else + vm.setNode(newName, source._resolved, true) + newName._resolved = source._resolved + end end return newName end @@ -88,11 +108,12 @@ local function cloneObject(source, resolved) end if source.type == 'doc.type.table' then local newTable = { - type = source.type, - start = source.start, - finish = source.finish, - parent = source.parent, - fields = {}, + type = source.type, + start = source.start, + finish = source.finish, + parent = source.parent, + isTuple = source.isTuple, + fields = {}, } for i, field in ipairs(source.fields) do local newField = { @@ -113,6 +134,8 @@ local function cloneObject(source, resolved) start = source.start, finish = source.finish, parent = source.parent, + async = source.async, + signs = source.signs, args = {}, returns = {}, } @@ -157,6 +180,13 @@ local function cloneObject(source, resolved) guide.eachSourceType(sign, 'doc.generic.name', function (src) if hasConcreteResolution(src[1]) then needsClone = true + elseif src._resolved then + for rn in src._resolved:eachObject() do + if rn.type == 'doc.generic.name' and hasConcreteResolution(rn[1]) then + needsClone = true + break + end + end end end) end diff --git a/test/type_inference/common.lua b/test/type_inference/common.lua index 2fe6417d2..6ff643f48 100644 --- a/test/type_inference/common.lua +++ b/test/type_inference/common.lua @@ -5278,3 +5278,111 @@ local myClass local = myClass:pass(42) ]] + +-- Generic method on generic class via @field +TEST 'Parser<[integer, string]>' [[ +---@class Parser +---@field parse fun(self: Parser, content: string): T? +---@field add fun(self: Parser, other: Parser): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local = x:add(y) +]] + +TEST 'integer?' [[ +---@class Parser +---@field parse fun(self: Parser, content: string): T? +---@field add fun(self: Parser, other: Parser): Parser<[T, U]> + +---@type Parser +local x +local = x:parse('foo') +]] + +TEST '[integer, string]?' [[ +---@class Parser +---@field parse fun(self: Parser, content: string): T? +---@field add fun(self: Parser, other: Parser): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local a = x:add(y) +local = a:parse('foo') +]] + +-- Generic method on generic class via ---@generic +TEST 'Parser<[integer, string]>' [[ +---@class Parser +local parser = {} + +---@generic U +---@param other Parser +---@return Parser<[T, U]> +function parser:add(other) + return {} +end + +---@param content string +---@return T? +function parser:parse(content) + return +end + +---@type Parser +local u +---@type Parser +local v +local = u:add(v) +]] + +TEST 'integer?' [[ +---@class Parser +local parser = {} + +---@generic U +---@param other Parser +---@return Parser<[T, U]> +function parser:add(other) + return {} +end + +---@param content string +---@return T? +function parser:parse(content) + return +end + +---@type Parser +local u +local = u:parse('foo') +]] + +TEST '[integer, string]?' [[ +---@class Parser +local parser = {} + +---@generic U +---@param other Parser +---@return Parser<[T, U]> +function parser:add(other) + return {} +end + +---@param content string +---@return T? +function parser:parse(content) + return +end + +---@type Parser +local u +---@type Parser +local v +local w = u:add(v) +local = w:parse('foo') +]] From f44c0e82319c8b59455e58cbaaa78cf51f62c037 Mon Sep 17 00:00:00 2001 From: Nathan <47441022+NathanSnail@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:35:18 +1300 Subject: [PATCH 2/2] chore: update changelog.md --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index daefdb5ae..278dce2a3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ # changelog ## Unreleased +* `FIX` Generic classes with generic methods now have correct type inference. ## 3.19.2