| name | lua-standards |
| description | MANDATORY for ALL Lua output - files AND conversational snippets. Covers local over global, LuaLS type annotations, StyLua formatting, module patterns, pcall error handling. Trigger: any Lua code, Neovim config, Hammerspoon, LÖVE2D, game scripting. No exceptions. Use when this capability is needed. |
Lua Best Practices
When to Use This Skill
This skill should be triggered when:
- Writing or reviewing Lua code
- Configuring Neovim, Hammerspoon, or other Lua-scriptable tools
- Working with game engines (LÖVE2D, Defold, Roblox)
- Discussing Lua patterns and conventions
- Setting up Lua tooling or type checking
Core Capabilities
- Scoping:
local everywhere, avoid globals
- Type Safety: LuaLS annotations for IDE support and checking
- Code Quality: StyLua for formatting, luacheck for linting
- Error Handling: pcall/xpcall for recoverable errors
- Module Pattern: Clean exports, no global pollution
Tooling
LuaLS (Lua Language Server)
Primary tool for type checking and IDE support. Understands EmmyLua-style annotations.
brew install lua-language-server
:MasonInstall lua-language-server
StyLua
Modern Lua formatter (like prettier for Lua):
brew install stylua
Configuration (.stylua.toml):
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
luacheck
Static analyzer and linter:
brew install luacheck
Configuration (.luacheckrc):
std = "lua51+luajit"
globals = {
"hs",
"vim",
"love",
}
ignore = {
"212",
}
max_line_length = 100
local Over Global
This is the most important Lua rule. Globals pollute the environment and are slower.
name = "Kevin"
function greet() end
local name = "Kevin"
local function greet() end
Why This Matters
- Performance: Local variables are stored in registers, globals require table lookup
- Safety: Globals leak between modules and can cause subtle bugs
- Clarity: Explicit scope makes code easier to understand
Detecting Globals
luacheck catches accidental globals:
luacheck --globals hs vim -- myfile.lua
Type Annotations (LuaLS)
Use EmmyLua-style annotations for type safety:
Basic Types
local name = "Kevin"
local count = 0
local enabled = true
local names = { "Alice", "Bob" }
local scores = { alice = 100, bob = 95 }
Function Annotations
local function calculateArea(width, height)
return width * height
end
Class-like Tables
local user = {
id = "123",
name = "Kevin",
email = "user@example.com",
}
Union Types
local state = "idle"
Discriminated Unions (Tagged Tables)
local function render(state)
if state.status == "idle" then
showPlaceholder()
elseif state.status == "loading" then
showSpinner()
elseif state.status == "success" then
showData(state.data)
elseif state.status == "error" then
showError(state.error)
end
end
Generic Types
local function first(items)
return items[1]
end
Module Pattern
Standard Module Structure
local M = {}
M.VERSION = "1.0.0"
function M.process(input)
return input:upper()
end
local function helper()
end
return M
Usage
local mymodule = require("mymodule")
local result = mymodule.process("hello")
Avoid Global Exports
MyModule = {}
function MyModule.doThing() end
local M = {}
function M.doThing() end
return M
Error Handling
pcall for Recoverable Errors
local data = json.decode(input)
local ok, data = pcall(json.decode, input)
if not ok then
print("Failed to parse JSON:", data)
return nil
end
return data
xpcall with Stack Trace
local function errorHandler(err)
return debug.traceback(err, 2)
end
local ok, result = xpcall(function()
return riskyOperation()
end, errorHandler)
if not ok then
print("Error with trace:", result)
end
Result Pattern
local function Ok(value)
return { ok = true, value = value }
end
local function Err(error)
return { ok = false, error = error }
end
local function parseJson(input)
local ok, data = pcall(json.decode, input)
if ok then
return Ok(data)
else
return Err(data)
end
end
local result = parseJson('{"name": "Kevin"}')
if result.ok then
print(result.value.name)
else
print("Error:", result.error)
end
Tables
Array vs Dictionary
local fruits = { "apple", "banana", "cherry" }
print(#fruits)
local user = {
name = "Kevin",
age = 30,
}
local mixed = { "a", "b", key = "value" }
Iterate Correctly
for i, fruit in ipairs(fruits) do
print(i, fruit)
end
for key, value in pairs(user) do
print(key, value)
end
Table Operations
table.insert(fruits, "date")
table.insert(fruits, 2, "blueberry")
table.remove(fruits, 1)
table.sort(fruits)
local str = table.concat(fruits, ", ")
Metatables (OOP Pattern)
Simple Class
local Counter = {}
Counter.__index = Counter
function Counter.new()
local self = setmetatable({}, Counter)
self.count = 0
return self
end
function Counter:increment()
self.count = self.count + 1
end
function Counter:getValue()
return self.count
end
local counter = Counter.new()
counter:increment()
print(counter:getValue())
Inheritance
local Animal = {}
Animal.__index = Animal
function Animal.new(name)
local self = setmetatable({}, Animal)
self.name = name
return self
end
function Animal:speak()
error("Not implemented")
end
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
function Dog.new(name)
local self = setmetatable(Animal.new(name), Dog)
return self
end
function Dog:speak()
return self.name .. " says woof!"
end
String Handling
Prefer String Methods
local upper = str:upper()
local lower = str:lower()
local trimmed = str:match("^%s*(.-)%s*$")
local parts = {}
for part in str:gmatch("[^,]+") do
table.insert(parts, part)
end
String Formatting
local msg = string.format("Hello, %s! You have %d messages.", name, count)
local greeting = "Hello, " .. name
Hammerspoon Specific
local M = {}
hs.loadSpoon("ReloadConfiguration")
spoon.ReloadConfiguration:start()
hs.hotkey.bind({ "cmd", "alt" }, "r", function()
hs.reload()
end)
local task = hs.task.new("/usr/bin/curl", function(exitCode, stdout, stderr)
if exitCode == 0 then
print(stdout)
else
print("Error:", stderr)
end
end, { "-s", "https://api.example.com" })
task:start()
return M
Neovim Specific
return {
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = { "lua", "typescript", "python" },
highlight = { enable = true },
})
end,
},
}
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.lua",
callback = function()
vim.lsp.buf.format()
end,
})
Project Structure
Library/Module Project
mylib/
├── .luacheckrc
├── .stylua.toml
├── mylib/
│ ├── init.lua # Main module (returns M)
│ ├── utils.lua # Utility functions
│ └── types.lua # Type definitions
└── tests/
└── mylib_spec.lua # busted tests
Neovim Plugin
myplugin.nvim/
├── .luacheckrc
├── .stylua.toml
├── lua/
│ └── myplugin/
│ ├── init.lua
│ └── config.lua
├── plugin/
│ └── myplugin.lua # Auto-loaded by Neovim
└── README.md
Hammerspoon Config
~/.hammerspoon/
├── init.lua
├── .luacheckrc
├── .stylua.toml
├── Spoons/
│ └── MySpoon.spoon/
│ ├── init.lua
│ └── docs.json
└── lib/
└── utils.lua
Quick Reference
| Tool | Purpose | Command |
|---|
| LuaLS | Type checking + LSP | Built into editor |
| StyLua | Formatting | stylua . |
| luacheck | Linting | luacheck . |
| busted | Testing | busted |
| Pattern | Preference |
|---|
| Scoping | local always (never implicit global) |
| Modules | Return table, don't set globals |
| Iteration | ipairs for arrays, pairs for dicts |
| Errors | pcall/xpcall for recoverable errors |
| Types | LuaLS annotations for IDE support |
| OOP | Metatables with __index |
| Strings | Methods (:upper()) over functions (string.upper()) |
Notes
- Lua is 1-indexed (arrays start at 1, not 0)
nil and false are falsy, everything else is truthy (including 0 and "")
- Tables are the only data structure - use them for arrays, dicts, objects, modules
- No built-in class system - metatables provide OOP patterns
# operator only works reliably on arrays without holes
- Always declare variables
local at the top of their scope
Converted and distributed by TomeVault — claim your Tome and manage your conversions.