Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# changelog

## Unreleased
* `FIX` Generic classes with generic methods now have correct type inference.
<!-- Add all new changes here. They will be moved under a version at release -->

## 3.19.2
Expand Down
39 changes: 28 additions & 11 deletions script/vm/compiler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 40 additions & 10 deletions script/vm/generic.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {},
}
Expand Down Expand Up @@ -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
Expand Down
108 changes: 108 additions & 0 deletions test/type_inference/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5278,3 +5278,111 @@ local myClass

local <?result?> = myClass:pass(42)
]]

-- Generic method on generic class via @field
TEST 'Parser<[integer, string]>' [[
---@class Parser<T>
---@field parse fun(self: Parser<T>, content: string): T?
---@field add fun<U>(self: Parser<T>, other: Parser<U>): Parser<[T, U]>

---@type Parser<integer>
local x
---@type Parser<string>
local y
local <?a?> = x:add(y)
]]

TEST 'integer?' [[
---@class Parser<T>
---@field parse fun(self: Parser<T>, content: string): T?
---@field add fun<U>(self: Parser<T>, other: Parser<U>): Parser<[T, U]>

---@type Parser<integer>
local x
local <?res?> = x:parse('foo')
]]

TEST '[integer, string]?' [[
---@class Parser<T>
---@field parse fun(self: Parser<T>, content: string): T?
---@field add fun<U>(self: Parser<T>, other: Parser<U>): Parser<[T, U]>

---@type Parser<integer>
local x
---@type Parser<string>
local y
local a = x:add(y)
local <?res?> = a:parse('foo')
]]

-- Generic method on generic class via ---@generic
TEST 'Parser<[integer, string]>' [[
---@class Parser<T>
local parser = {}

---@generic U
---@param other Parser<U>
---@return Parser<[T, U]>
function parser:add(other)
return {}
end

---@param content string
---@return T?
function parser:parse(content)
return
end

---@type Parser<integer>
local u
---@type Parser<string>
local v
local <?w?> = u:add(v)
]]

TEST 'integer?' [[
---@class Parser<T>
local parser = {}

---@generic U
---@param other Parser<U>
---@return Parser<[T, U]>
function parser:add(other)
return {}
end

---@param content string
---@return T?
function parser:parse(content)
return
end

---@type Parser<integer>
local u
local <?res?> = u:parse('foo')
]]

TEST '[integer, string]?' [[
---@class Parser<T>
local parser = {}

---@generic U
---@param other Parser<U>
---@return Parser<[T, U]>
function parser:add(other)
return {}
end

---@param content string
---@return T?
function parser:parse(content)
return
end

---@type Parser<integer>
local u
---@type Parser<string>
local v
local w = u:add(v)
local <?res?> = w:parse('foo')
]]
Loading