diff --git a/changelog.md b/changelog.md index c9375a4cd..43a0e296f 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## Unreleased +* `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'` diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 5a673e5fa..81cf73a0d 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -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`). 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? +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 ---@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 @@ -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) @@ -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) diff --git a/test/type_inference/common.lua b/test/type_inference/common.lua index bd2de1ea5..566a3a7a5 100644 --- a/test/type_inference/common.lua +++ b/test/type_inference/common.lua @@ -320,6 +320,29 @@ end _, = 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 +---@field Value T + +---@type Storage +local storage +local value = storage.Value +print() +]] + +-- Arguments still win where they are given. +TEST 'string' [[ +---@class Storage2 +---@field Value T + +---@type Storage2 +local storage +local value = storage.Value +print() +]] + TEST 'A' [[ ---@class A