askill
lua-standards

lua-standardsSafety 95Repository

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.

0 stars
1.2k downloads
Updated 2/7/2026

Package Files

Loading files...
SKILL.md

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

  1. Scoping: local everywhere, avoid globals
  2. Type Safety: LuaLS annotations for IDE support and checking
  3. Code Quality: StyLua for formatting, luacheck for linting
  4. Error Handling: pcall/xpcall for recoverable errors
  5. Module Pattern: Clean exports, no global pollution

Tooling

LuaLS (Lua Language Server)

Primary 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

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

local Over Global

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

Why This Matters

  1. Performance: Local variables are stored in registers, globals require table lookup
  2. Safety: Globals leak between modules and can cause subtle bugs
  3. 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

---@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 }

Function Annotations

---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-like Tables

---@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",
}

Union Types

---@alias LoadingState
---| "idle"
---| "loading"
---| "success"
---| "error"

---@type LoadingState
local state = "idle"

Discriminated Unions (Tagged Tables)

---@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 Types

---@generic T
---@param items T[]
---@return T?
local function first(items)
  return items[1]
end

Module Pattern

Standard Module Structure

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

Usage

local mymodule = require("mymodule")
local result = mymodule.process("hello")

Avoid Global Exports

-- BAD - pollutes global namespace
MyModule = {}
function MyModule.doThing() end

-- GOOD - return module table
local M = {}
function M.doThing() end
return M

Error Handling

pcall for Recoverable Errors

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

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

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

Tables

Array vs Dictionary

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

Iterate Correctly

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

Table Operations

-- 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, ", ")

Metatables (OOP Pattern)

Simple Class

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

Inheritance

---@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 Handling

Prefer String Methods

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

-- string.format (printf-style)
local msg = string.format("Hello, %s! You have %d messages.", name, count)

-- Concatenation (simple cases only)
local greeting = "Hello, " .. name

Hammerspoon Specific

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

Neovim Specific

-- 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,
})

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

ToolPurposeCommand
LuaLSType checking + LSPBuilt into editor
StyLuaFormattingstylua .
luacheckLintingluacheck .
bustedTestingbusted
PatternPreference
Scopinglocal always (never implicit global)
ModulesReturn table, don't set globals
Iterationipairs for arrays, pairs for dicts
Errorspcall/xpcall for recoverable errors
TypesLuaLS annotations for IDE support
OOPMetatables with __index
StringsMethods (: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

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

90/100Analyzed 2/23/2026

Comprehensive Lua best practices skill covering scoping, type annotations, formatting, error handling, and module patterns. Excellent when-to-use section, detailed tooling setup with config examples, and rich code patterns for Neovim, Hammerspoon, and game engines. Slightly penalized for mismatched tags (lists api/ci-cd but content is Lua-specific) and missing metadata icon.

95
90
95
90
90

Metadata

Licenseunknown
Version-
Updated2/7/2026
Publisherdungle-scrubs

Tags

apici-cdlinting