Skip to content

Commit f322cdc

Browse files
RomanSpectorclaude
andcommitted
Add class-level generic constraints, subtype check for generic parents, and iterator fixes
- Parse constraints in class sign parameters: @Class Foo<T: Button> now stores T's constraint (extends) for display and future use - Add doc.generic.name to child map in guide.lua so constraint types are walked by the compiler - Auto-bind class-level generics to methods via post-pass in bindDocs, so methods without @Generic inherit their class's type parameters - Fix subtype check for generic class parents: Child : Parent<T> is now recognized as subtype of Parent (handles doc.type.sign extends) - Fix V[] matching T[] in iterator resolution when T is a generic name (direct assignment bypassing resolve() filter) - Add tests for isearch/ipairs with T[] field types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5c78df1 commit f322cdc

File tree

5 files changed

+173
-2
lines changed

5 files changed

+173
-2
lines changed

script/parser/guide.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ local childMap = {
175175
['doc.field'] = {'field', 'extends', 'comment'},
176176
['doc.generic'] = {'#generics', 'comment'},
177177
['doc.generic.object'] = {'generic', 'extends', 'comment'},
178+
['doc.generic.name'] = {'extends'},
178179
['doc.vararg'] = {'vararg', 'comment'},
179180
['doc.type.array'] = {'node'},
180181
['doc.type.function'] = {'#args', '#returns', '#signs', 'comment'},

script/parser/luadoc.lua

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,11 @@ local function parseSigns(parent)
485485
}
486486
break
487487
end
488+
-- Parse optional constraint: <T: Button>
489+
if checkToken('symbol', ':', 1) then
490+
nextToken()
491+
sign.extends = parseType(sign)
492+
end
488493
signs[#signs+1] = sign
489494
if checkToken('symbol', ',', 1) then
490495
nextToken()
@@ -2253,6 +2258,81 @@ local function bindDocs(state)
22532258
end
22542259
end
22552260
end
2261+
2262+
-- Second pass: bind class-level generic parameters for methods.
2263+
-- When a method like QernelContainerTemplate:OnHide has no @generic T,
2264+
-- the T comes from @class QernelContainerTemplate<T: Button>.
2265+
local classGenerics = {}
2266+
for _, doc in ipairs(state.ast.docs) do
2267+
if doc.type == 'doc.class' and doc.signs then
2268+
local className = doc.class[1]
2269+
if className then
2270+
local signs = {}
2271+
for _, sign in ipairs(doc.signs) do
2272+
signs[sign[1]] = sign
2273+
end
2274+
if next(signs) then
2275+
classGenerics[className] = signs
2276+
end
2277+
end
2278+
end
2279+
end
2280+
if next(classGenerics) then
2281+
for _, group in ipairs(state.ast.docs.groups) do
2282+
-- Find the bind source (the function/assignment this doc is attached to)
2283+
local bindSource
2284+
for _, doc in ipairs(group) do
2285+
if doc.bindSource then
2286+
bindSource = doc.bindSource
2287+
break
2288+
end
2289+
end
2290+
if not bindSource then
2291+
goto NEXT_GROUP
2292+
end
2293+
-- Get the class name from setmethod/setfield source
2294+
local parent = bindSource.type == 'function'
2295+
and bindSource.parent
2296+
if not parent then
2297+
goto NEXT_GROUP
2298+
end
2299+
local className
2300+
if parent.type == 'setmethod' or parent.type == 'setfield' then
2301+
className = parent.node and guide.getKeyName(parent.node)
2302+
end
2303+
if not className or not classGenerics[className] then
2304+
goto NEXT_GROUP
2305+
end
2306+
-- Skip if this group already has its own @generic
2307+
local hasOwnGeneric = false
2308+
for _, doc in ipairs(group) do
2309+
if doc.type == 'doc.generic' then
2310+
hasOwnGeneric = true
2311+
break
2312+
end
2313+
end
2314+
if hasOwnGeneric then
2315+
goto NEXT_GROUP
2316+
end
2317+
-- Bind class-level generics to type names in this group
2318+
local signs = classGenerics[className]
2319+
for _, doc in ipairs(group) do
2320+
if doc.type == 'doc.param'
2321+
or doc.type == 'doc.return'
2322+
or doc.type == 'doc.type'
2323+
or doc.type == 'doc.field'
2324+
or doc.type == 'doc.overload' then
2325+
guide.eachSourceType(doc, 'doc.type.name', function (src)
2326+
if signs[src[1]] then
2327+
src.type = 'doc.generic.name'
2328+
src.generic = signs[src[1]]
2329+
end
2330+
end)
2331+
end
2332+
end
2333+
::NEXT_GROUP::
2334+
end
2335+
end
22562336
end
22572337

22582338
local function findTouch(state, doc)

script/vm/sign.lua

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,22 @@ function mt:resolve(uri, args)
173173
if not handled then
174174
for n in node:eachObject() do
175175
if n.type == 'doc.type.array' then
176-
-- number[] -> T[]
177-
resolve(object.node, vm.compileNode(n.node))
176+
-- number[] -> T[], or T[] -> V[] (generic element)
177+
local elementNode = vm.compileNode(n.node)
178+
-- When element is a generic name (T[] matching V[]),
179+
-- assign directly since resolve() filters generics
180+
if object.node and object.node.type == 'doc.generic.name' then
181+
local vKey = object.node[1]
182+
for en in elementNode:eachObject() do
183+
if en.type == 'doc.generic.name' then
184+
resolved[vKey] = vm.createNode(en, resolved[vKey])
185+
handled = true
186+
end
187+
end
188+
end
189+
if not handled then
190+
resolve(object.node, elementNode)
191+
end
178192
elseif n.type == 'doc.type.table' then
179193
-- { [integer]: number } -> T[]
180194
local tvalueNode = vm.getTableValue(uri, node, 'integer', true)

script/vm/type.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,14 @@ function vm.isSubType(uri, child, parent, mark, errs)
595595
mark[childName] = nil
596596
return true
597597
end
598+
-- Handle generic class parents: Child : Parent<T>
599+
if ext.type == 'doc.type.sign'
600+
and ext.node and ext.node[1]
601+
and (not isBasicType or guide.isBasicType(ext.node[1]))
602+
and vm.isSubType(uri, ext.node[1], parent, mark, errs) == true then
603+
mark[childName] = nil
604+
return true
605+
end
598606
end
599607
end
600608
end

test/type_inference/common.lua

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,74 @@ function listB:foo()
951951
end
952952
]]
953953

954+
TEST '<T>' [[
955+
---@generic T: table, V
956+
---@param t T
957+
---@return fun(table: V[]): V
958+
---@return T
959+
local function isearch(t) end
960+
961+
---@class listC<T>: {[integer]:T}
962+
963+
---@generic T
964+
---@param self listC<T>
965+
function listC:foo()
966+
for <?v?> in isearch(self) do
967+
end
968+
end
969+
]]
970+
971+
-- Test: field access inside class method
972+
TEST 'string[]' [[
973+
---@class listD0
974+
---@field buttons string[]
975+
local listD0 = {}
976+
977+
function listD0:foo()
978+
local <?b?> = self.buttons
979+
end
980+
]]
981+
982+
-- With T[] field type
983+
TEST '<T>' [[
984+
---@generic T: table, V
985+
---@param t T
986+
---@return fun(table: V[]): V
987+
---@return T
988+
local function isearch(t) end
989+
990+
---@class listD<T>: {[integer]:T}
991+
---@field buttons T[]
992+
local listD = {}
993+
994+
function listD:foo()
995+
for <?v?> in isearch(self.buttons) do
996+
end
997+
end
998+
]]
999+
1000+
-- With list<T> field type — concrete (should work)
1001+
TEST 'string' [[
1002+
---@generic T: table, V
1003+
---@param t T
1004+
---@return fun(table: V[]): V
1005+
---@return T
1006+
local function isearch(t) end
1007+
1008+
---@class listE<T>: {[integer]:T}
1009+
1010+
---@type listE<string>
1011+
local strings
1012+
1013+
for <?v?> in isearch(strings) do
1014+
end
1015+
]]
1016+
1017+
-- TODO: nested generic resolution (listF<T> field) not yet supported
1018+
-- When @field buttons listF<T>, iterating gives the whole table type
1019+
-- instead of the element. This requires resolving V through nested
1020+
-- generic sign parameters.
1021+
9541022
TEST 'boolean' [[
9551023
---@generic T: table, K, V
9561024
---@param t T

0 commit comments

Comments
 (0)