Skip to content

Commit 5c78df1

Browse files
RomanSpectorclaude
andcommitted
Refactor: extract shared helpers and reduce nesting
- Extract vm.isResolvedToGeneric() helper to replace duplicated allGeneric checks in generic.lua, compiler.lua (3 locations) - Extract findGenericInTableFields() and simplify findGenericInExtendsTable() in sign.lua to reduce nesting - Add changelog entries for generic resolution fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d31f0c8 commit 5c78df1

File tree

4 files changed

+78
-57
lines changed

4 files changed

+78
-57
lines changed

changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* `CHG` Improved the output of test failures during development
77
* `FIX` Fix type inference for `x == nil and "default" or x` idiom [#2236](https://github.com/LuaLS/lua-language-server/issues/2236)
88
* `FIX` Fix type loss for assignments inside `if`/`for` blocks due to circular dependency in tracer [#2374](https://github.com/LuaLS/lua-language-server/issues/2374) [#2494](https://github.com/LuaLS/lua-language-server/issues/2494)
9+
* `FIX` Resolve generic class method return types for `@param self list<T>` pattern
10+
* `FIX` Fix `ipairs(self)` type resolution in generic class methods
11+
* `FIX` Fix double angle brackets in generic sign display (`list<<T>>` -> `list<T>`)
12+
* `FIX` Fix nil crash in `getParentClass` for `doc.field` without class
913

1014
## 3.17.1
1115
`2026-01-20`

script/vm/compiler.lua

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,19 +1671,10 @@ local function bindReturnOfFunction(source, mfunc, index, args)
16711671
else
16721672
local clonedObject = vm.cloneObject(nd, resolved)
16731673
if clonedObject then
1674-
if clonedObject.type == 'doc.generic.name' and clonedObject._resolved then
1675-
local allGeneric = true
1676-
for rn in clonedObject._resolved:eachObject() do
1677-
if rn.type ~= 'doc.generic.name' then
1678-
allGeneric = false
1679-
break
1680-
end
1681-
end
1682-
if allGeneric then
1683-
result:merge(clonedObject)
1684-
else
1685-
result:merge(vm.compileNode(clonedObject))
1686-
end
1674+
if clonedObject.type == 'doc.generic.name'
1675+
and clonedObject._resolved
1676+
and vm.isResolvedToGeneric(clonedObject._resolved) then
1677+
result:merge(clonedObject)
16871678
else
16881679
result:merge(vm.compileNode(clonedObject))
16891680
end
@@ -1790,16 +1781,7 @@ local function bindReturnOfFunction(source, mfunc, index, args)
17901781
elseif rnode._resolved then
17911782
-- Allow generics that resolved to another generic type
17921783
-- parameter (e.g. V -> T in generic method's ipairs(self)).
1793-
-- Only allow when resolved purely to other generics, not
1794-
-- to concrete types like string/boolean.
1795-
local allGeneric = true
1796-
for rn in rnode._resolved:eachObject() do
1797-
if rn.type ~= 'doc.generic.name' then
1798-
allGeneric = false
1799-
break
1800-
end
1801-
end
1802-
if allGeneric then
1784+
if vm.isResolvedToGeneric(rnode._resolved) then
18031785
vm.setNode(source, rnode)
18041786
end
18051787
end

script/vm/generic.lua

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,20 +199,10 @@ function mt:resolve(uri, args)
199199
-- inside a generic method), keep the resolved wrapper so
200200
-- the resolution chain is preserved and downstream filters
201201
-- can distinguish "resolved to generic T" from "unresolved".
202-
if clonedObject.type == 'doc.generic.name' and clonedObject._resolved then
203-
local allGeneric = true
204-
for rn in clonedObject._resolved:eachObject() do
205-
if rn.type ~= 'doc.generic.name' then
206-
allGeneric = false
207-
break
208-
end
209-
end
210-
if allGeneric then
211-
result:merge(clonedObject)
212-
else
213-
local clonedNode = vm.compileNode(clonedObject)
214-
result:merge(clonedNode)
215-
end
202+
if clonedObject.type == 'doc.generic.name'
203+
and clonedObject._resolved
204+
and vm.isResolvedToGeneric(clonedObject._resolved) then
205+
result:merge(clonedObject)
216206
else
217207
local clonedNode = vm.compileNode(clonedObject)
218208
result:merge(clonedNode)
@@ -243,6 +233,20 @@ function vm.isGenericUnsolved(source)
243233
return false
244234
end
245235

236+
--- Check if a resolved node contains only generic name objects.
237+
--- Used to distinguish "V resolved to generic T" (preserve wrapper)
238+
--- from "V resolved to concrete string" (unwrap normally).
239+
---@param node vm.node
240+
---@return boolean
241+
function vm.isResolvedToGeneric(node)
242+
for rn in node:eachObject() do
243+
if rn.type ~= 'doc.generic.name' then
244+
return false
245+
end
246+
end
247+
return true
248+
end
249+
246250
---@param source parser.object
247251
---@param generic vm.generic
248252
function vm.setGeneric(source, generic)

script/vm/sign.lua

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,53 @@ local guide = require 'parser.guide'
22
---@class vm
33
local vm = require 'vm.vm'
44

5+
--- Find a generic name referenced in a doc.type.table's fields
6+
--- that exists in the given genericMap.
7+
---@param tableType parser.object doc.type.table with fields
8+
---@param genericMap table<string, vm.node>
9+
---@return string? The matching generic key name
10+
local function findGenericInTableFields(tableType, genericMap)
11+
for _, field in ipairs(tableType.fields) do
12+
if field.extends then
13+
local found
14+
guide.eachSourceType(field.extends, 'doc.generic.name', function (src)
15+
if genericMap[src[1]] then
16+
found = src[1]
17+
end
18+
end)
19+
if found then
20+
return found
21+
end
22+
end
23+
end
24+
return nil
25+
end
26+
27+
--- Search for a generic name in extends tables of a class definition.
28+
--- For classes like `@class list<T>: {[integer]:T}`, the [integer] field
29+
--- lives in the extends doc.type.table, not in @field annotations.
30+
---@param uri uri
31+
---@param classGlobal vm.global
32+
---@param genericMap table<string, vm.node>
33+
---@return string? The class generic name that maps to the integer field
34+
local function findGenericInExtendsTable(uri, classGlobal, genericMap)
35+
for _, set in ipairs(classGlobal:getSets(uri)) do
36+
if set.type ~= 'doc.class' or not set.extends then
37+
goto CONTINUE
38+
end
39+
for _, ext in ipairs(set.extends) do
40+
if ext.type == 'doc.type.table' and ext.fields then
41+
local key = findGenericInTableFields(ext, genericMap)
42+
if key then
43+
return key
44+
end
45+
end
46+
end
47+
::CONTINUE::
48+
end
49+
return nil
50+
end
51+
552
---@class vm.sign
653
---@field parent parser.object
754
---@field signList vm.node[]
@@ -112,26 +159,10 @@ function mt:resolve(uri, args)
112159
end)
113160
-- Also search extends tables (for @class list<T>: {[integer]:T})
114161
if not handled then
115-
for _, set in ipairs(classGlobal:getSets(uri)) do
116-
if set.type == 'doc.class' and set.extends then
117-
for _, ext in ipairs(set.extends) do
118-
if ext.type == 'doc.type.table' and ext.fields then
119-
for _, field in ipairs(ext.fields) do
120-
if field.extends then
121-
guide.eachSourceType(field.extends, 'doc.generic.name', function (src)
122-
if genericMap[src[1]] then
123-
resolved[vKey] = genericMap[src[1]]
124-
handled = true
125-
end
126-
end)
127-
end
128-
if handled then break end
129-
end
130-
end
131-
if handled then break end
132-
end
133-
end
134-
if handled then break end
162+
local genericKey = findGenericInExtendsTable(uri, classGlobal, genericMap)
163+
if genericKey then
164+
resolved[vKey] = genericMap[genericKey]
165+
handled = true
135166
end
136167
end
137168
end

0 commit comments

Comments
 (0)