agentic-lua-class

star 514

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.

carlos-algms By carlos-algms schedule Updated 6/7/2026

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 __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

--- @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, ...) 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)  -- 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 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 _)
    -- ❌ 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()
    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:

    --- @class MyClass
    --- @field public_field string Public API
    --- @field __protected_field table For subclasses
    --- @field _private_field number Internal only
    
  • 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:

-- 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
Install via CLI
npx skills add https://github.com/carlos-algms/agentic.nvim --skill agentic-lua-class
Repository Details
star Stars 514
call_split Forks 47
navigation Branch main
article Path SKILL.md
More from Creator
carlos-algms
carlos-algms Explore all skills →