Skip to content

Adding ---@class breaks auto completion #3327

Open

Description

@distantforest1

How are you using the lua-language-server?

Visual Studio Code Extension (sumneko.lua)

Which OS are you using?

Linux

What is the issue affecting?

Completion

Expected Behaviour

I'm running into an issue where adding the ---@class annotation to a local variable seems to break auto completion. It should appear correctly even if the ---@Class is affixed to a local variable.

Actual Behaviour

The autocomplete variables and functions are not appearing.

Reproduction steps

Here is a simple example of creating a new player class with the issue.

-- Here is a simple example of creating a new player class with the issue
-- Uncommenting the line below *breaks* the autocompletion for p.new()
-- ---@class err.Player
local Player = {}
Player.__index = Player

local function mixin_xy(o)
    o.x = 0
    o.y = 0

    o.set_position = function(self, x, y)
        self.x = x
        self.y = y
    end

    return o
end

function Player.new()
    local self = {}
    self.happy = true
    self = mixin_xy(self)

    return setmetatable(self, Player)
end

local p = Player.new()
-- `p:set_position(), p.x, p.y, p.happy` no longer showing in auto complete if `---@class err.Player` is uncommented

When ---@class err.Player is uncommented, and above local Player = {}:
Image

When ---@class err.Player is removed or commented:

Image

The above image is the expected behaviour. However, when ---@class err.Player is added above local Player = {} it no longer works.

Additional Notes

No response

Log File

No response

Activity

  1. tomlau10 commented on Jan 9, 2026

    @tomlau10
    Contributor

    improper use of @class

    When you use @class, only the fields directly injected into a variable marked by @class, or fields that are defined in method of this variable will be considered as fields of that class.

    In you example

    ---@class Player
    local Player = {}  --<< now this `Player` variable is marked with `@class`
    Player.__index = Player --<< so `__index` is a field of the class
    
    local function mixin_xy(o)
        -- `o` is just a variable with `Player` type, it is NOT marked with `@class`
        o.x = 0 -- so this `x` field is not injected to `Player`
        o.y = 0 -- so as `y`
    
        o.set_position = function(self, x, y)
            self.x = x
            self.y = y
        end
    
        return o
    end
    
    function Player.new()
        local self = {} -- `self` is just a `table`, not marked with `@class`
        self.happy = true -- thus `happy` is not injected to `Player`
        self = mixin_xy(self) -- no effect to `Player` class
    
        return setmetatable(self, Player) -- mark `self` as `Player` type
    end
    
    local p = Player.new() -- as you see, none of the code above has effect to `Player` class
    -- so only the `Player.new` and `Player.__index` is shown here

    how to use @class in your example

    If you want to use @class, one way is like this

    ---@class Player
    local Player = {}
    Player.__index = Player
    
    local function mixin_xy(o)
        ---@class Player
        local o = o -- need to mark variable `o` as `@class Player`
        o.x = 0 -- to let `x` be injected into `Player` class
        o.y = 0 -- so as `y`
    
        o.set_position = function(self, x, y)
            self.x = x
            self.y = y
        end
    
        return o
    end
    
    function Player.new()
        ---@class Player
        local self = {} -- need to mark `self` as `@class Player` too
        self.happy = true
        self = mixin_xy(self)
    
        return setmetatable(self, Player)
    end
    
    local p = Player.new() -- now this should work

    if your want mixin_xy for more general use

    you will have to define it as another class (interface)

    ---@class Player
    local Player = {}
    Player.__index = Player
    
    local function mixin_xy(o)
        ---@class MixinXY
        local o = o
        o.x = 0 -- now `x` is injected into `MixinXY`
        o.y = 0 -- so as `y`
    
        o.set_position = function(self, x, y)
            self.x = x
            self.y = y
        end
    
        return o -- actually dont need this line
    end
    
    function Player.new()
        -- we let `Player` class extend the `MixinXY` interface
        ---@class Player: MixinXY
        local self = {}
        self.happy = true
        mixin_xy(self) -- no need `self =` here, otherwise will pollute `self` to become `Player|MixinXY` type
    
        return setmetatable(self, Player)
    end
    
    local p = Player.new() -- now this should also work

    but...

    As you can see there is a strange line local o = o inside function mixin_xy 😇
    which is required because @class needs to be put above a variable definition line.
    I don't know if there are better/simpler ways...

  2. distantforest1 commented on Jan 9, 2026

    @distantforest1
    Author

    Hmmm it seems like it creates a lot of overhead for the developer to add ---@classes everywhere, mixin documentation and the actual mixin code itself in lua...

    Is there a ---@Class that is less strict and allows fields to be freely injected? At this point the developer will need to remember both the documentation version of the Mixin and the actual mixin code.

  3. tomlau10 commented on Jan 10, 2026

    @tomlau10
    Contributor

    (I am not maintainer but just a user/contributor, so the following are just based on my usage experience and knowledge)

    Is there a ---@Class that is less strict and allows fields to be freely injected?

    I don't think so and I agree that this maybe one of the limits of current LuaLS 😇

    • When you don't write any annotation, those types are anonymous
    • But since they just track all injected fields, it cannot be used for type checking
      (i.e. checking if you are accessing undefined fields / injecting unwanted fields)
      => because there is no actual class definition for it
    • If you want type checking, then server must have a way to know which injected fields are allowed / disallowed
      => this is the usage of @class annotation

    When using LuaLS you cannot dynamically create a class type, you have to use @class and define all its inheritance/mixins types.

    Here is how I do mixins in my projects

    • a wrapper function that copy mixins interface to base class
    ---@class util.copy
    local copy = {}
    
    -- shallow copy
    function copy.shallow(A, B, doNotReplace)
        for k, v in pairs(B) do
            if A[k] == nil or not doNotReplace then
                A[k] = v
            end
        end
        return A
    end
    
    -- extend, shallow copy with mulitple args and will not replace if exist
    function copy.extend(A, ...)
        for _, B in ipairs({...}) do
            copy.shallow(A, B, true)
        end
        return A
    end
    
    return copy
    • then write the class and mixins in separate namespace
    -- MixinXY.lua
    ---@class MixinXY
    local MixinXY = {}
    function MixinXY:initPosition()
        self.x = 0
        self.y = 0
    end
    function MixinXY:setPosition(x, y)
        self.x = x
        self.y = y
    end
    
    -- Player.lua
    ---@class Player: MixinXY # write the inheritance here
    local Player = {}
    copy.extend(Player, MixinXY) -- which match with the `extend` statement
    Player.__index = Player
    
    function Player.new()
        ---@class Player # here just mark `self` as class, no need write inheritance again
        local self = setmetatable({}, Player) -- setmetatable at the beginning
        self.happy = true
        self:initPosition() -- such that we can call methods right away
        return self
    end
    
    local p = Player.new() -- now this should also work

    for inheriting a parent class and with multiple mixins

    Note that @class can be written multiple times.
    So if your Player class have another parent class, you may write like this:

    ---@class Player: Character
    ---@class Player: Loc, Buffs
    ---@class Player: QuestInfo, TeamMember
    local Player = {}
    copy.extend(Player, Character,
        Loc,
        Buffs,
        QuestInfo,
        TeamMember
    )
    Player.__index = Player

    => Pc inherits Character class, and then mixin with a number of interfaces

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions