LUACHECK(1)

πŸ‘‘ LUACHECK(1) β€” the luacheck manual, bestie edition πŸ‘‘

okay so hi πŸ’– you opened the manual page for luacheck, and ngl this is the whole documentation, all of it, in one long beautiful scroll. no gatekeeping here. we're gonna read every single section together and i'm gonna be right next to you the whole time 🫢

this is part one of eleven, so don't panic when it just… starts. it's giving "table of contents energy" and that's on purpose. strap in ✨


πŸ’– NAME πŸ’–

luacheck β€” luacheck Documentation

Contents:


that's the whole vibe of this first bit, fr fr β€” the name, the docs, and a Contents situation that's about to unfold into everything πŸ¦‹

no skips, no "tl;dr", no summarising the boring parts away β€” we're reading the whole thing because every flag and every line matters, iykyk πŸ’…

bestie heads up: this is only part 1 of 11, so nothing is ending here. keep scrolling, we're just getting started πŸ”₯

okay bestie, this is the part where we go through every single warning code Luacheck can throw at you πŸ’… like, all of them, no skipping, we're doing this together 🫢

✨ how the codes work ✨

So warnings that Luacheck produces are sorted into categories using three-digit warning codes. Those codes can be shown in your CLI output if you turn on the --codes CLI option, or if you set the codes config option. iykyk.

Errors also have codes starting with zero β€” but here's the tea: unlike warnings, they can not be ignored. Nope. Not happening. Periodt. πŸ’€


πŸŽ€ the codes, one by one πŸŽ€

  • 011 β€” A syntax error. (yeah, this one hurts 😭)
  • 021 β€” An invalid inline option.
  • 022 β€” An unpaired inline push directive.
  • 023 β€” An unpaired inline pop directive.
  • 033 β€” Invalid use of a compound operator. (Lua doesn't support compound operator by default; if using an extension that does, please set the operators option.)
  • 111 β€” Setting an undefined global variable.
  • 112 β€” Mutating an undefined global variable.
  • 113 β€” Accessing an undefined global variable.
  • 121 β€” Setting a read-only global variable.
  • 122 β€” Setting a read-only field of a global variable.
  • 131 β€” Unused implicitly defined global variable.
  • 142 β€” Setting an undefined field of a global variable.
  • 143 β€” Accessing an undefined field of a global variable.
  • 211 β€” Unused local variable.
  • 212 β€” Unused argument.
  • 213 β€” Unused loop variable.
  • 214 β€” Used variable.
  • 221 β€” Local variable is accessed but never set.
  • 231 β€” Local variable is set but never accessed.
  • 232 β€” An argument is set but never accessed.
  • 233 β€” Loop variable is set but never accessed.
  • 241 β€” Local variable is mutated but never accessed.
  • 311 β€” Value assigned to a local variable is unused.
  • 312 β€” Value of an argument is unused.
  • 313 β€” Value of a loop variable is unused.
  • 314 β€” Value of a field in a table literal is unused.
  • 321 β€” Accessing uninitialized local variable.
  • 331 β€” Value assigned to a local variable is mutated but never accessed.
  • 341 β€” Mutating uninitialized local variable.
  • 411 β€” Redefining a local variable.
  • 412 β€” Redefining an argument.
  • 413 β€” Redefining a loop variable.
  • 421 β€” Shadowing a local variable.
  • 422 β€” Shadowing an argument.
  • 423 β€” Shadowing a loop variable.
  • 431 β€” Shadowing an upvalue.
  • 432 β€” Shadowing an upvalue argument.
  • 433 β€” Shadowing an upvalue loop variable.
  • 511 β€” Unreachable code.
  • 512 β€” Loop can be executed at most once.
  • 521 β€” Unused label.
  • 531 β€” Left-hand side of an assignment is too short.
  • 532 β€” Left-hand side of an assignment is too long.
  • 541 β€” An empty do end block.
  • 542 β€” An empty if branch.
  • 551 β€” An empty statement.
  • 561 β€” Cyclomatic complexity of a function is too high.
  • 571 β€” A numeric for loop goes from #(expr) down to 1 or less without negative step.

and yes bestie, that list is a whole mood 🌈 β€” we literally went through every single one, ate and left no crumbs πŸ‘‘

  • 581 β€” Negation of a relational operator; the operator can be flipped.
  • 582 β€” Error prone negation: negation has a higher priority than equality.
  • 611 β€” A line consists of nothing but whitespace.
  • 612 β€” A line contains trailing whitespace.
  • 613 β€” Trailing whitespace in a string.
  • 614 β€” Trailing whitespace in a comment.
  • 621 β€” Inconsistent indentation (SPACE followed by TAB).
  • 631 β€” Line is too long.

✨ Global variables (1xx) ✨

okay so for every single file, Luacheck builds up a list of the globals and fields that are allowed to show up in there. By default the only globals it knows about are the ones from the Lua standard library πŸ’…. Wanna add your own? Use the --globals CLI option or the globals config option. Wanna pick which version of the standard library it's vibing with? That's the --std CLI option or the std config option, bestie. And the moment an undefined global or field gets set, mutated, or accessed β€” boom, Luacheck hits you with a warning. 🫢

πŸ’– Read-only globals πŸ’–

By default, most standard globals and fields are marked as read-only, which means the second you try to set one you get a warning. Not me being overprotective, it's just how it is 😭. If you want your own read-only globals and fields, add them with the --read-globals CLI option or the read_globals config option, or just roll a custom set of globals. See Custom sets of globals.

Globals and fields that are NOT read-only by default:

  • _G
  • _ENV (treated as a global by Luacheck)
  • package.path
  • package.cpath
  • package.loaded
  • package.preload
  • package.loaders
  • package.searchers

🌸 Implicitly defined globals 🌸

Luacheck can be told to consider globals that get assigned under certain conditions as being defined implicitly, iykyk. Two flavors:

  • With the -d/--allow_defined CLI option or the allow_defined config option, all assignments to globals define them.
  • With the -t/--allow_defined_top CLI option or the allow_defined_top config option, assignments to globals in the top level function scope (aka the main chunk, bestie) define them.

And if an implicitly defined global never gets accessed anywhere? Warning. Periodt. ✨

πŸ¦‹ Modules πŸ¦‹

You can mark files as modules with the -m/--module CLI option or the module config option, which simulates the semantics of the deprecated module function. πŸ‘‘ Globals that get implicitly defined inside a module count as part of its interface β€” they're not visible outside and they don't get reported as unused. Assignments to other globals though? Not allowed. Not even to defined ones. 🚫


πŸ’œ Unused variables (2xx) and values (3xx) πŸ’œ

Luacheck drops warnings for every unused local variable… except one that's literally named _. That one gets a pass, always. πŸ’… It also catches variables that are set but never accessed, and variables that are accessed but never set.

⭐ "Unused hint" (214) ⭐

If a function argument starts with an underscore _, it receives an "unused hint" β€” meaning it's meant to be left unused, no drama. But if you actually use it? That's a 214 warning, bestie. πŸ™ˆ

🌈 Unused values and uninitialized variables 🌈

For every value that gets assigned to a local variable, Luacheck computes the set of expressions where that value could be used. Then:

  • unused values β†’ warning when a value can't be used anywhere
  • uninitialized variables β†’ warning when no values can reach an expression

Like, look at this snippet: the value assigned to foo on line 1 is unused, and variable bar is uninitialized on line 9. 😭

local foo = expr1()
local bar

if condition() then
   foo = expr2()
   bar = expr3()
else
   foo = expr4()
   print(bar)
end

return foo, bar

πŸŽ€ Secondary values and variables πŸŽ€

An unused value assigned to a local variable is secondary if its origin is the last item on the RHS of an assignment, and another value from that same item is used. Secondary values usually pop up when a function call's result gets dumped into locals and only some of them are used later.

In this example, the value assigned to b is secondary, the value assigned to c is used, and the value assigned to a is just… unused, plain and simple. πŸ’€

local a, b, c = f(), g()

return c

A variable is secondary if all values assigned to it are secondary. So in that snippet above, b is a secondary variable.

Wanna yeet warnings about unused secondary values and variables? Use the -s/--no-unused-secondaries CLI option or the unused_secondaries config option. 🫢


πŸ‘‘ Shadowing declarations (4xx) πŸ‘‘

Luacheck catches declarations of local variables that shadow previous declarations β€” unless the variable is named _, because _ is untouchable and iconic. If the previous declaration lives in the same scope as the new one, that's called redefining.

Also, real talk: you do NOT need to declare a whole new local variable just to overwrite an argument. Look:

local function f(x)
   local x = x or "default" -- bad
end

local function f(x)
   x = x or "default" -- good
end

See the difference? One's a crime, one's a slay. πŸ’…


πŸ”₯ Control flow and data flow issues (5xx) πŸ”₯

πŸ’€ Unreachable code

Luacheck detects unreachable code. It also detects it when the end of a loop block is unreachable β€” which basically means the loop can only ever run once. Not me exposing your loop like that:

for i = 1, 100 do
   -- Break statement is outside the `if` block,
   -- so that the loop always stops after the first iteration.
   if cond(i) then f() end break
end

🎯 Unused labels

Labels that no goto statement ever uses? Reported as unused. Simple as that. ✨

βš–οΈ Unbalanced assignments

If an assignment has a left side and a right side with different lengths, it's unbalanced and Luacheck will say something about it.

The one exception: initializing several local variables in a single statement while leaving some uninitialized. Like this:

local a, b, c = nil -- Effectively sets `a`, `b`, and `c` to nil, no warning.

No warning there, bestie. You're good. 🫢

🫧 Empty blocks

Luacheck warns about empty do end blocks and empty if branches (then else, then elseif, and then end).

πŸ“ Empty statements

In Lua 5.2+ semicolons count as statements and can show up even when they're not following normal statements. Those semicolons get flagged, because they are completely useless. Like, why are you even here? πŸ’€

πŸ“ˆ Cyclomatic complexity

If a limit is set with the --max-cyclomatic-complexity CLI option or the corresponding config or inline options, Luacheck warns about functions whose cyclomatic complexity is too high. πŸ“ˆ

πŸ”„ Reversed numeric for loops

Iterating a table in reverse with a numeric for loop going from #t to 1 requires a negative loop step. So Luacheck warns about loops going from #(some expression) to 1 (or a smaller constant) when the loop step isn't negative:

-- Warning for this loop:
-- numeric for loop goes from #(expr) down to 1 but loop step is not negative
for i = #t, 1 do
   print(t[i])
end

-- This loop is okay.
for i = #t, 1, -1 do
   print(t[i])
end

🚨 Error-prone and Unnecessary Negations

Negation has a higher priority than relational operators, so (not x == 3) gets read as (not x) == 3, rather than not (x == 3). Sneaky, right? 😭

And negating the output of a relational operator is just unnecessary β€” every single one has another operator you can use directly:

not (x == y) => x ~= y
not (x ~= y) => x == y
not (x > y) => x <= y
not (x >= y) => x < y
not (x < y) => x >= y
not (x <= y) => x > y
Heads up bestie: these replacements work for all numbers, but they can fail with metatables or NaN's. ⚠️

🌈 Formatting issues (6xx) 🌈

🌸 Whitespace issues

Luacheck warns about trailing whitespace and inconsistent indentation (SPACE followed by TAB).

Some examples of trailing whitespace Luacheck finds:

-- Whitespace example.
print("Hello")

print("World")

Here:

  • Any tabs or spaces after either ) would be considered trailing.
  • Any tabs or spaces after the . in the comment would be considered trailing
  • Any tabs or spaces on the empty line between the two print statements would also be considered a form of trailing whitespace.

Trailing whitespace in any of these forms is useless, can be a nuisance to developers navigating around a file, and is forbidden in many formatting styles. 🚫✨

πŸ“ Line length limits

Luacheck warns about lines that are longer than some limit. The default limit is 120 characters. You can change it with the --max-line-length CLI option, or disable the check completely with --no-max-line-length; there are similar config and inline options too. πŸŽ€

Also, you can set separate limits for three different types of lines:

  • "String" lines have their line ending inside a string, typically a long string using [[...]] syntax.
  • "Comment" lines have their line ending inside a long comment (--[[...]]), or end with a short comment using normal --... syntax.
  • "Code" lines are all other lines.

These line types are limited using CLI options named --[no-]max-string-line-length, --[no-]max-comment-line-length, and --[no-]max-code-line-length, with similar config and inline options. πŸ’…


πŸ’– COMMAND LINE INTERFACE πŸ’–

The luacheck program accepts files, directories and rockspecs as arguments, bestie. And you can filter them with the --include-files and --exclude-files options β€” more on those below. πŸ‘‘

  • Given a file, luacheck will check it. βœ…
  • Given -, luacheck will check stdin. 🫢
  • Given a directory, luacheck will check all files within it, selecting only files with .lua extension unless the --include-files option is used.
psst β€” that directory feature requires LuaFileSystem (installed automatically if LuaRocks was used to install Luacheck). πŸ¦‹
  • And listen, if you feed it a rockspec (a file with a .rockspec extension, periodt πŸ’…), luacheck will check all files with .lua extension mentioned in the rockspec in build.install.lua, build.install.bin and build.modules tables. Iconic.

The output is separate reports for each checked file, and it ends with a lil summary, like this:

$ luacheck src
Checking src/bad_code.lua                         5 warnings

    src/bad_code.lua:3:16: unused variable helper
    src/bad_code.lua:3:23: unused variable length argument
    src/bad_code.lua:7:10: setting non-standard global variable embrace
    src/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7
    src/bad_code.lua:9:11: accessing undefined variable hepler

Checking src/good_code.lua                        OK
Checking src/python_code.lua                      1 error

    src/python_code.lua:1:6: expected '=' near '__future__'

Checking src/unused_code.lua                      9 warnings

    src/unused_code.lua:3:18: unused argument baz
    src/unused_code.lua:4:8: unused loop variable i
    src/unused_code.lua:5:13: unused variable q
    src/unused_code.lua:7:11: unused loop variable a
    src/unused_code.lua:7:14: unused loop variable b
    src/unused_code.lua:7:17: unused loop variable c
    src/unused_code.lua:13:7: value assigned to variable x is unused
    src/unused_code.lua:14:1: value assigned to variable x is unused
    src/unused_code.lua:22:1: value assigned to variable z is unused

Total: 14 warnings / 1 error in 4 files

Look at that glow-up. ✨ OK files get called out, the messy ones get receipts, and everything gets a total at the bottom.

Now, how does luacheck pick its exit code? Bestie, it's a whole little grading scale:

  • Exit code is 0 if no warnings or errors occurred. flawless, no notes 🫢
  • Exit code is 1 if some warnings occurred but there were no syntax errors or invalid inline options.
  • Exit code is 2 if there were some syntax errors or invalid inline options.
  • Exit code is 3 if some files couldn't be checked, typically due to an incorrect file name.
  • Exit code is 4 if there was a critical error (invalid CLI arguments, config, or cache file). the whole vibe is off, run 😭

πŸŽ€ Command line options πŸŽ€

Okay so short options that don't take an argument? You can absolutely stack them like friendship bracelets β€” -qqu is literally equivalent to -q -q -u. Slay. And for long options, both --option value or --option=value work, so do whatever feels right.

Options that take several arguments can also be used several times, because why not β€” --ignore foo --ignore bar is equivalent to --ignore foo bar. ✨

⚠️ Real talk though: options that may take several arguments, like --globals, should not be used immediately before positional arguments. Because if you write --globals foo bar file.lua, luacheck will treat all of foo, bar and file.lua as globals and then panic as there are no file names left. Not the serve you wanted. πŸ’€

  • -g, --no-global β€” filter out warnings related to global variables.
  • -u, --no-unused β€” filter out warnings related to unused variables and values.
  • -r, --no-redefined β€” filter out warnings related to redefined variables.
  • -a, --no-unused-args β€” filter out warnings related to unused arguments and loop variables.
  • -s, --no-unused-secondaries β€” filter out warnings related to unused variables set together with used ones.
    • See Secondary values and variables.
  • --no-self β€” filter out warnings related to implicit self argument.
  • --std <std> β€” set standard globals, default is max. <std> can be one of:
    • max β€” union of globals of Lua 5.1 - 5.4 and LuaJIT 2.x;
    • min β€” intersection of globals of Lua 5.1 - 5.4 and LuaJIT 2.x;
    • lua51 β€” globals of Lua 5.1 without deprecated ones;
    • lua51c β€” globals of Lua 5.1;
    • lua52 β€” globals of Lua 5.2;
    • lua52c β€” globals of Lua 5.2 compiled with LUA_COMPAT_ALL;
    • lua53 β€” globals of Lua 5.3;
    • lua53c β€” globals of Lua 5.3 compiled with LUA_COMPAT_5_2;
    • lua54 β€” globals of Lua 5.4;
    • lua54c β€” globals of Lua 5.4 compiled with LUA_COMPAT_5_3;
    • luajit β€” globals of LuaJIT 2.x;
    • ngx_lua β€” globals of Openresty lua-nginx-module 0.10.10, including standard LuaJIT 2.x globals;
    • love β€” globals added by LΓ–VE;
    • busted β€” globals added by Busted 2.0, by default added for files ending with _spec.lua within spec, test, and tests subdirectories;
    • rockspec β€” globals allowed in rockspecs, by default added for files ending with .rockspec;
    • luacheckrc β€” globals allowed in Luacheck configs, by default added for files ending with .luacheckrc;
    • ldoc β€” globals allowed in LDoc config, by default added for files named config.ld;
    • sile β€” globals allowed in The SILE Typesetter and its package ecosystem;
    • none β€” no standard globals.

See Sets of standard globals


  • --globals [<name>] ... β€” Add custom global variables or fields on top of the standard ones. Peep Sets of standard globals and Defining extra globals and fields for the full deets.
  • --read-globals [<name>] ... β€” Add read-only global variables or fields.
  • --new-globals [<name>] ... β€” Set custom global variables or fields. Heads up: this removes custom globals you added previously.
  • --new-read-globals [<name>] ... β€” Set read-only global variables or fields. Also removes read-only globals added previously.
  • --not-globals [<name>] ... β€” Remove custom <em>and</em> standard global variables or fields. Bye bestie πŸ‘‹
  • -c, --compat β€” Literally just the same as --std max. No cap.
  • -d, --allow-defined β€” Allow defining globals implicitly by setting them. See Implicitly defined globals for the tea.
  • -t, --allow-defined-top β€” Allow defining globals implicitly by setting them in the top level scope. See Implicitly defined globals.
  • -m, --module β€” Limit the visibility of implicitly defined globals to their files. See Modules.
  • --max-line-length <length> β€” Set the maximum allowed line length. Default is 120.
  • --no-max-line-length β€” Do not limit line length. Live your truth, long lines and all πŸ’…
  • --max-code-line-length <length> β€” Set the maximum allowed length for lines ending with code. Default is 120.
  • --no-max-code-line-length β€” Do not limit code line length. Code can be as long as it wants ✨
  • --max-string-line-length <length> β€” Set the maximum allowed length for lines within a string. Default is 120.
  • --no-max-string-line-length β€” Do not limit string line length. String lines can go off πŸ’€
  • --max-comment-line-length <length> β€” Set the maximum allowed length for comment lines. Default is 120.
  • --no-max-comment-line-length β€” Do not limit comment line length. Comment away, bestie πŸŽ€
  • --max-cyclomatic-complexity <limit> β€” Set the maximum cyclomatic complexity for functions. Keep it cute and simple, y'know?
  • --no-max-cyclomatic-complexity β€” Do not limit function cyclomatic complexity. This is the default.
  • --ignore, -i <patt> [<patt>] ... β€” Filter out warnings matching the patterns. Like a block list for warnings 🚫
  • --enable, -e <patt> [<patt>] ... β€” Do <em>not</em> filter out warnings matching the patterns. It's giving unblocked πŸ”“
  • --only, -o <patt> [<patt>] ... β€” Filter out warnings <em>not</em> matching the patterns. Only the chosen ones get through πŸ‘‘
  • --operators <patt> [<patt>] ... β€” Allow compound operators matching the patterns. Note: multiple assignment is not supported, because this is specifically for the Playdate SDK.
  • --config <config> β€” Path to a custom configuration file. Default is .luacheckrc.
  • --no-config β€” Do not look up a custom configuration file. No config, no drama πŸ’…
  • --default-config <config> β€” Default path to a custom configuration file. This gets used if --[no-]config is not used and .luacheckrc is not found. The default global location is:
    • %LOCALAPPDATA%\Luacheck\.luacheckrc on Windows;
    • ~/Library/Application Support/Luacheck/.luacheckrc on OS X/macOS;
  • $XDG_CONFIG_HOME/luacheck/.luacheckrc or
    • ~/.config/luacheck/.luacheckrc on other systems.
  • --no-default-config β€” pls don't fall back to a default configuration file. we're not doing that today. 🚫
  • --filename <filename> β€” use a different name for the file in the output, bestie. this is what gets used for picking config overrides and for file filtering. lowkey important.
  • --exclude-files <glob> [<glob>] ... β€” do not check files that match these globbing patterns. recursive globs like **/*.lua are fully supported, no cap. ✨
  • --include-files <glob> [<glob>] ... β€” do not check files that don't match these globbing patterns. (yes it's the mirror twin of the one above πŸ’…)
  • --cache [<cache>] β€” path to the cache file. (default: .luacheckcache). head to Caching for the tea.
  • --no-cache β€” no cache. zero. none. bye. πŸ™ˆ
  • -j | --jobs β€” check <jobs> files in parallel. requires LuaLanes. default number of jobs = however many processing units you've got. multiprocess slay. πŸ”₯
  • --formatter <formatter> β€” use a custom formatter. <formatter> has to be a module name, or one of these icons:
    • TAP β€” Test Anything Protocol formatter;
    • JUnit β€” JUnit XML formatter;
    • visual_studio β€” MSBuild/Visual Studio aware formatter;
    • plain β€” simple warning-per-line formatter;
    • default β€” standard formatter.
  • -q | --quiet β€” suppress report output for files with no warnings. and if you stack it:
    • -qq β€” suppress output of warnings.
    • -qqq β€” only output summary. that's it, that's the whole vibe.
  • --codes β€” show warning codes.
  • --ranges β€” show ranges of columns related to warnings.
  • --no-color β€” do not colorize output. (tragic, but okay 😭)
  • -v | --version β€” show version of Luacheck and its dependencies, then exit.
  • -h | --help β€” show help and exit.

🌈 Patterns

okay so this is the good part. the CLI options --ignore, --enable and --only (plus their corresponding config options) let you filter warnings using pattern matching on warning codes, variable names, or both. here's the ruleset, bestie:

if a pattern contains a slash, the part before the slash matches the warning code and the part after matches the variable name. otherwise, if a pattern contains a letter or an underscore, it matches the variable name. otherwise, it matches the warning code. iykyk. for example:

  • 4.2 β€” shadowing declarations of arguments or redefining them.
  • .*_ β€” warnings related to variables with _ suffix.
  • 4.2/.*_ β€” shadowing declarations of arguments with _ suffix or redefining them.

unless they're already anchored, patterns matching variable names get anchored at both sides, and patterns matching warning codes get anchored at their beginnings. this is the little trick that lets you filter warnings by category β€” like, --only 1 focuses luacheck on global-related warnings. πŸ‘‘


🌸 Defining extra globals and fields

the CLI options --globals, --new-globals, --read-globals, --new-read-globals, and their corresponding config options, add new allowed globals or fields. for example, --read-globals foo --globals foo.bar lets you access the foo global and mutate its bar field. --not-globals also operates on globals and fields, and it removes definitions of both standard and custom globals. gone. πŸ’€


πŸ¦‹ Sets of standard globals

the CLI option --stds lets you combine built-in sets described above using +. for example, --std max is equivalent to:

--std=lua51c+lua52c+lua53c+luajit

a leading plus sign adds new sets to the current one instead of replacing it. for instance, --std +love is perfect for checking files using the LΓ–VE framework. πŸ’– and if you want your own custom sets of globals, you define them by mutating the global variable stds in config. see Custom sets of globals.


✨ Formatters

the CLI option --formatter lets you select a custom formatter for luacheck output. a custom formatter is a Lua module that returns a function with three arguments:

  1. report as returned by luacheck module (see Report format),
  2. array of file names,
  3. table of options.

those options contain the values assigned to the quiet, color, limit, codes, ranges and formatter options in CLI or config. the formatter function must return a string. that's the deal. 🫢


πŸŽ€ Caching

if LuaFileSystem is available, Luacheck can cache the results of checking files. on subsequent checks, only the files that have changed since the last check get rechecked, which improves run time significantly. iconic behaviour honestly.

heads up though: changing options (like defining additional globals) does not invalidate the cache. πŸ™ƒ

you can turn caching on with the --cache <cache> option or the cache config option. using --cache with no argument, or setting the cache config option to true, sets .luacheckcache as the cache file. and note this: --cache must be used every time luacheck is run, not just on the first run only. don't forget it, bestie.


πŸ‘‘ Stable interface for editor plugins and tools

real talk: the command-line interface of Luacheck can change between minor releases. but starting from version 0.11.0, the following interface is guaranteed at least till version 1.0.0, and it should be used by tools that consume Luacheck output β€” e.g. editor plugins. 🌈

  • Luacheck should be started from the directory containing the checked file.
  • The file can be passed through stdin using - as argument, or using a temporary file. the real filename should be passed using the --filename option.
  • The plain formatter should be used. it outputs one issue (warning or error) per line.
  • Wanna know exactly where the problem is? bestie, use the --ranges option πŸ’…. Every line kicks off with the real filename (that's the one you passed in with --filename), and then right after it comes :<line>:<start_column>-<end_column>: β€” where <line> is the line number where the issue popped off, and <start_column>-<end_column> is the inclusive range of columns of the token that the issue is about. Counting starts from 1, no zero-index shenanigans here ✨. And if you don't use --ranges? Then the end column and the dash just don't get printed. Periodt.
  • Want the codes too? That's --codes 🌈. On each line, the little substring hanging out between parentheses holds a three digit issue code, prefixed with E for errors and W for warnings. And if there's no such substring at all? That's a fatal error (think I/O error energy 😭), not a normal warning.
  • The rest of the line after all that? That's just the warning message, bestie. Simple as πŸ“.

If you're out here wanting compatibility with an older Luacheck version, you can peek at the output of luacheck --help to grab its version πŸ‘‘. If it contains a string like 0.<minor>.<patch>, where <minor> is at least 11 and <patch> is any number, then the interface we just described is the one you should be using. Iykyk πŸ’–.

πŸŽ€ CONFIGURATION FILE πŸŽ€

Okay so here's the tea β˜•: luacheck tries to load its config from a .luacheckrc file sitting in the current directory. Not there? Then it goes looking in the parent directory, and then that parent, and so on, climbing up and up until it hits the file system root πŸ¦‹. It's giving main character determination fr fr.

Wanna point it somewhere specific? Use the --config option, and in that case it'll be used during recursive loading too. Important vibe: paths within the config are interpreted relatively to the directory the config was loaded from.

You can also just shut the whole thing down with the --no-config flag πŸ™ˆ.

Now β€” if you use none of --config, --no-config, and --no-default-config, then luacheck will try to load config from the value of the --default-config option. And if you didn't set that either, it falls back to these defaults per system 🌸:

  • %LOCALAPPDATA%\Luacheck\.luacheckrc on Windows
  • ~/Library/Application Support/Luacheck/.luacheckrc on OS X/macOS
  • $XDG_CONFIG_HOME/luacheck/.luacheckrc or ~/.config/luacheck/.luacheckrc on other systems
Paths within the default config are interpreted relatively to the current directory. (Yes, different from the --config case, don't @ me πŸ’€)

So what is a config, actually? It's simply a Lua script that luacheck executes 🧚. It can set all kinds of options by assigning to globals, or by returning a table that has option names as keys. Either way slay ✨.

And here's the priority hierarchy, listen up: options loaded from config have the lowest priority πŸ”₯. So you can totally overwrite them with CLI options or inline options. Config is the understudy, not the star πŸ’œ.

πŸŽ€ Config options πŸŽ€

okay bestie, gather round, because this is the full menu of config options ✨ β€” every single one, the type it's expecting, and the value it quietly falls back to when you don't say anything. no skipping, we're doing all 33, it's giving completionist. πŸ’…

  • quiet β€” wants an Integer in range 0..3. ghost it and it defaults to 0.
  • color β€” Boolean. defaults to true. 🌈
  • codes β€” Boolean. defaults to false.
  • ranges β€” Boolean. defaults to false.
  • formatter β€” String or function. defaults to "default".
  • cache β€” Boolean or string. defaults to false.
  • jobs β€” Positive integer. defaults to 1.
  • exclude_files β€” Array of strings. defaults to {}.
  • include_files β€” Array of strings. default is (Include all files).
  • global β€” Boolean. defaults to true.
  • unused β€” Boolean. defaults to true.
  • redefined β€” Boolean. defaults to true.
  • unused_args β€” Boolean. defaults to true.
  • unused_secondaries β€” Boolean. defaults to true.
  • self β€” Boolean. defaults to true.
  • std β€” String or set of standard globals. defaults to "max".
  • globals β€” Array of strings or field definition map. defaults to {}.
  • new_globals β€” Array of strings or field definition map. default is (Do not overwrite).
  • read_globals β€” Array of strings or field definition map. defaults to {}.
  • new_read_globals β€” Array of strings or field definition map. default is (Do not overwrite).
  • not_globals β€” Array of strings. defaults to {}.
  • operators β€” Array of strings. defaults to {}.
  • compat β€” Boolean. defaults to false.
  • allow_defined β€” Boolean. defaults to false.
  • allow_defined_top β€” Boolean. defaults to false.
  • module β€” Boolean. defaults to false.
  • max_line_length β€” Number or false. defaults to 120.
  • max_code_line_length β€” Number or false. defaults to 120.
  • max_string_line_length β€” Number or false. defaults to 120.
  • max_comment_line_length β€” Number or false. defaults to 120.
  • max_cyclomatic_complexity β€” Number or false. defaults to false.
  • ignore β€” Array of patterns (see Patterns πŸ‘€). defaults to {}.
  • enable β€” Array of patterns. defaults to {}.
  • only β€” Array of patterns. default is (Do not filter).

so here's a lil example of a config that makes luacheck ensure that only globals from the portable intersection of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0 are used, and also disables detection of unused arguments. iconic combo tbh 🫢

std = "min"
ignore = {"212"}

πŸ¦‹ Custom sets of globals πŸ¦‹

the std option lets you set a custom standard set of globals using a table. this table can have two fields: globals and read_globals. both of them should contain a field definition map defining some globals. and the simplest way to define globals? just list their names, periodt:

std = {
   globals = {"foo", "bar"}, -- these globals can be set and accessed.
   read_globals = {"baz", "quux"} -- these globals can only be accessed.
}

see the difference bestie? globals get to be set and accessed, read_globals can only be accessed. that's the whole vibe check right there πŸ’œ

For globals defined like this, Luacheck is also gonna lowkey count any fields inside them as defined. But bestie, if you want a global with a restricted set of fields? You use the global name as the key and a table as the value. Inside that table, the fields subtable holds the fields in that exact same format:

std = {
   read_globals = {
      foo = { -- Defining read-only global `foo`...
         fields = {
            field1 = { -- `foo.field1` is now defined...
               fields = {
                  nested_field = {} -- `foo.field1.nested_field` is now defined...
               }
            },
            field2 = {} -- `foo.field2` is defined.
         }
      }
   }
}

Globals and fields can be marked read-only (or not πŸ’…) using the read_only property with a boolean value. And the other_fields property controls whether the global or field can also contain other unspecified fields:

std = {
   read_globals = {
      foo = { -- `foo` and its fields are read-only by default (because they are within `read_globals` table).
         fields = {
            bar = {
               read_only = false, -- `foo.bar` is not read-only, can be set.
               other_fields = true, -- `foo.bar[anything]` is defined and can be set or mutated (inherited from `foo.bar`).
               fields = {
                  baz = {read_only = true}, -- `foo.bar.baz` is read-only as an exception.
               }
            }
         }
      }
   }
}

Custom sets can get names too, fr fr β€” you do it by mutating the global stds variable, so then they're usable in the --std CLI option and the std inline and config option:

stds.some_lib = {...}
std = "min+some_lib"

And in config, globals, new_globals, read_globals, and new_read_globals can also hold definitions in that same format:

read_globals = {
   server = {
      fields = {
         -- Allow mutating `server.sessions` with any keys...
         sessions = {read_only = false, other_fields = true},
         -- other fields...
      }
   },
   --- other globals...
}

🌸 Per-file and per-path overrides 🌸

Okay so the environment where Luacheck loads the config has a special global called files. When it's checking a file <path>, Luacheck will override options from the main config with entries from files[<glob>] if <glob> matches <path> β€” and it applies entries for the more general globs first. ✨

Example time: this config re-enables detection of unused arguments only for files in src/dir, but not for files ending with _special.lua:

std = "min"
ignore = {"212"}
files["src/dir"] = {enable = {"212"}}
files["src/dir/**/*_special.lua"] = {ignore = {"212"}}
Heads up bestie: the files table supports autovivification, so these two are literally equivalent:
files["src/dir"].enable = {"212"}
files["src/dir"] = {enable = {"212"}}

The configs get processed in order of increasing specificity, k? globals and read_globals will add to the set of allowed globals. not_globals is your girl for removing previously allowed globals. And new_globals and new_read_globals override the whole set of globals β€” wiping all previously allowed globals or read_globals respectively, and replacing them with the new entries. Slay. πŸ’…


πŸ¦‹ Default per-path std overrides πŸ¦‹

Luacheck ships with a set of default per-path overrides, no cap:

files["**/spec/**/*_spec.lua"].std = "+busted"
files["**/test/**/*_spec.lua"].std = "+busted"
files["**/tests/**/*_spec.lua"].std = "+busted"
files["**/*.rockspec"].std = "+rockspec"
files["**/*.luacheckrc"].std = "+luacheckrc"
files["**/config.ld"].std = "+ldoc"

These get added to the global std specified in the config file. Each of these can be overridden by setting a different std value for the corresponding key in files. And setting std on the commandline removes these default overrides entirely. ✨

✨ INLINE OPTIONS ✨

okay bestie so here's the tea: Luacheck lets you set some options directly inside the files it's checking, using inline configuration comments. And these inline options are the highest priority β€” they overwrite both config options AND CLI options. like, they run the whole group chat, fr fr. πŸ‘‘

An inline configuration comment is a short comment that starts with the luacheck: label, possibly after some whitespace. The body of that comment should have comma separated options, where each option invocation is its name plus space separated arguments. It can also contain notes wrapped in balanced parentheses</>, and those just get ignored (they're giving "no thoughts, head empty" energy). 🫢

Here's every option that's supported, plus how many arguments each one takes:

  • global β€” 0 arguments
  • unused β€” 0 arguments
  • redefined β€” 0 arguments
  • unused args β€” 0 arguments
  • unused secondaries β€” 0 arguments
  • self β€” 0 arguments
  • compat β€” 0 arguments
  • module β€” 0 arguments
  • allow defined β€” 0 arguments
  • allow defined top β€” 0 arguments
  • max line length β€” 1 argument (with no and no arguments, it disables line length checks)
  • max code line length β€” 1 argument (with no and no arguments, it disables code line length checks)
  • max string line length β€” 1 argument (with no and no arguments, it disables string line length checks)
  • max comment line length β€” 1 argument (with no and no arguments, it disables comment line length checks)
  • std β€” 1 argument
  • globals β€” 0 or more arguments
  • new globals β€” 0 or more arguments
  • read globals β€” 0 or more arguments
  • new read globals β€” 0 or more arguments
  • not globals β€” 0 or more arguments
  • ignore β€” 0 or more arguments (without arguments, everything is ignored)
  • enable β€” 1 or more arguments
  • only β€” 1 or more arguments

Options that take no arguments can be prefixed with no to invert their meaning. E.g. --luacheck: no unused args disables unused argument warnings. iconic, honestly. πŸ’…


Now, which part of the file gets affected by an inline option depends on where you put it. If there's any code on the same line as the option, only that line is affected. Otherwise, everything up until the end of the current closure is. And in particular, inline options at the very top of the file affect all of it. main character placement energy. πŸ‘‘

-- luacheck: globals g1 g2, ignore foo
local foo = g1(g2) -- No warnings emitted.

-- The following unused function is not reported.
local function f() -- luacheck: ignore
   -- luacheck: globals g3
   g3() -- No warning.
end

g3() -- Warning is emitted as the inline option defining g3 only affected function f.

For fine-grained control over inline option visibility, use the luacheck: push and luacheck: pop directives:

-- luacheck: push ignore foo
foo() -- No warning.
-- luacheck: pop
foo() -- Warning is emitted.

it's giving save-state-and-restore, and honestly? we're obsessed. πŸŽ€

✨ LUACHECK MODULE ✨

okay bestie, plot twist β€” luacheck isn't just a command you yell at in your terminal, it's also a whole Lua module you can pull into your own code. Just hit it with:

local luacheck = require "luacheck"

and boom, you've got these functions to play with:

  • luacheck.get_report(source) β€” hand it a source string, it hands you back the analysis data (a table, obviously, this is Lua πŸ’…).
  • luacheck.process_reports(reports, options) β€” takes an array of analysis reports and applies options to them. So like, reports[i] gets checked against options, options[i], options[i][1], options[i][2], ... as its options, and each one overrides the last in that order. The options table has fields just like the config options do β€” go peep Config options for the deets. Any analysis report with a fatal field gets ignored entirely, no cap. Returns the final report, and yes, Report format is right below if you're lost.
  • luacheck.check_strings(sources, options) β€” checks an array of sources using options and returns the final report. Tables in the sources array that have a fatal field get skipped, sorry bestie 🫢.
  • luacheck.check_files(files, options) β€” checks an array of files with options, returns the final report. Fun fact: you can pass open file handles instead of filenames! They get read till EOF and then closed. Very demure, very mindful ✨.
  • luacheck.get_message(issue) β€” give it an issue, it gives you back a string message. See Report format.

and also, lowkey important:

  • luacheck._VERSION β€” contains the Luacheck version as a string in MAJOR.MINOR.PATCH format πŸ‘‘

one more thing: using luacheck as a function is literally equivalent to calling luacheck.check_files. same energy, shorter name.


🌸 Report format 🌸

A final report is an array of file reports, plus three extra fields chilling on it: warnings, errors, and fatals β€” holding the total number of warnings, errors, and fatal errors respectively. slay.

A file report is an array of issues (those are your warnings and errors). But if a fatal error happened while checking that file, its report will instead have a fatal field holding the error type and a msg field holding the error message. πŸ’€

An issue is a table with a code field telling you its type (go see List of warnings), plus line, column, and end_column fields pointing at exactly where in the source the warning lives. There might also be a name field with the name of the related variable. And some issue types come with extra bonus fields:

  • 011 β€” the msg field contains the syntax error message.
  • 111 β€” the module field indicates that the assignment is to a non-module global variable.
  • 122, 142, 143 β€” the indirect field indicates that the global field was accessed using a local alias.
  • 122, 142, 143 β€” the field field contains the string representation of the related global field.
  • 211 β€” the func field indicates that the unused variable is a function.
  • 211 β€” the recursive field indicates that the unused function is recursive.
  • 211 β€” the mutually_recursive field is set for unused mutually recursive functions.
  • 314 β€” the field field contains the string representation of the ununsed field or index. (yes bestie, that spelling is straight from the source, I don't make the rules πŸ’€)
  • 011 β€” the prev_line, prev_column, and prev_end_column fields may point to an extra relevant location, like the opening unpaired bracket. so helpful fr fr.
  • 4.. β€” the prev_line, prev_column, and prev_end_column fields contain the location of the overwritten definition.
  • 521 β€” the label field contains the label name.
  • 631 β€” the line_ending field contains "comment" or "string" if the line ending is within a comment or a string.
  • 631 β€” the max_length field contains the maximum allowed line length.

Other fields may be present for internal reasons. mysterious. iconic. we don't ask questions. πŸ™ˆ

This is documentation for version 1.2.0 of Luacheck, a linter for Lua. πŸ¦‹


πŸ’– AUTHOR πŸ’–

Peter Melnichenko


2024 - 2018, Peter Melnichenko


and that's a wrap on the whole page, bestie!! 🫢 this was version 1.2.0 of Luacheck, dated Jun 13, 2024 β€” go forth and lint absolutely everything, you absolute main character πŸ‘‘βœ¨

πŸ“¦ where this page comes from