| 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:
local Animal = {}
Animal.__index = Animal
function Animal:new()
self = setmetatable({}, self)
return self
end
function Animal:move()
print("Animal moves")
end
Key points:
- Set
__index to self for inheritance
- Use
setmetatable to create instances
- Return the instance from constructor
Method definition syntax:
-
function Class:method() - Instance method, receives self implicitly
- Called as:
instance:method() or instance.method(instance)
- Use for methods that need access to instance state
-
function Class.method() - Module function, static, does NOT receive self
- Called as:
Class.method() or instance.method() (both work, but no
self)
- Use for utility functions, constructors, or static helpers
Inheritance Pattern
Class setup (module-level):
local Parent = {}
Parent.__index = 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)
local instance = Parent.new(Parent, name)
instance.child_state = extra
return setmetatable(instance, Child)
end
Critical rules:
- Always pass parent class explicitly:
Parent.new(Parent, ...) not
Parent.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)
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 by local scope)
- ❌
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 _)
local MyClass = {}
MyClass.__index = MyClass
function MyClass:new()
return setmetatable({ counter = 0 }, self)
end
local MyClass = {}
MyClass.__index = MyClass
function MyClass:new()
return setmetatable({
_counter = 0
}, self)
end
function MyClass:__protected_method()
self._counter = self._counter + 1
end
local function format_value(val)
return tostring(val)
end
function Child:use_parent_state()
self:__protected_method()
end
Note: The @private annotation is NOT necessary for private class methods
- LuaLS infers privacy from the
_ prefix automatically
- Only use
@protected for protected methods (__*, luals limitation)
-
Document intent with LuaCATS - Use visibility annotations:
-
Regular 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? type for every field from MyOpts
@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:
function M.create_block(lines)
return {
start_line = 1,
end_line = #lines,
content = lines,
}
end
function M.create_block(lines)
local block = {
start_line = 1,
end_line = #lines,
content = lines,
}
return block
end
Appending to typed arrays
Use arr[#arr + 1] = value, not table.insert(arr, value), when arr has a
LuaCATS element type (string[], agentic.acp.Content[], a typed field, etc.).
table.insert's second arg is variadic any, so LuaLS skips
assign-type-mismatch (an Error in .luarc.json). The indexed-assignment form
is type-checked against the element type and catches wrong-type appends and
stale @return / @type annotations.
- RIGHT:
lines[#lines + 1] = value (flags a non-string pushed to string[])
- WRONG:
table.insert(lines, value) (push silently accepted)
table.insert stays fine for positional inserts (table.insert(t, i, v)) and
untyped scratch tables where no element type is declared.