name: agentic-lua-class description: > MANDATORY before writing or editing ANY .lua file in this repo - classes, methods, fields, functions, or LuaCATS annotations. Holds the project's enforced Lua style: class pattern, visibility prefixes (_private, __protected), and optional-type syntax. Skipping it produces luals/selene failures at make validate. Load it before the first edit, not after.
Lua Class Pattern
Basic class structure:
--- @class Animal
local Animal = {}
Animal.__index = Animal
function Animal:new()
self = setmetatable({}, self)
return self
end
function Animal:move()
print("Animal moves")
end
Key points:
- Set
__indextoselffor inheritance - Use
setmetatableto create instances - Return the instance from constructor
Method definition syntax:
function Class:method()- Instance method, receivesselfimplicitly- Called as:
instance:method()orinstance.method(instance) - Use for methods that need access to instance state
- Called as:
function Class.method()- Module function, static, does NOT receiveself- Called as:
Class.method()orinstance.method()(both work, but noself) - Use for utility functions, constructors, or static helpers
- Called as:
Inheritance Pattern
Class setup (module-level):
local Parent = {}
Parent.__index = Parent
--- @class Child : Parent
local Child = setmetatable({}, { __index = Parent })
Child.__index = Child
Constructor with parent initialization:
function Parent:new(name)
local instance = {
name = name,
parent_state = {}
}
return setmetatable(instance, self)
end
function Child:new(name, extra)
-- Call parent constructor with Parent class
local instance = Parent.new(Parent, name)
-- Add child-specific state
instance.child_state = extra
-- Re-metatable to child class for proper inheritance chain
return setmetatable(instance, Child)
end
Critical rules:
- Always pass parent class explicitly:
Parent.new(Parent, ...)notParent.new(self, ...) - Re-assign metatable to child class after parent initialization
- Inheritance chain:
instance → Child → Parent
Calling parent methods:
function Child:move()
Parent.move(self) -- Explicit parent method call
print("Child-specific movement")
end
Class Design Guidelines: creating and modifying
Minimize class properties - Only include properties that:
- Are accessed by external code (other modules/classes)
- Are part of the public API
- Need to be accessed by subclasses
Use visibility prefixes for encapsulation - Control what external code can access:
Visibility levels (configured in
.luarc.json):_*: Private - Hidden from external consumers (applies to class methods/fields ONLY)__*: Protected - Visible to subclasses- No prefix: Public - Visible everywhere
IMPORTANT: Module-level local functions and variables do NOT need
_prefix:- ✅
local function helper()- correct (already private bylocalscope) - ❌
local function _helper()- incorrect (redundant_prefix) - ✅
local config = {}- correct - ❌
local _config = {}- incorrect (redundant_prefix) - ✅
function MyClass:_private_method()- correct (class method needs_) - ✅
@field _private_field- correct (class field needs_)
-- ❌ Bad: Unnecessary public exposure of `counter` property, not used externally --- @class MyClass --- @field counter number local MyClass = {} MyClass.__index = MyClass function MyClass:new() return setmetatable({ counter = 0 }, self) end -- ✅ Good: Proper visibility control --- @class MyClass local MyClass = {} MyClass.__index = MyClass function MyClass:new() return setmetatable({ -- Counter is internal state, not exposed publicly _counter = 0 }, self) end --- @protected function MyClass:__protected_method() self._counter = self._counter + 1 end --- Module-level helper functions (no underscore prefix needed) local function format_value(val) return tostring(val) end --- @class Child : MyClass function Child:use_parent_state() self:__protected_method() endNote: The
@privateannotation is NOT necessary for private class methods- LuaLS infers privacy from the
_prefix automatically - Only use
@protectedfor protected methods (__*, luals limitation)
Document intent with LuaCATS - Use visibility annotations:
--- @class MyClass --- @field public_field string Public API --- @field __protected_field table For subclasses --- @field _private_field number Internal onlyRegular cleanup - When adding new code, review class definitions and remove:
- Unused properties
- Properties that were needed during development but are no longer used
- Properties that could be local variables instead
LuaCATS annotation syntax
Space after --- for descriptions and annotations. Do NOT write param/return
descriptions unless requested. Group related annotations together.
Return format
@return {type} return_name description (type first, then name).
- RIGHT:
@return boolean success Whether the operation succeeded - WRONG:
@return boolean Whether the operation succeeded(missing name) - WRONG:
@return success boolean(wrong order)
Optional types
Format depends on annotation type. See LuaLS issue #2385 for the underlying validator limitation.
@param and fun() - MUST use type|nil:
- RIGHT:
@param winid number|nil - RIGHT:
@param callback fun(result: table|nil) - WRONG:
@param winid? number(LuaLS does not validate optional syntax) - WRONG:
fun(result?: table)(optional syntax ignored)
@field - Use variable? type:
- RIGHT:
@field _state? string - RIGHT:
@field diff? { all?: boolean }(inline tables also use?) - WRONG:
@field _state string|nil(use?here instead) - WRONG:
@field _state string?(?goes after variable name, not type)
For a partial variant of an existing class, use @class (partial) extending the
source type instead of re-declaring every field as optional.
- RIGHT:
@class (partial) MyOptsOverride: MyOpts - WRONG: re-listing
@field field? typefor every field fromMyOpts
@return, @type, @alias - Use explicit type|nil:
- RIGHT:
@return string|nil result,@type table<string, number|nil>,@alias MyType string|nil - WRONG: trailing
?on the type (e.g.string?,number?)
Typed variables before return
LuaLS cannot infer types from inline returns of complex types. Use a typed intermediate variable:
-- Bad: LuaLS cannot infer the return type
function M.create_block(lines)
return {
start_line = 1,
end_line = #lines,
content = lines,
}
end
-- Good: Type annotation enables proper type checking
--- @return MyModule.Block block
function M.create_block(lines)
--- @type MyModule.Block
local block = {
start_line = 1,
end_line = #lines,
content = lines,
}
return block
end