-
-
Notifications
You must be signed in to change notification settings - Fork 423
fix: generic type resolution for class methods and iterators #3392
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
46d0faf
eb1135e
2276dc2
c3c783e
a72090f
3e19eb2
d31f0c8
5c78df1
513cedd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1671,7 +1671,22 @@ local function bindReturnOfFunction(source, mfunc, index, args) | |
| else | ||
| local clonedObject = vm.cloneObject(nd, resolved) | ||
| if clonedObject then | ||
| result:merge(vm.compileNode(clonedObject)) | ||
| if clonedObject.type == 'doc.generic.name' and clonedObject._resolved then | ||
| local allGeneric = true | ||
| for rn in clonedObject._resolved:eachObject() do | ||
| if rn.type ~= 'doc.generic.name' then | ||
| allGeneric = false | ||
| break | ||
| end | ||
| end | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic to check if a resolved node contains only generic names is duplicated in this file (lines 1796-1801) and also in For example, you could define a function in function vm.isPurelyGenericNode(node)
if not node then return false end
for rn in node:eachObject() do
if rn.type ~= 'doc.generic.name' then
return false
end
end
return true
endThen you could simplify this block and the other occurrences by calling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Extracted |
||
| if allGeneric then | ||
| result:merge(clonedObject) | ||
| else | ||
| result:merge(vm.compileNode(clonedObject)) | ||
| end | ||
| else | ||
| result:merge(vm.compileNode(clonedObject)) | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
@@ -1686,13 +1701,25 @@ local function bindReturnOfFunction(source, mfunc, index, args) | |
| end | ||
| end | ||
|
|
||
| if mfunc.type == 'function' then | ||
| if mfunc.type == 'function' or mfunc.type == 'doc.type.function' then | ||
| local hasUnresolvedGeneric = false | ||
| for rnode in returnNode:eachObject() do | ||
| if vm.isGenericUnsolved(rnode) then | ||
| hasUnresolvedGeneric = true | ||
| break | ||
| end | ||
| -- Also check inside doc.type.sign for unresolved generics | ||
| -- (e.g. list<T> where T is not yet resolved) | ||
| if rnode.type == 'doc.type.sign' and rnode.signs then | ||
| guide.eachSourceType(rnode, 'doc.generic.name', function (src) | ||
| if not src._resolved then | ||
| hasUnresolvedGeneric = true | ||
| end | ||
| end) | ||
| if hasUnresolvedGeneric then | ||
| break | ||
| end | ||
| end | ||
| end | ||
| if hasUnresolvedGeneric then | ||
| local sign = vm.getSign(mfunc) | ||
|
|
@@ -1760,6 +1787,21 @@ local function bindReturnOfFunction(source, mfunc, index, args) | |
| for rnode in returnNode:eachObject() do | ||
| if rnode.type ~= 'doc.generic.name' then | ||
| vm.setNode(source, rnode) | ||
| elseif rnode._resolved then | ||
| -- Allow generics that resolved to another generic type | ||
| -- parameter (e.g. V -> T in generic method's ipairs(self)). | ||
| -- Only allow when resolved purely to other generics, not | ||
| -- to concrete types like string/boolean. | ||
| local allGeneric = true | ||
| for rn in rnode._resolved:eachObject() do | ||
| if rn.type ~= 'doc.generic.name' then | ||
| allGeneric = false | ||
| break | ||
| end | ||
| end | ||
| if allGeneric then | ||
| vm.setNode(source, rnode) | ||
| end | ||
| end | ||
| end | ||
| if returnNode:isOptional() then | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,28 +83,83 @@ function mt:resolve(uri, args) | |
| return | ||
| end | ||
| if object.type == 'doc.type.array' then | ||
| -- If the argument contains a doc.type.sign (generic class like | ||
| -- list<T> extending { [integer]: V }), resolve element type | ||
| -- exclusively through class generic map. This directly maps | ||
| -- the array element generic (V) to the sign parameter, even | ||
| -- when it's another generic name (T inside a method body). | ||
| local handled = false | ||
| for n in node:eachObject() do | ||
| if n.type == 'doc.type.array' then | ||
| -- number[] -> T[] | ||
| resolve(object.node, vm.compileNode(n.node)) | ||
| end | ||
| if n.type == 'doc.type.table' then | ||
| -- { [integer]: number } -> T[] | ||
| local tvalueNode = vm.getTableValue(uri, node, 'integer', true) | ||
| if tvalueNode then | ||
| resolve(object.node, tvalueNode) | ||
| if n.type == 'doc.type.sign' and n.signs and n.node and n.node[1] then | ||
| local classGlobal = vm.getGlobal('type', n.node[1]) | ||
| if classGlobal then | ||
| local genericMap = vm.getClassGenericMap(uri, classGlobal, n.signs) | ||
| if genericMap and object.node and object.node.type == 'doc.generic.name' then | ||
| -- V[] matching list<T>: look up [integer] field, | ||
| -- find which class generic it references, then | ||
| -- map V directly to the sign's concrete parameter | ||
| local vKey = object.node[1] | ||
| -- First try @field annotations | ||
| vm.getClassFields(uri, classGlobal, vm.declareGlobal('type', 'integer'), function (field) | ||
| if field.extends then | ||
| guide.eachSourceType(field.extends, 'doc.generic.name', function (src) | ||
| if genericMap[src[1]] then | ||
| resolved[vKey] = genericMap[src[1]] | ||
| handled = true | ||
| end | ||
| end) | ||
| end | ||
| end) | ||
| -- Also search extends tables (for @class list<T>: {[integer]:T}) | ||
| if not handled then | ||
| for _, set in ipairs(classGlobal:getSets(uri)) do | ||
| if set.type == 'doc.class' and set.extends then | ||
| for _, ext in ipairs(set.extends) do | ||
| if ext.type == 'doc.type.table' and ext.fields then | ||
| for _, field in ipairs(ext.fields) do | ||
| if field.extends then | ||
| guide.eachSourceType(field.extends, 'doc.generic.name', function (src) | ||
| if genericMap[src[1]] then | ||
| resolved[vKey] = genericMap[src[1]] | ||
| handled = true | ||
| end | ||
| end) | ||
| end | ||
| if handled then break end | ||
| end | ||
| end | ||
| if handled then break end | ||
| end | ||
| end | ||
| if handled then break end | ||
| end | ||
| end | ||
|
Comment on lines
+150
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of code has a couple of areas for improvement:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Extracted |
||
| end | ||
| end | ||
| if handled then break end | ||
| end | ||
| if n.type == 'global' and n.cate == 'type' then | ||
| -- ---@field [integer]: number -> T[] | ||
| ---@cast n vm.global | ||
| vm.getClassFields(uri, n, vm.declareGlobal('type', 'integer'), function (field) | ||
| resolve(object.node, vm.compileNode(field.extends)) | ||
| end) | ||
| end | ||
| if n.type == 'table' and #n >= 1 then | ||
| -- { x } / { ... } -> T[] | ||
| resolve(object.node, vm.compileNode(n[1])) | ||
| end | ||
| if not handled then | ||
| for n in node:eachObject() do | ||
| if n.type == 'doc.type.array' then | ||
| -- number[] -> T[] | ||
| resolve(object.node, vm.compileNode(n.node)) | ||
| elseif n.type == 'doc.type.table' then | ||
| -- { [integer]: number } -> T[] | ||
| local tvalueNode = vm.getTableValue(uri, node, 'integer', true) | ||
| if tvalueNode then | ||
| resolve(object.node, tvalueNode) | ||
| end | ||
| elseif n.type == 'global' and n.cate == 'type' then | ||
| -- ---@field [integer]: number -> T[] | ||
| ---@cast n vm.global | ||
| vm.getClassFields(uri, n, vm.declareGlobal('type', 'integer'), function (field) | ||
| resolve(object.node, vm.compileNode(field.extends)) | ||
| end) | ||
| elseif n.type == 'table' and #n >= 1 then | ||
| -- { x } / { ... } -> T[] | ||
| resolve(object.node, vm.compileNode(n[1])) | ||
| end | ||
| end | ||
| end | ||
| return | ||
|
|
@@ -176,6 +231,21 @@ function mt:resolve(uri, args) | |
| end | ||
| return | ||
| end | ||
| if object.type == 'doc.type.sign' and object.signs then | ||
| -- list<T> -> list<string>: match sign parameters positionally | ||
| for n in node:eachObject() do | ||
| if n.type == 'doc.type.sign' and n.signs | ||
| and n.node and object.node | ||
| and n.node[1] == object.node[1] then | ||
| for i, signParam in ipairs(object.signs) do | ||
| if n.signs[i] then | ||
| resolve(vm.compileNode(signParam), vm.compileNode(n.signs[i])) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| return | ||
| end | ||
| end | ||
|
|
||
| ---@param sign vm.node | ||
|
|
@@ -191,7 +261,8 @@ function mt:resolve(uri, args) | |
| end | ||
| if obj.type == 'doc.type.table' | ||
| or obj.type == 'doc.type.function' | ||
| or obj.type == 'doc.type.array' then | ||
| or obj.type == 'doc.type.array' | ||
| or obj.type == 'doc.type.sign' then | ||
| ---@cast obj parser.object | ||
| local hasGeneric | ||
| guide.eachSourceType(obj, 'doc.generic.name', function (src) | ||
|
|
@@ -203,7 +274,8 @@ function mt:resolve(uri, args) | |
| end | ||
| end | ||
| if obj.type == 'variable' | ||
| or obj.type == 'local' then | ||
| or obj.type == 'local' | ||
| or obj.type == 'self' then | ||
| goto CONTINUE | ||
| end | ||
| local view = vm.getInfer(obj):view(uri) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changelog entries are for secondary fixes in this PR. Please also add entries for the main fixes related to generic type resolution as described in the PR summary. For example:
FIXResolve generic class method return types.FIXCorrectly resolve nested generics indoc.type.sign.FIXResolve loop variables foripairs(self)in generic methods.FIXFix display of generic types with double angle brackets (e.g.,list<<T>>).FIXPrevent nil crash ingetParentClassfordoc.fieldwithout a class.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Added changelog entries for all main generic resolution fixes.