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

Class generics produce unusable and wrong union types

Open vaderkos opened this issue 4 months ago • 1 comments

How are you using the lua-language-server?

Visual Studio Code Extension (sumneko.lua)

Which OS are you using?

MacOS, Windows

What is the issue affecting?

Completion

Expected Behaviour

Methods Peek,Push,Pop have proper type hints

Actual Behaviour

While types of a, b, c are correct the type hints of methods are unusable with unnecessary union types.

If Push is removed from superclass, then type hints go to normal without unions but still with <T> in many places

local _lua_setmetatable = setmetatable

Stack = {}

--- @generic T
--- @class Stack<T> : {
---     Pop: (fun(self: Stack<T>): T),
---     Peek: (fun(self: Stack<T>): T),
---     Push: (fun(self: Stack<T>, value: T): Stack<T>),
--- }
--- @field protected _stack any[]
--- @field protected _count integer
Stack.__index = {}

--- Creates a new, empty stack.
--- @generic T
--- @return Stack<T>
function Stack.New()
    local this = {
        _stack = {},
        _count = 0,
    }

    return _lua_setmetatable(this, Stack)
end






local st = Stack.New() --[[@as Stack<{ num: integer }>]]

local a = st:Peek()
local b = st:Pop()
local c = st:Push({})
Image

Reproduction steps

Analyze specified code in VSCode with luals installed

Additional Notes

No response

Log File

No response

vaderkos avatar Sep 01 '25 17:09 vaderkos

AFAIK, the generic support of LuaLS is very limited. 😇 There are many caveats, and the "best" annotation that I attempted to achieve your needs is as follow:

---@class Stack<T>: { __hidden: T }
---@field protected _stack any[]
---@field protected _count integer
Stack = {}
Stack.__index = Stack

---@return Stack
function Stack.New()
    local self = {
        _stack = {},
        _count = 0,
    }
    return setmetatable(self, Stack)
end

---@generic T
---@param self Stack<T>
---@return T
function Stack:Pop() return nil end

---@generic T
---@param self Stack<T>
---@return T
function Stack:Peek() return nil end

---@generic self, T
---@param self self|Stack<T>
---@param value T
---@return self
function Stack:Push(value) return self end

--- demo

---@type Stack<{ num: integer }>
local st = Stack.New()

local a = st:Peek() --> { num: integer }
local b = st:Pop() --> { num: integer }
local c = st:Push({}) --> Stack<{ num: integer }>
Image

caveat 1

A generic class must inherit a table with a field to store the generic type T ref: https://github.com/LuaLS/lua-language-server/issues/1532#issuecomment-2542346524 In my example you can see there is a { __hidden: T } there. If you remove it, then ---@param self Stack<T> annotation in the methods won't work => which will cause :Peek() and :Pop() annotation failing to work

caveat 2

A generic type in function annotation can only do type capturing but not type enforcing

  • i.e. the ---@param value T in Stack:Push actually does nothing
  • because it can only be used the match the type T in Stack<T>
  • but cannot be used the enforce value param to be of the captured type T
  • and therefore LuaLS cannot check against incorrect value type that you pass to :Push()

If you use generic types heavily, you may want to try CppCXY's new language server written in Rust. It has much better generic class support 👀

  • https://github.com/LuaLS/lua-language-server/issues/3017
  • https://github.com/LuaLS/lua-language-server/issues/3017#issuecomment-2876633965

tomlau10 avatar Sep 02 '25 09:09 tomlau10