LuaLS / lua-language-server

A language server that offers Lua language support - programmed in Lua
https://luals.github.io
MIT License
3.28k stars 306 forks source link

Overriding methods #2569

Open Luke100000 opened 6 months ago

Luke100000 commented 6 months ago

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?

C3pa commented 6 months ago

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.

Luke100000 commented 6 months ago

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.

C3pa commented 6 months ago

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.

Luke100000 commented 6 months ago

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.

C3pa commented 6 months ago

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.

NicameOurister commented 3 weeks ago

同求相关支持。

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

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

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

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

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

NicameOurister commented 3 weeks ago

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.

Luke100000 commented 3 weeks ago

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!
NicameOurister commented 3 weeks ago

Thanks so much!

NicameOurister commented 3 weeks ago

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

Luke100000 commented 2 weeks ago

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

NicameOurister commented 2 weeks ago

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.

tomlau10 commented 2 weeks ago

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
NicameOurister commented 2 weeks ago

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.

tomlau10 commented 2 weeks ago

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 😄

NicameOurister commented 2 weeks ago

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!

tomlau10 commented 2 weeks ago

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 commented 2 weeks ago

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

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 🤔 
NicameOurister commented 1 week ago

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.

tomlau10 commented 1 week ago

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

Fixing side effects

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:

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

NicameOurister commented 1 week ago

Roger that. Thanks for all these work.

tomlau10 commented 11 hours ago

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 👀