Skip to content

Call method operator not resolved properly when using Hump's class.lua #3167

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

Closed
cprn opened this issue Apr 26, 2025 · 2 comments
Closed

Call method operator not resolved properly when using Hump's class.lua #3167

cprn opened this issue Apr 26, 2025 · 2 comments

Comments

@cprn
Copy link

cprn commented Apr 26, 2025

How are you using the lua-language-server?

NeoVim

Which OS are you using?

Linux

What is the issue affecting?

Completion

Expected Behaviour

When using class.lua from a seemingly popular Hump library for Love2D, LS should autocomplete class methods, e.g.:

-- player.lua
Player = class{}

function Player:init(x, y)
    self.x = x
    self.y = y
end

function Player:update(dt)
    -- [...]
end
-- main.lua
class = require('class')
require('player')

p = Player( -- it should autocomplete `Player:init()` params
p: -- ctrl+space after `:` should show `update(dt)`, etc.

Actual Behaviour

LS says Player is:

(global) Player: {
    init: function,
    update: function,
    x: any,
    y: any,
}

...but p is:

(global) p: unknown

My understanding is that p:update(dt) is just a syntactic sugar for p.update(self, dt), so p: should trigger LuaLS to look up methods from the table and the meta table. AFAIU class.lua does some magic with setmetatable() to make that work. The code itself works fine, so Lua interpreter doesn't have issues finding the methods in meta, just LS does.

Reproduction steps

  1. Go to https://github.com/vrld/hump/blob/master/class.lua - download raw file
  2. Put it in a fresh project
  3. Create sample class:
-- player.lua
Player = class{}

function Player:init(x, y)
    self.x = x
    self.y = y
end

function Player:update(dt)
    -- [...]
end
  1. Require both in main:
-- main.lua
class = require('class')
require('player')

p = Player(5, 10)
  1. Try autocompleting methods in main

Additional Notes

I'm new to Lua, and don't understand yet what class.lua does exactly, so it might be that (old) library's fault... especially because the same issue doesn't happen for push.lua (a resolution scaling library for Love2D), but push.lua doesn't try to extend Lua objects.

I wasted 14 hours trying to solve it with AI and all models I tried say it should work. I looked for similar issues, but I only found some about exact class, and IDK what that is yet - if it's the same thing, sorry.

Both, Hump's class.lua and push.lua, are used in Harvard CS50's gamedev courses.

Log File

log file on Gist

(it doesn't fit, because it loads Luv library and the entire Neovim's runtime - sorry for that 🙁 that's what the "initial" Neovim's config does, so many Neovim users will have exactly that)

@tomlau10
Copy link
Contributor

The code itself works fine, so Lua interpreter doesn't have issues finding the methods in meta, just LS does.

LuaLS is not a Lua interpreter, it is a static code analyzer to provide type inferencing. So it doesn't have the same magic with a Lua interpreter and it will not run your actual code. To leverage its full potential, you need to use type annotations: https://luals.github.io/wiki/annotations/

While LuaLS can do basic type inferencing without annotation, it cannot dynamically create the class and constructor for you. You will need a simple @class along with @overload to specify the Player class and its constructor:

  • player.lua
---@class Player
---@overload fun(x, y): Player
Player = class{}

function Player:init(x, y)
    self.x = x
    self.y = y
end

function Player:update(dt)
    -- [...]
end
  • main.lua
class = require 'class'
require 'player'

p = Player(5, 10) --> should now see `p: Player`
p: --> will suggest `init:(x, y)`, `update(dt)`

especially because the same issue doesn't happen for push.lua

  • LuaLS can analyze a simple library using its module table return. For example:
local mylib = {}

function mylib.a() end
function mylib.b() end

return mylib

=> In this case when you do local mylib = require "mylib"
=> LuaLS will know that mylib is a table with 2 functions: a() / b()

  • however for the class.lua you provided, it's returning some "magic" table
-- the module
return setmetatable({new = new, include = include, clone = clone},
	{__call = function(_,...) return new(...) end})

=> so not many analysis can be done on LuaLS side, especially you are using Player = class {} to create this Player class object
=> now without further annotations, Player is just a table with the defined methods but with no call operator
=> you can see Player.* completions, but after you do a p = Player(), the p will become unknown
(since no call operator annotation is specified in Player)


I looked for similar issues, but I only found some about exact class, and IDK what that is yet - if it's the same thing, sorry.

No, exact class is another (advanced) feature: https://luals.github.io/wiki/annotations/#class
Since you said you are new to Lua, I bet you are new to LuaLS as well, in the mean time I think you don't need to touch that 😄.

You may consider using the discussion page as well: https://github.com/LuaLS/lua-language-server/discussions, since issues are mainly for reporting bugs.

@cprn
Copy link
Author

cprn commented Apr 27, 2025

Thank you, very much! This explains everything. I've seen annotations on the wiki, and I was struggling to deal with it, but your answer made it crystal clear. 😀

And sorry for the ticket - closing. Discussions seem to be a fairly new addition to GitHub. I wasn't aware of them.

@cprn cprn closed this as completed Apr 27, 2025
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

2 participants