diff --git a/changelog.md b/changelog.md index f712d4813..05bf272f8 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ * `FIX` A `.gitignore` line containing only whitespace no longer ignores the entire workspace [#3458](https://github.com/LuaLS/lua-language-server/issues/3458) * `FIX` `need-check-nil` diagnostic is no longer reported on safe navigation access (e.g. `x?.field`, `f?.()`, `t?.[key]`), since the optional access itself already handles the nil check. Note that a non-safe access chained after a safe one (e.g. `x.upper()?.field`) still reports, because the safe access only protects its own result. +* `FIX` Fields accessed on a value typed as a constrained generic (e.g. `---@generic T: SomeClass` with `---@param self T`) are now looked up on the constraint, so existing fields no longer report `undefined-field` (unknown fields still do) [#2640](https://github.com/LuaLS/lua-language-server/discussions/2640) ## 3.19.1 `2026-08-14` diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index a648c196c..4eff68127 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -355,7 +355,8 @@ local function resolveGenericField(uri, classGlobal, field, signs) } end -local searchFieldSwitch = util.switch() +local searchFieldSwitch +searchFieldSwitch = util.switch() : case 'table' : call(function (_suri, source, key, pushResult) if type(key) == 'string' @@ -515,6 +516,19 @@ local searchFieldSwitch = util.switch() vm.getClassFields(suri, node, key, pushResult) end end) + : case 'doc.generic.name' + : call(function (suri, source, key, pushResult) + searchFieldByLocalID(source, key, pushResult) + searchFieldByGlobalID(suri, source, key, pushResult) + -- unresolved generic `T: SomeClass`: search fields of its constraint + local constraint = source.generic and source.generic.extends + if not constraint then + return + end + for n in vm.compileNode(constraint):eachObject() do + searchFieldSwitch(n.type, suri, n, key, pushResult) + end + end) : default(function (suri, source, key, pushResult) searchFieldByLocalID(source, key, pushResult) searchFieldByGlobalID(suri, source, key, pushResult) diff --git a/test/diagnostics/undefined-field.lua b/test/diagnostics/undefined-field.lua index 24232eb57..9ca4936f0 100644 --- a/test/diagnostics/undefined-field.lua +++ b/test/diagnostics/undefined-field.lua @@ -156,3 +156,44 @@ X = { print(X.) ]] + +TEST [[ +---@class SomeClass +---@field someExistingVar integer +local SomeClass = {} + +---@generic T: SomeClass +---@param self T +---@return T +function SomeClass:xyz() + print(self.someExistingVar) + print(self.) + return self +end +]] + +TEST [[ +---@class A +---@field a integer + +---@class B +---@field b integer + +---@generic T: A|B +---@param x T +local function f(x) + print(x.a, x.b, x.) +end +]] + +TEST [[ +---@class A +local A = {} + +-- self-referential constraint must not recurse forever +---@generic T: T +---@param self T +function A:f() + print(self.) +end +]]