lua-language-server icon indicating copy to clipboard operation
lua-language-server copied to clipboard

Overriding methods

Open Luke100000 opened this issue 1 year ago • 22 comments

It would be great if I can override fields, not just shadow them, to retain their signature and doc.

---@class (exact) A
local a = {}

---Test Comment
---@param test string Some argument
function a:Method(test) end


---@class (exact) B : A
local b = {}

function b:Method(test) end


---@type B
local test

test:Method("") -- Annotations are gone

The signature wont change for overridden methods, and thus I would like to avoid copy pasting the whole doc for each method. How would I resolve this efficiently?

Luke100000 avatar Mar 14 '24 10:03 Luke100000

We don't have a way to do this currently. You may be interested in writing a short plugin that would do that for you.

C3pa avatar Mar 14 '24 18:03 C3pa

I looked into plugins and I can e.g. replace ---@override with a ---@see parent.method, but I don't know how I can retrieve the parents function definition directly.

Luke100000 avatar Mar 15 '24 08:03 Luke100000

Oh, looks like the Wiki docs aren't up to date anymore. There is more info on plugins on the project's GitHub pages: https://luals.github.io/wiki/plugins/. I can't really help you from here, I haven't used plugin api.

C3pa avatar Mar 15 '24 22:03 C3pa

Alright thank you for the links, I made a plugin which injects ---@alias at definitions, and ---@type and ---@see at overrides. Only param/return comments disappear but otherwise seems to be working fine for me.

However I struggle to understand another concept, maybe you can help me here :)

---@class (exact) A
---@field ox number
local a = {}

function a:init()
    --This is fine
    self.ox = 0
end

---@class (exact) B : A
local b = {}

function b:init()
    --This is an injection and thus invalid
    self.ox = 0
end

---@type B
local x

--But this is fine again?
x.ox = 0

Why does it want to inject a new field into B in the B's init? How can I specify that its fine to inject when its already inherited?

Another viewpoint would be: I don't want inheritance, but composition. Right now I have A.ox AND B.ox, but in my case it doesn't matter who introduced that value, its now just ox.

Luke100000 avatar Mar 17 '24 09:03 Luke100000

In my opinion, inject field diagnostic isn't smart enough yet and has its false positives. I suggest opening an issue or looking if an issue about this was already reported.

C3pa avatar Mar 17 '24 12:03 C3pa

同求相关支持。

使用场景: 项目中有很多角色脚本通过外部的通知进行驱动(击中\玩家输入之类),每个脚本针对相同通知的响应逻辑都不同,因此创建了一个父类用来定义一共有多少个通知响应接口以及每个接口的参数类型,具体的实现就交由各个脚本。

现有问题: 角色重写父类函数时会识别为一个在局部新建的函数,并默认将所有参数类型全部设置为 any,导致父类函数定义的参数类型在重写的函数体内全部丢失。

image 这里写定义的时候还是可以正确识别的。

image 进入函数体内之后参数类型就丢失了。

期待效果: 希望子类在重写父类函数时,除非子类手动重新指定函数的参数类型或者增加新的参数,否则子类函数直接采用父类函数定义的参数类型。

NicameOurister avatar Aug 31 '24 08:08 NicameOurister

Alright thank you for the links, I made a plugin which injects ---@alias at definitions, and ---@type and ---@see at overrides. Only param/return comments disappear but otherwise seems to be working fine for me.

However I struggle to understand another concept, maybe you can help me here :)

---@class (exact) A
---@field ox number
local a = {}

function a:init()
    --This is fine
    self.ox = 0
end

---@class (exact) B : A
local b = {}

function b:init()
    --This is an injection and thus invalid
    self.ox = 0
end

---@type B
local x

--But this is fine again?
x.ox = 0

Why does it want to inject a new field into B in the B's init? How can I specify that its fine to inject when its already inherited?

Another viewpoint would be: I don't want inheritance, but composition. Right now I have A.ox AND B.ox, but in my case it doesn't matter who introduced that value, its now just ox.

Could you please consider sharing your plug-in? I'm stuck with then same issue.

Appologise if you've already done publishing it. I've tried and failed to find it.

NicameOurister avatar Aug 31 '24 09:08 NicameOurister

Could you please consider sharing your plug-in? I'm stuck with then same issue.

Appologise if you've already done publishing it. I've tried and failed to find it.

Here: https://pastebin.com/fmWLcjjw It works for me but I'm certain that there are edge cases which breaks it.

Annotate overridden functions with ---@override and it will output something like this:

---@class (exact) A
local a = {}

---Test Comment
---@param test string Some argument
---@return string @ Some return
---@alias def.A.Method fun(self: A, test : string) : string
function a:Method(test) return "" end


---@class (exact) B : A
local b = {}

---@see A.Method
---@type def.A.Method
function b:Method(test)
	return "hi"
end


---@type B
local test 

test:Method("") -- Annotations are there!

Luke100000 avatar Aug 31 '24 09:08 Luke100000

Thanks so much!

NicameOurister avatar Aug 31 '24 11:08 NicameOurister

已经有相同的issue了 https://github.com/LuaLS/lua-language-server/issues/2158

NicameOurister avatar Sep 02 '24 04:09 NicameOurister

Partially implemented in https://github.com/LuaLS/lua-language-server/commit/7c481f57c407f7e4d0b359a3cfbce66add99ec2f now, arguments are inferred.

Luke100000 avatar Sep 08 '24 07:09 Luke100000

Thanks for the work and the notification, but there's still some distance to what I need.

In my use case, I have lots of objs which are only generated at runtime but customized at coding stage. They're still not getting any auto completion inside their overridden function bodies.

For example, I need to define the logic of diferent character skills, but skill information is only fetched at runtime and it's the same with skill objs. Thus, the code will end up like this: image To make it more complicated, the definition of skill, generate_skill_objs, list, list.skill1.onBegin all belongs to different files.

Could someone please look into this? Can't be more grateful.

NicameOurister avatar Sep 11 '24 07:09 NicameOurister

If I understand the recent change correctly, it's for child class overriding parent class's method. In the code you provided, the list is table<string, skill>, which means list.skill1 is still skill class (not a child class). Therefore luals will think you are redefining the skill:onBegin() in the skill class, so it will instead throw a duplicate-set-field warning.

---@class skill
local skill = {}

---@param casterID integer
---@param targetID integer
function skill:onBegin(casterID, targetID) end

---@param casterID integer
---@param targetID integer
function skill:onEnd(casterID, targetID) end

---@return table<string, skill>
local generate_skill_objs = function ()
    return {}
end
local list = generate_skill_objs()

function list.skill1:onBegin(casterID, targetID) end  --< duplicate-set-field
function list.skill1:onEnd(casterID, targetID) end  --< duplicate-set-field

In order to make use of the recent change, you have to create new class for each of your skill, and extending the base skill class, like the following

-- same code to generate `list` as above

---@class skill1: skill
local skill1 = list.skill1
function skill1:onBegin(casterID, targetID)
    print(casterID, targetID)  --< integer, integer
end
function skill1:onEnd(casterID, targetID)
    print(casterID, targetID)  --< integer, integer
end

It can be interpreted as skill1 is implementing the skill interface 🤔 But as you can see, this requires an extra @class annotation for each of your skill, ~and you said the definitions belong to different files which makes things more complicated~, so it may not suit you need... 😇 @NicameOurister

edit: Seems it doesn't matter if all the definitions are in different files 🤔 Because right after using:

---@class skills1: skill
local skill1 = list.skill1
  • luals will ignore the original type in list.skill1 and cast it to the new skills1 type
  • and you can start overriding methods in the local skill1 class variable, while still getting the parent method's params' type

tomlau10 avatar Sep 11 '24 10:09 tomlau10

If I understand the recent change correctly, it's for child class overriding parent class's method. In the code you provided, the list is table<string, skill>, which means list.skill1 is still skill class (not a child class). Therefore luals will think you are redefining the skill:onBegin() in the skill class, so it will instead throw a duplicate-set-field warning.

---@class skill
local skill = {}

---@param casterID integer
---@param targetID integer
function skill:onBegin(casterID, targetID) end

---@param casterID integer
---@param targetID integer
function skill:onEnd(casterID, targetID) end

---@return table<string, skill>
local generate_skill_objs = function ()
    return {}
end
local list = generate_skill_objs()

function list.skill1:onBegin(casterID, targetID) end  --< duplicate-set-field
function list.skill1:onEnd(casterID, targetID) end  --< duplicate-set-field

In order to make use of the recent change, you have to create new class for each of your skill, and extending the base skill class, like the following

-- same code to generate `list` as above

---@class skill1: skill
local skill1 = list.skill1
function skill1:onBegin(casterID, targetID)
    print(casterID, targetID)  --< integer, integer
end
function skill1:onEnd(casterID, targetID)
    print(casterID, targetID)  --< integer, integer
end

It can be interpreted as skill1 is implementing the skill interface 🤔 But as you can see, this requires an extra @class annotation for each of your skill, ~and you said the definitions belong to different files which makes things more complicated~, so it may not suit you need... 😇 @NicameOurister

edit: Seems it doesn't matter if all the definitions are in different files 🤔 Because right after using:

---@class skills1: skill
local skill1 = list.skill1
  • luals will ignore the original type in list.skill1 and cast it to the new skills1 type
  • and you can start overriding methods in the local skill1 class variable, while still getting the parent method's params' type

Thanks for comment.

The recent change does cover part of my need, but not all of it. I know that I can do it this way. but there's still reason for the need of a more powerful implement.

In my case, there are tens of skill objs in every character's script, and the number of character script is more. It will be considerable lines of redundant code and makes it easier to cause name conflict.

Thus, I'm still hoping that we can just skip all those repeat annotations. Hopefully this enhancement could solve all the same related issues in #477, #1779 and #1757.

NicameOurister avatar Sep 11 '24 11:09 NicameOurister

btw your link for #1779 and #1757 are incorrect, they all points to #477 🙈

Actually you can just write #1779 or paste the link https://github.com/LuaLS/lua-language-server/issues/1779 when you write comment, and github will be smart enough to turns it into [#1779](https://github.com/LuaLS/lua-language-server/issues/1779), which can be verified in Preview tab. No need to write the url markdown in full form 😄

tomlau10 avatar Sep 11 '24 11:09 tomlau10

btw your link for #1779 and #1757 are incorrect, they all points to #477 🙈

Actually you can just write #1779 or paste the link https://github.com/LuaLS/lua-language-server/issues/1779 when you write comment, and github will be smart enough to turns it into [#1779](https://github.com/LuaLS/lua-language-server/issues/1779), which can be verified in Preview tab. No need to write the url markdown in full form 😄

Thank you!

NicameOurister avatar Sep 11 '24 11:09 NicameOurister

Strange, if I define the interface using @type fun() style, then when overriding it, params' types are there ‼️ 🤔 (although it will still trigger duplicate-set-field warnings)

---@class skill
local skill = {}

---@type fun(self: skill, casterID: integer, targetID: integer)
function skill:onBegin(casterID, targetID) end

---@type fun(self: skill, casterID: integer, targetID: integer)
function skill:onEnd(casterID, targetID) end

---@return table<string, skill>
local generate_skill_objs = function ()
    return {}
end
local list = generate_skill_objs()

function list.skill1:onBegin(casterID, targetID) end  --< integer, integer, but with `duplicate-set-field` warning
function list.skill1:onEnd(casterID, targetID) end  --< integer, integer, but with `duplicate-set-field` warning

tomlau10 avatar Sep 11 '24 12:09 tomlau10

I think I have found a way!! 😄 @NicameOurister

  • You have to define it using @field first, then all the methods will have params type infer && no duplicate-set-field warnings
  • But the downside is you can't add descriptions to your params, and you have to write the definition in 1 line ... (fun() syntax do not support multi line)
---@class skill
---@field onBegin fun(self: skill, casterID: integer, targetID: integer)
---@field onEnd fun(self: skill, casterID: integer, targetID: integer)
local skill = {}

function skill:onBegin(casterID, targetID)
    print(casterID, targetID)   -- integer, integer
    -- your default implementation
end

function skill:onEnd(casterID, targetID)
    print(casterID, targetID)   -- integer, integer
    -- your default implementation
end

---@return table<string, skill>
local generate_skill_objs = function ()
    return {}
end
local list = generate_skill_objs()

function list.skill1:onBegin(casterID, targetID)
    print(casterID, targetID)   -- integer, integer
    -- no duplicate-set-field
end
function list.skill2:onBegin(casterID, targetID)
    print(casterID, targetID)   -- integer, integer
    -- no duplicate-set-field
end

I'm still trying to find out why @param style won't work 🤔

tomlau10 avatar Sep 11 '24 12:09 tomlau10

Thanks for the solution!

It works as needed and does really solved the biggest problem, but it produces some little new problems. The most obvious problem is that the ---field definition will mask out the real code and becomes the only target of that function's definition jump.

I can totally accept those extra ---field definitions . But I'm still kind of bothered with the definition jump mask problem. After all, the whole purpose of mine is to make my code library as accessible as possible for others.

I'm going to settle with the current solution while keep looking for a better way. Thanks again for the help.

NicameOurister avatar Sep 12 '24 10:09 NicameOurister

Strange, if I define the interface using @type fun() style, then when overriding it, params' types are there ... I'm still trying to find out why @param style won't work

I think I have successfully made it working without breaking anything 🎉 (at least all existing tests passed).

Root cause

I believe the function param infer logic resides in here: https://github.com/LuaLS/lua-language-server/blob/b9639f6e135b9ab5340b5e64c64f3643a9618a74/script/vm/compiler.lua#L1110-L1124

  • The reason why @type fun() works but not @param, is because the former one is of doc.type.function type. Here the logic will only consider the arguments' node types in a doc.type.function (the fun() annotation)
  • If I change the if conditions in line 1124 to include function type, then it works! But with some side effects and breaking the tests... 🙁

Fixing side effects

  • I think it's because when a function is defined on its own, it will still have a function infer type. And this causes problems when trying to infer its own params' type by its own definition => loop => causing the types to be unknown instead of any
  • To fix this, I found out that it shall skip the infer if the argument source object is identical to self source:
    if (n.type == 'doc.type.function' or n.type == 'function')
    and n.args[aindex] and n.args[aindex] ~= source
    then
    
    note the n.args[aindex] ~= source added here => then all existing tests passed 🙂

Fixing duplicate set field

We still have to deal with the duplicate-set-field warning. The related logic is here: https://github.com/LuaLS/lua-language-server/blob/b9639f6e135b9ab5340b5e64c64f3643a9618a74/script/core/diagnostics/duplicate-set-field.lua#L54-L70

So we have to find a way to skip the warning of this overriding class function / method use case. The one that I can think of is that:

  • whenever the function definition (def in the above logic) is defined within a class namespace, we should allow overriding it (src in the above logic) in a non-class namespace.
    • i.e. if we mark ---@class A; local A = {}, here A has a class attached. If there are duplicated function A:f() end; function A:f() end => should throw warnings
    ---@class A
    local A = {}
    function A:f() end
    function A:f() end  -- should throw warnings
    
    • on the other hand, if it is just a type variable ---@type A; local a = {}, here a has no class attached. Then we should allow function a:f() end for overriding
    ---@class A
    local A = {}
    function A:f() end
    
    ---@type A
    local a = {}
    function a:f() end  -- allow this
    
  • So I up come with the following (add it below line 70)
            if vm.getDefinedClass(guide.getUri(def), def.node)
            and not vm.getDefinedClass(guide.getUri(src), src.node)
            then
                -- allow type variable to override function defined in class variable
                goto CONTINUE
            end

Conclusion

With the above proposed change, now we can have param type inference in your original annotation style, and without duplicate-set-field warnings 👀

---@class skill
local skill = {}

---@param casterID integer
---@param targetID integer
function skill:onBegin(casterID, targetID) end

---@type table<string, skill>
local list = {}

function list.skill1:onBegin(casterID, targetID)
    print(casterID, targetID)   -- integer, integer
    -- no `duplicate-set-field` as well
end

function list.skill2:onBegin(casterID, targetID)
    print(casterID, targetID)   -- integer, integer
    -- no `duplicate-set-field` as well
end

I will need some time to do a more in-depth test before I create a PR. Meanwhile you may try to patch this in your local copy of luals to see the actual effect 😄@NicameOurister

tomlau10 avatar Sep 14 '24 06:09 tomlau10

Roger that. Thanks for all these work.

NicameOurister avatar Sep 14 '24 07:09 NicameOurister

My related PR is merged recently, so this use case: https://github.com/LuaLS/lua-language-server/issues/2569#issuecomment-2342854384 will be supported in the next release 👀

tomlau10 avatar Sep 25 '24 07:09 tomlau10