MANDATORY for ALL Lua output - files AND conversational snippets. Covers local over global, LuaLS type annotations, StyLua formatting, module patterns, pcall error handling.
This skill should be triggered when:
local everywhere, avoid globalsPrimary tool for type checking and IDE support. Understands EmmyLua-style annotations.
# Install via Homebrew
brew install lua-language-server
# Or via Mason in Neovim
:MasonInstall lua-language-server
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"
Static analyzer and linter:
brew install luacheck
Configuration (.luacheckrc):
std = "lua51+luajit" -- or "lua54" for Lua 5.4
globals = {
"hs", -- Hammerspoon
"vim", -- Neovim
"love", -- LÖVE2D
}
ignore = {
"212", -- Unused argument (often intentional in callbacks)
}
max_line_length = 100
This is the most important Lua rule. Globals pollute the environment and are slower.
-- BAD - creates global
name = "Kevin"
function greet() end
-- GOOD - always use local
local name = "Kevin"
local function greet() end
luacheck catches accidental globals:
luacheck --globals hs vim -- myfile.lua
Use EmmyLua-style annotations for type safety:
---@type string
local name = "Kevin"
---@type number
local count = 0
---@type boolean
local enabled = true
---@type string[]
local names = { "Alice", "Bob" }
---@type table<string, number>
local scores = { alice = 100, bob = 95 }
---Calculate the area of a rectangle
---@param width number The width
---@param height number The height
---@return number area The calculated area
local function calculateArea(width, height)
return width * height
end
---@class User
---@field id string
---@field name string
---@field email string
---@field age number?
---@type User
local user = {
id = "123",
name = "Kevin",
email = "user@example.com",
}
---@alias LoadingState
---| "idle"
---| "loading"
---| "success"
---| "error"
---@type LoadingState
local state = "idle"
---@class IdleState
---@field status "idle"
---@class LoadingState
---@field status "loading"
---@class SuccessState
---@field status "success"
---@field data any
---@class ErrorState
---@field status "error"
---@field error string
---@alias FetchState IdleState | LoadingState | SuccessState | ErrorState
---@param state FetchState
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 T
---@param items T[]
---@return T?
local function first(items)
return items[1]
end
-- mymodule.lua
local M = {}
---@type string
M.VERSION = "1.0.0"
---Process data
---@param input string
---@return string
function M.process(input)
return input:upper()
end
-- Private function (not exported)
local function helper()
-- ...
end
return M
local mymodule = require("mymodule")
local result = mymodule.process("hello")
-- BAD - pollutes global namespace
MyModule = {}
function MyModule.doThing() end
-- GOOD - return module table
local M = {}
function M.doThing() end
return M
-- BAD - crashes on error
local data = json.decode(input)
-- GOOD - handle errors
local ok, data = pcall(json.decode, input)
if not ok then
print("Failed to parse JSON:", data) -- data is error message
return nil
end
return data
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
---@class Result
---@field ok boolean
---@field value any?
---@field error string?
---@param value any
---@return Result
local function Ok(value)
return { ok = true, value = value }
end
---@param error string
---@return Result
local function Err(error)
return { ok = false, error = error }
end
---@param input string
---@return Result
local function parseJson(input)
local ok, data = pcall(json.decode, input)
if ok then
return Ok(data)
else
return Err(data)
end
end
-- Usage
local result = parseJson('{"name": "Kevin"}')
if result.ok then
print(result.value.name)
else
print("Error:", result.error)
end
-- Array (sequential integer keys)
local fruits = { "apple", "banana", "cherry" }
print(#fruits) -- 3
-- Dictionary (string keys)
local user = {
name = "Kevin",
age = 30,
}
-- Mixed (avoid this)
local mixed = { "a", "b", key = "value" } -- confusing
-- Arrays: use ipairs
for i, fruit in ipairs(fruits) do
print(i, fruit)
end
-- Dictionaries: use pairs
for key, value in pairs(user) do
print(key, value)
end
-- NEVER use pairs on arrays (order not guaranteed)
-- Insert at end
table.insert(fruits, "date")
-- Insert at position
table.insert(fruits, 2, "blueberry")
-- Remove
table.remove(fruits, 1)
-- Sort
table.sort(fruits)
-- Concatenate
local str = table.concat(fruits, ", ")
---@class Counter
---@field count number
local Counter = {}
Counter.__index = Counter
---@return 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
-- Usage
local counter = Counter.new()
counter:increment()
print(counter:getValue()) -- 1
---@class Animal
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
---@class Dog : Animal
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 methods
local upper = str:upper()
local lower = str:lower()
local trimmed = str:match("^%s*(.-)%s*$") -- trim whitespace
local parts = {}
for part in str:gmatch("[^,]+") do
table.insert(parts, part)
end
-- string.format (printf-style)
local msg = string.format("Hello, %s! You have %d messages.", name, count)
-- Concatenation (simple cases only)
local greeting = "Hello, " .. name
-- init.lua
local M = {}
-- Use hs.loadSpoon for Spoons
hs.loadSpoon("ReloadConfiguration")
spoon.ReloadConfiguration:start()
-- Bind hotkeys
hs.hotkey.bind({ "cmd", "alt" }, "r", function()
hs.reload()
end)
-- Async tasks (non-blocking)
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
-- lua/plugins/init.lua
return {
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = { "lua", "typescript", "python" },
highlight = { enable = true },
})
end,
},
}
-- Options
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
-- Keymaps
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })
-- Autocommands
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.lua",
callback = function()
vim.lsp.buf.format()
end,
})
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
myplugin.nvim/
├── .luacheckrc
├── .stylua.toml
├── lua/
│ └── myplugin/
│ ├── init.lua
│ └── config.lua
├── plugin/
│ └── myplugin.lua # Auto-loaded by Neovim
└── README.md
~/.hammerspoon/
├── init.lua
├── .luacheckrc
├── .stylua.toml
├── Spoons/
│ └── MySpoon.spoon/
│ ├── init.lua
│ └── docs.json
└── lib/
└── utils.lua
| 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()) |
nil and false are falsy, everything else is truthy (including 0 and "")# operator only works reliably on arrays without holeslocal at the top of their scope