Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overriding methods #2569

Open
Luke100000 opened this issue Mar 14, 2024 · 27 comments
Open

Overriding methods #2569

Luke100000 opened this issue Mar 14, 2024 · 27 comments

Comments

@Luke100000
Copy link
Contributor

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
Copy link
Contributor

C3pa commented Mar 14, 2024

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
Copy link
Contributor Author

Luke100000 commented Mar 15, 2024

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
Copy link
Contributor

C3pa commented Mar 15, 2024

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
Copy link
Contributor Author

Luke100000 commented Mar 17, 2024

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
Copy link
Contributor

C3pa commented Mar 17, 2024

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
Copy link

NicameOurister commented Aug 31, 2024

同求相关支持。

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

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

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

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

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

@NicameOurister
Copy link

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
Copy link
Contributor Author

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
Copy link

Thanks so much!

@NicameOurister
Copy link

已经有相同的issue了
#2158

@Luke100000
Copy link
Contributor Author

Luke100000 commented Sep 8, 2024

Partially implemented in 7c481f5 now, arguments are inferred.

@NicameOurister
Copy link

NicameOurister commented Sep 11, 2024

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
Copy link
Contributor

tomlau10 commented Sep 11, 2024

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

@NicameOurister
Copy link

NicameOurister commented Sep 11, 2024

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

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
Copy link
Contributor

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 🤔

@NicameOurister
Copy link

NicameOurister commented Sep 12, 2024

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
Copy link
Contributor

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:

local function compileFunctionParam(func, source)
local aindex
for index, arg in ipairs(func.args) do
if arg == source then
aindex = index
break
end
end
---@cast aindex integer
-- local call ---@type fun(f: fun(x: number));call(function (x) end) --> x -> number
local funcNode = vm.compileNode(func)
local found = false
for n in funcNode:eachObject() do
if n.type == 'doc.type.function' and n.args[aindex] then

  • 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:

for _, def in ipairs(defs) do
if def == src then
goto CONTINUE
end
if def.type ~= 'setfield'
and def.type ~= 'setmethod'
and def.type ~= 'setindex' then
goto CONTINUE
end
local defTopBlock = getTopFunctionOfIf(def)
if uri == guide.getUri(def) and myTopBlock ~= defTopBlock then
goto CONTINUE
end
local defValue = vm.getObjectValue(def)
if not defValue or defValue.type ~= 'function' then
goto CONTINUE
end

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

@NicameOurister
Copy link

Roger that. Thanks for all these work.

@tomlau10
Copy link
Contributor

My related PR is merged recently, so this use case: #2569 (comment) will be supported in the next release 👀

@jakitliang
Copy link

I think it is no need to check duplicate-set-field since Lua is weak typed and dynamic Language.
If you wanna static features why not enjoy such like C# and TypeScript.
So I advice that it should remove the duplicate-set-field since Lua doesn't own that language level concept.

@tomlau10
Copy link
Contributor

duplicate-set-field diagnostic is useful in the sense that, in sometime we just accidentally used the same function name for a completely unrelated logic, instead of intentionally to do so.
Or maybe that after solving git merge conflict, two of the same named function exist in the same class
=> this usually hints a bug in the code, as in the same class, each method name should be unique.

For example:

---@class Player
local Player = {}

function Player:setName(name)
    self.name = name
end

function Player:setName(name)
    self.playerName = name
end
  • two :setName() are defined, this maybe due to wrong conflict resolution between 2 teammates
  • although this code is totally valid in lua (in which the later definition will just overwrite the previous one)
    this code makes no sense and leads to confusion: maybe when reading the codebase, one will think that :setName() will set to the field self.name, but actually now it will set to self.playerName instead.

This had helped me successfully found a bug in my team's project🤦‍♂️ where the method wrongly defined using another namespace (instead of the namespace used in that file)
=> this caused duplicate-set-field in that other namespace

local Parent = require "Parent"

-- this file should define methods of `Child` class
---@class Child: Parent
local Child = {}

function Child:f1() end
function Child:f2() end
function Parent:f3() end --> don't know why suddenly used wrong namespace here ...
-- and `Parent` does already have a `:f3()`
--> duplicate-set-field triggered
function Child:f4() end

@jakitliang
Copy link

jakitliang commented Jan 11, 2025

intentionally

If you mean "we just accidentally used the same function name for completely unrelated logic, instead of doing so intentionally," and think that’s important for Lua, then why don’t other linters, like Robocop and PEP8 linters, focus on this issue?
In my opinion, this kind of approach is only useful for specific scenarios, perhaps for beginners or for your own company’s logic, rather than being a common Lua practice.
It feels like gilding the lily.

And here are some additional comments:

Accidental function redefinition:
This is clearly a developer’s mistake, not an issue with the language or the linter itself. If a team encounters such issues, it should be a problem with the team’s code management, code review process, and development standards, not a universal code style issue. In many cases, redefining functions is a team-level management issue, and a linter should not enforce it as a rule.

Git merge conflict issues:
Git itself does not cause errors. Merge conflicts simply reflect issues in the team’s collaboration process (such as insufficient code review, merge strategies, etc.). Errors arising from Git merges are problems in the development workflow and code management, not something that should be addressed by a linter. Even when a merge conflict leads to a redefined function, the correct approach is to resolve it through code review, communication, and clear team conventions, not by letting a public linter enforce it.

Why it is inappropriate for a public linter to solve private issues:

Impact on development flexibility:
A public linter should follow general programming standards and should not interfere too much with specific project design decisions. For features like polymorphism, which are common in object-oriented programming, an overly strict check on function redefinition will limit the flexibility of developers.

Over-generalization of checks:
Applying a universal rule for specific situations, like Git merge conflicts, feels like an "over-generalization." These rules are more suited to address specific problems rather than common issues in the broader development ecosystem. Different teams may have very different approaches to code management and review, so a single tool should not try to address the unique problems of all teams.

Code review and process issues:
Issues with merge conflicts and duplicate code are more related to team culture and the development process. Using automated tools to enforce these checks may seem like it helps catch potential errors, but it doesn't fundamentally solve the communication and coding standard issues within teams. The correct approach is to resolve such issues through team code reviews, clear Git merge strategies, and conventions, reducing the chances of these problems from arising in the first place.

@tomlau10
Copy link
Contributor

LuaLS (the language server) is to do type inferencing and provide type hints / completions.
On top of that, it comes with a diagnostic framework / system, in which it can do a number of diagnostics: https://luals.github.io/wiki/diagnostics/

Now that each diagnostics can be turned on or off, if you think this diagnostic is useless, then you can just turn it off 😅
Like in my team's project, we turned off the followings:

    // ignore follow warning as code base is not fully annotated yet
    "diagnostics.neededFileStatus": {
        "need-check-nil": "None!",
        "param-type-mismatch": "None!",
        "assign-type-mismatch": "None!",
        "unbalanced-assignments": "None!",
        "cast-local-type": "None!",
        "invisible": "None!"
    },

This is clearly a developer’s mistake, not an issue with the language or the linter itself

I agree with you 👍 that every mistakes are made by human, not the language or the linter tool.
(given that the language and tools are almost bug free)
But hey, this is the reason why we need a linter.👀
It can catch possible bugs which are hard to catch by human eyes.
For example one of the luals diagnostics is undefined-field, this helped my team catch quite a number of bugs:
(during code review as well as existing bugs in codebase)

---@enum MyEnum
local MyEnum = {
    a = 1,
    b = 2,
}

print(MyEnum.c) --> warning: Undefined field `c`.

Even when a merge conflict leads to a redefined function, the correct approach is to resolve it through code review

Code reviewers are also human, they can certainly reduce the chance of a coding error being merged to upstream, but they cannot catch all coding errors 😂.
Linters are the same, they can only catch possible errors which they can detect, but not all other logic errors.

Why it is inappropriate for a public linter to solve private issues:

I don't quite understand this statement 😕 .
What do you mean by private issues?
Does coding mistake count as a private issue? Because any mistake is made by a single person?

and a linter should not enforce it as a rule.

LuaLS doesn't enforce duplicate-set-field check, it's just enabled by default.
You can always turn it off (along with other diagnostics that you think are not appropriate for you projects)


PS: I am not maintainer of this project, I am just a user and a contributor.
What I said cannot represent maintainers' opinions. 😄
And that LuaLS is awesome 😀

@jakitliang
Copy link

I agree with you 👍 that every mistakes are made by huma

Okay, good! Thats alright 👍

Robocop and Pep8 have millions of contributors and a large community that LuaLS can't match. I just give my opinion and hope it
goes stronger then.

Thank you for your recognition ❤.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants