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

Best way to implement something like an abstract base class?

Open emmericp opened this issue 1 year ago • 3 comments

I'm working on a framework where users of it implement a kind of module that mixes their provided logic with some optional event handlers.

Here is a simple example

-- provided by framework
---@class Base
---@field OnClick fun(self: Base, x: number, y: number)
local base = {}

---@generic T: Base
---@param name `T`
---@return T
function New(name)
	return setmetatable({}, {__index = base})
end

-- usage of framework by user

---@class MyImpl: Base
local foo = New("MyImpl")

function foo:OnClick(x, y) -- x and y are inferred as any instead of number :(
end

function foo:myHelperFunction()
end

So I expect the user to implement OnClick and fill it with their logic. My goal here is that the user automatically gets the correct types on the parameters for foo:OnClick. Here in this simple example it's just two numbers, but my real app has like > 10 such methods and some of these take complex types as parameters.

With the code above it doesn't know anything about the parameters in OnClick and shows them as any. Maybe I got something wrong with the generics, but even without all the fancy generic stuff (just @return base on New) it's the same problem.

The only work-around I found was to not use @class on the user-defined class and not define a generic, this makes the parameters on the OnCLick function definition correct! However, it now triggers injected-field on all user-defined values not defined in the base class like the myHelperFunction method :(

emmericp avatar Dec 22 '23 18:12 emmericp

No you didn't get anything wrong afaik. I wish this was supported too.

However, it now triggers injected-field on all user-defined values not defined in the base class like the myHelperFunction method :(

You (or the downstream user) could always just manually ignore those errors I suppose (using ---@diagnostic), and I think those funcs/methods would still get applied to the derived class type.

Also, FWIW, there's this lol:

---@class MyImpl: Base
local foo = New("MyImpl")

;(--[[@as Base]]foo).OnClick = function(self, x, y)

end

function foo:myHelperFunction()
end

...but that would have to be done manually for each overridden method, and this LSP doesn't necessarily warn you or make it obvious when you are doing so (overriding).

Another hack that might work (in some sense?):

local foo = New {
    OnClick = function(self, x, y)

    end,

    myHelperFunction = function()

    end
}

but then users would be required to call New() like this every single time...

Hopefully this gets fixed or improved if/when the LSP is refactored (#2366).

tmillr avatar Jan 29 '24 22:01 tmillr

FWIW I implemented this as a plugin for our stuff that just injects a @class annotation together with a lot of @field annotations: https://github.com/DeadlyBossMods/LuaLS-Config/blob/main/DBM-Plugin.lua

emmericp avatar Jan 30 '24 22:01 emmericp

I'm looking through your solution, @emmericp and I can't wrap my head around what exactly you've done to accomplish this.

bavalpey avatar Feb 12 '24 16:02 bavalpey