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
Expand Up @@ -2,6 +2,7 @@

## Unreleased
<!-- Add all new changes here. They will be moved under a version at release -->
* `FIX` Resolve unbound class generic parameters to `any` instead of leaving the parameter as the field type [#3442](https://github.com/LuaLS/lua-language-server/pull/3442)
* `NEW` Support type inference for `@field` and `@type` function declarations in method overrides [#3367](https://github.com/LuaLS/lua-language-server/issues/3367)
* `FIX` Deduplicate documentation bindings for parameters
* `FIX` Correct `math.type` meta return annotation to use `nil` instead of the string literal `'nil'`
Expand Down
61 changes: 55 additions & 6 deletions script/vm/compiler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -304,22 +304,45 @@ function vm.getClassGenericMap(uri, classGlobal, signs)
return nil
end

---Maps every declared generic parameter of a class to `any`.
---
---Used when a generic class is referenced without arguments (`---@type Box` instead of
---`Box<string>`). Its fields would otherwise keep the parameter itself as their type,
---which no value can ever have, making the field unusable and every place that mentions
---the class without arguments a false error.
---@param uri uri
---@param classGlobal vm.global
---@return table<string, vm.node>?
local function getClassAnyGenericMap(uri, classGlobal)
for _, set in ipairs(classGlobal:getSets(uri)) do
if set.type == 'doc.class' and set.signs then
local resolved = {}
local anyNode = vm.createNode()
anyNode:merge(vm.declareGlobal('type', 'any'))
for _, signName in ipairs(set.signs) do
if signName[1] then
resolved[signName[1]] = anyNode
end
end
if next(resolved) then
return resolved
end
break
end
end
return nil
end

---@param field parser.object | vm.generic
---@param signs parser.object[]
---@param resolved table<string, vm.node>
---@return parser.object?
local function resolveGenericField(uri, classGlobal, field, signs)
local function resolveFieldWithMap(field, resolved)
if field.type ~= 'doc.field' or not field.extends then
return nil
end
if not containsGenericName(field.extends) then
return nil
end
local resolved = vm.getClassGenericMap(uri, classGlobal, signs)
if not resolved then
return nil
end
local newExtends = vm.cloneObject(field.extends, resolved)
if not newExtends then
return nil
Expand All @@ -336,6 +359,25 @@ local function resolveGenericField(uri, classGlobal, field, signs)
}
end

---@param uri uri
---@param classGlobal vm.global
---@param field parser.object | vm.generic
---@param signs parser.object[]
---@return parser.object?
local function resolveGenericField(uri, classGlobal, field, signs)
if field.type ~= 'doc.field' or not field.extends then
return nil
end
if not containsGenericName(field.extends) then
return nil
end
local resolved = vm.getClassGenericMap(uri, classGlobal, signs)
if not resolved then
return nil
end
return resolveFieldWithMap(field, resolved)
end

local searchFieldSwitch = util.switch()
: case 'table'
: call(function (_suri, source, key, pushResult)
Expand Down Expand Up @@ -493,6 +535,13 @@ local searchFieldSwitch = util.switch()
end
end
if node.cate == 'type' then
local resolved = getClassAnyGenericMap(suri, node)
if resolved then
vm.getClassFields(suri, node, key, function (field, isMark)
pushResult(resolveFieldWithMap(field, resolved) or field, isMark)
end)
return
end
vm.getClassFields(suri, node, key, pushResult)
end
end)
Expand Down
23 changes: 23 additions & 0 deletions test/type_inference/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,29 @@ end
_, <?y?> = xpcall(x)
]]

-- A generic class used without arguments: the parameter has nothing to resolve to, and
-- keeping it as the field's type would leave a type no value can have.
TEST 'any' [[
---@class Storage<T>
---@field Value T

---@type Storage
local storage
local value = storage.Value
print(<?value?>)
]]

-- Arguments still win where they are given.
TEST 'string' [[
---@class Storage2<T>
---@field Value T

---@type Storage2<string>
local storage
local value = storage.Value
print(<?value?>)
]]

TEST 'A' [[
---@class A

Expand Down
Loading