From 255e83b34c20fb28caa4fe0e5e86cfc915a14ecf Mon Sep 17 00:00:00 2001 From: ChouUn Date: Fri, 6 Mar 2026 00:17:00 +0800 Subject: [PATCH 1/3] feat: support type inference for @field and @type function declarations in method overrides When a child class overrides a parent class method that was declared using @field or @type annotations (instead of function declarations), the parameter types are now correctly inferred from the parent class. This extends the existing method override type inference (PR #2859) to support field-style function declarations. Example: ```lua ---@class Buff local mt = {} ---@type (fun(self: Buff, target: Buff): boolean)? mt.on_cover = nil ---@class Buff.CommandAura : Buff local tpl = {} function tpl:on_cover(target) -- target type is now correctly inferred as Buff (was any before) return self.level > target.level end ``` Changes: - Modified compileFunctionParam in script/vm/compiler.lua to extract function type from doc.field nodes by accessing their extends property - Added support for doc.type.function in parent class field lookup - Added comprehensive test cases for @field and @type method overrides Fixes #3367 Co-Authored-By: Claude Opus 4.6 --- script/vm/compiler.lua | 24 +++++++-- test/type_inference/field_override.lua | 68 ++++++++++++++++++++++++++ test/type_inference/init.lua | 1 + 3 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 test/type_inference/field_override.lua diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 5267a037b..df286c5e4 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1353,10 +1353,16 @@ local function compileFunctionParam(func, source) ]] local found = false for n in funcNode:eachObject() do - if (n.type == 'doc.type.function' or n.type == 'function') - and n.args[aindex] and n.args[aindex] ~= source + -- Extract actual function type node + local funcType = n + if n.type == 'doc.field' and n.extends then + funcType = n.extends + end + + if (funcType.type == 'doc.type.function' or funcType.type == 'function') + and funcType.args and funcType.args[aindex] and funcType.args[aindex] ~= source then - local argNode = vm.compileNode(n.args[aindex]) + local argNode = vm.compileNode(funcType.args[aindex]) for an in argNode:eachObject() do if an.type ~= 'doc.generic.name' then vm.setNode(source, an) @@ -1460,8 +1466,16 @@ local function compileFunctionParam(func, source) end vm.getClassFields(suri, extClass, key, function (field, _isMark) for n in vm.compileNode(field):eachObject() do - if n.type == 'function' and n.args[aindex] then - local argNode = vm.compileNode(n.args[aindex]) + -- Extract actual function type node + local funcType = n + if n.type == 'doc.field' and n.extends then + funcType = n.extends + end + + if (funcType.type == 'function' or funcType.type == 'doc.type.function') + and funcType.args and funcType.args[aindex] + then + local argNode = vm.compileNode(funcType.args[aindex]) for an in argNode:eachObject() do if an.type ~= 'doc.generic.name' then vm.setNode(source, an) diff --git a/test/type_inference/field_override.lua b/test/type_inference/field_override.lua new file mode 100644 index 000000000..71e5b1866 --- /dev/null +++ b/test/type_inference/field_override.lua @@ -0,0 +1,68 @@ + +-- Test @type field declaration with method override +TEST 'Buff' [[ +---@class Buff +local mt = {} +---@type (fun(self: Buff, target: Buff): boolean)? +mt.on_cover = nil + +---@class Buff.CommandAura : Buff +local tpl = {} +function tpl:on_cover() + return true +end +]] + +-- Test @field declaration with method override +TEST 'Animal' [[ +---@class Animal +---@field can_eat (fun(self: Animal, other: Animal): boolean)? +local base = {} + +---@class Dog : Animal +local dog = {} +function dog:can_eat() + return true +end +]] + +-- Test optional method with @type +TEST 'string' [[ +---@class Base +local base = {} +---@type (fun(self: Base, x: string): number)? +base.callback = nil + +---@class Child : Base +local child = {} +function child:callback() + return 1 +end +]] + +-- Test non-optional @field +TEST 'number' [[ +---@class Handler +---@field process fun(self: Handler, value: number): string +local handler = {} + +---@class CustomHandler : Handler +local custom = {} +function custom:process() + return tostring(value) +end +]] + +-- Test multiple parameters with @type +TEST 'string' [[ +---@class Processor +local proc = {} +---@type fun(self: Processor, a: number, b: string): boolean +proc.handle = nil + +---@class MyProcessor : Processor +local my = {} +function my:handle(a, ) + return true +end +]] diff --git a/test/type_inference/init.lua b/test/type_inference/init.lua index 4eaad03d2..b45a1a4dd 100644 --- a/test/type_inference/init.lua +++ b/test/type_inference/init.lua @@ -46,3 +46,4 @@ end require 'type_inference.common' require 'type_inference.param_match' +require 'type_inference.field_override' From 8ca15991d966270fe70c311455106a95e01caa73 Mon Sep 17 00:00:00 2001 From: ChouUn Date: Fri, 6 Mar 2026 00:30:01 +0800 Subject: [PATCH 2/3] fix: address code review feedback and add changelog entry - Remove unnecessary doc.field handling (vm.compileNode already handles it) - Keep doc.type.function support in parent class field lookup - Add changelog entry for the new feature --- changelog.md | 1 + script/vm/compiler.lua | 24 ++++++------------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/changelog.md b/changelog.md index 3b605d2cf..5dc7bfd6a 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## Unreleased +* `NEW` Support type inference for `@field` and `@type` function declarations in method overrides [#3367](https://github.com/LuaLS/lua-language-server/issues/3367) * `CHG` Modified the `ResolveRequire` function to pass the source URI as a third argument. ## 3.17.1 diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index df286c5e4..96363642a 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1353,16 +1353,10 @@ local function compileFunctionParam(func, source) ]] local found = false for n in funcNode:eachObject() do - -- Extract actual function type node - local funcType = n - if n.type == 'doc.field' and n.extends then - funcType = n.extends - end - - if (funcType.type == 'doc.type.function' or funcType.type == 'function') - and funcType.args and funcType.args[aindex] and funcType.args[aindex] ~= source + if (n.type == 'doc.type.function' or n.type == 'function') + and n.args and n.args[aindex] and n.args[aindex] ~= source then - local argNode = vm.compileNode(funcType.args[aindex]) + local argNode = vm.compileNode(n.args[aindex]) for an in argNode:eachObject() do if an.type ~= 'doc.generic.name' then vm.setNode(source, an) @@ -1466,16 +1460,10 @@ local function compileFunctionParam(func, source) end vm.getClassFields(suri, extClass, key, function (field, _isMark) for n in vm.compileNode(field):eachObject() do - -- Extract actual function type node - local funcType = n - if n.type == 'doc.field' and n.extends then - funcType = n.extends - end - - if (funcType.type == 'function' or funcType.type == 'doc.type.function') - and funcType.args and funcType.args[aindex] + if (n.type == 'function' or n.type == 'doc.type.function') + and n.args and n.args[aindex] then - local argNode = vm.compileNode(funcType.args[aindex]) + local argNode = vm.compileNode(n.args[aindex]) for an in argNode:eachObject() do if an.type ~= 'doc.generic.name' then vm.setNode(source, an) From 91e41aa98eaf7652e23cdca4fdf1542a997cdd29 Mon Sep 17 00:00:00 2001 From: ChouUn Date: Fri, 6 Mar 2026 00:46:36 +0800 Subject: [PATCH 3/3] fix: remove redundant n.args check n.args is guaranteed to exist for function/doc.type.function nodes, only n.args[aindex] needs to be checked. --- script/vm/compiler.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 96363642a..af9f81f2e 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1354,7 +1354,7 @@ local function compileFunctionParam(func, source) local found = false for n in funcNode:eachObject() do if (n.type == 'doc.type.function' or n.type == 'function') - and n.args and n.args[aindex] and n.args[aindex] ~= source + and n.args[aindex] and n.args[aindex] ~= source then local argNode = vm.compileNode(n.args[aindex]) for an in argNode:eachObject() do @@ -1461,7 +1461,7 @@ local function compileFunctionParam(func, source) vm.getClassFields(suri, extClass, key, function (field, _isMark) for n in vm.compileNode(field):eachObject() do if (n.type == 'function' or n.type == 'doc.type.function') - and n.args and n.args[aindex] + and n.args[aindex] then local argNode = vm.compileNode(n.args[aindex]) for an in argNode:eachObject() do