Linter¶
The linter owns code-quality guidance. Its role is comparable to ESLint: it
reports suspicious constructs without rewriting presentation. tua lint
enables Tua's standard rules automatically and reports syntax and lint
diagnostics; use tua check for type, project, and domain diagnostics.
--strict adds every Tua-specific optional lint to the standard rules. The
standard rules are also enabled by default in tua check and the editor. A
project can opt out of that default without changing tua lint:
Rules¶
Tua's standard rules cover portable Lua/Tua code-quality checks. Several rules share an existing Tua code because they are the same analysis.
| Rule | Tua diagnostic |
|---|---|
UnknownGlobal |
TL4004 |
GlobalUsedAsLocal |
TL4052 |
LocalShadow |
TL4035 |
SameLineStatement |
TL4036 |
MultiLineStatement |
TL4037 |
LocalUnused |
TL4005 |
FunctionUnused |
TL4055 |
DeprecatedApi |
TL4056 |
ParentConstructorPath |
TL4057 |
VirtualCallInInit |
TL4058 |
RedundantInheritedField |
TL4059 |
ImportUnused |
TL4054 |
BuiltinGlobalWrite |
TL4038 |
PlaceholderRead |
TL4039 |
UnreachableCode |
TL4040 |
UnknownType |
TL4041 |
ForRange |
TL4042 |
UnbalancedAssignment |
TL4027, TL4028 |
ImplicitReturn |
TL4053 |
DuplicateLocal |
TL4043 |
FormatString |
TL4044 |
TableLiteral |
TL4001 |
UninitializedLocal |
TL4045 |
DuplicateFunction |
TL4046 |
TableOperations |
TL4047 |
DuplicateCondition |
TL4012, TL4013 |
MisleadingAndOr |
TL4048 |
CommentDirective |
TL4049 |
IntegerParsing |
TL4050 |
ComparisonPrecedence |
TL4051 |
UnknownGlobal considers plain global assignments and simple global function
declarations anywhere in the current Tua file to be definitions, regardless of
source order. Qualified writes still require a known root, and compound
assignment reads its target before writing it. Runtime-provided or cross-file
globals should be listed in diagnostics.globals.
BuiltinGlobalWrite reports direct assignments, compound assignments, and
simple global function declarations that overwrite protected Lua 5.1/LuaJIT,
LOVE, or Tua helper globals. It resolves lexical bindings first, so assigning a
predeclared local or parameter with builtin-like spelling remains valid. Field
writes such as math.custom = value do not replace the builtin root and are not
reported. The protected set follows Tua's LuaJIT/LOVE target rather than
completion-only entries from other Lua versions.
PlaceholderRead treats the exact name _ as a write-only placeholder for
locals, parameters, loop bindings, and globals. Plain assignments can discard a
value without producing unused or global-local guidance. Reading _, including
through a compound assignment or as the base of a field access, reports the
warning and recommends a named variable. Longer _-prefixed names retain their
normal value semantics while remaining exempt from unused-binding guidance.
UnreachableCode reports the first statement in each unreachable region and
identifies whether the preceding path always returns, breaks, errors, or merges
those outcomes. Control-flow termination propagates through nested blocks even
when they already contain unreachable statements. Direct calls to the resolved
Lua builtins error(...) and assert(false) count as errors; lexically shadowed,
configured, or assigned globals with those names remain ordinary calls.
UnknownType validates string names compared with the resolved LuaJIT builtin
type(...). Its accepted results are nil, boolean, userdata, number,
string, table, function, thread, and LuaJIT's cdata; Luau-only names
such as vector and buffer are not part of Tua's target. Tua does not invent a
global typeof, and lexically shadowed, configured, or assigned type functions
remain ordinary calls. For a statically known LOVE object, the same rule checks
object:type() comparisons and literal object:typeOf(...) arguments against
Tua's generated LOVE 11.x type catalog. Use Tua's value is Type syntax for
named metatable, class, and alias checks; unsupported is targets are reported
separately as TL1007.
ForRange checks numeric for loops that use Lua's implicit 1 step. It
reports constant bounds that count downward or cannot land exactly on the limit,
including decimal and hexadecimal-integer LuaJIT literals, and recognizes the
common #items, 1 and #items, 0 array mistakes. Dynamic bounds remain
permissive, and writing any explicit step silences this opinionated guidance.
FormatString validates literal LuaJIT/Lua 5.1 string.format formats, Lua
patterns, capture references, and literal string.gsub replacements on calls
to the resolved builtin string library. Literal receivers also support the
standard ("..."):format(...) form. string.find(..., true) is a plain search,
and an unknown plain flag stays permissive. Pattern methods such as
value:match(...) are not checked because the receiver is not resolved as the
builtin library. os.date formats remain host-defined by LuaJIT's C strftime
integration and are therefore not labelled invalid by this portable rule.
UnbalancedAssignment compares explicit targets and values without guessing a
function's return count. A shortage ending in a direct call or varargs remains
valid Lua multi-return code, including intentional selection of a call's first
result. A trailing nil also records that missing values are intentional.
Unambiguous shortages recommend adding nil; excess values warn that some
results are unused.
ImplicitReturn reports a function that explicitly returns one or more values
on one path but can still reach its end without a return. Bare return and
return nil both make a path explicit, and resolved error(...) or
assert(false) calls terminate it. A repeat body that always terminates and
an unbreakable while true or repeat ... until false loop cannot fall
through; a reachable break restores the possible fallthrough. The diagnostic
names declared functions, cites the first valued-return line, and points to the
last statement on the fallthrough path.
GlobalUsedAsLocal follows the common enclosing function across nested
closures, so a value shared only by children of one function can still become a
local in that parent. It also reports globals whose reads in separate functions
all follow an unconditional assignment. Module-scope definitions and reads that
may occur before a conditional assignment remain valid global patterns.
LocalShadow reports a used local that hides an earlier declaration in the
same function, including a parameter or a sequential declaration in the same
block. It also reports locals that hide a user global referenced anywhere in
the file. Shadowing across function boundaries, unused locals, duplicate
declarations, Lua/LOVE builtins, and configured runtime globals do not produce
this warning.
DuplicateLocal reports repeated names in one local declaration or function
parameter list as TL4043. It identifies the original declaration by line or
column and recognizes the implicit self parameter of colon methods. Repeating
the exact placeholder name _ remains valid. The duplicate_parameters
setting can enable the parameter check without enabling the complete standard
rule set.
DuplicateFunction reports repeated local, global, and qualified function
declarations in one lexical block. Separate branch and nested blocks remain
independent, while dot and colon declarations of the same method share one
qualified name. Every later duplicate identifies the original definition.
DuplicateCondition compares runtime expression structure rather than source
text. It flattens grouped and and or chains, treats equivalent numeric and
escaped-string literals consistently, and preserves meaningful whitespace
inside strings. The repeated condition is highlighted and identifies the
original line or column. The common Lua value and value or fallback ternary
idiom remains quiet. TL4012 covers if/elseif chains, while TL4013
covers boolean chains and the optional broader duplicate_binary_operands
checks.
MisleadingAndOr reports condition and false or fallback and the equivalent
literal-nil form because Lua's or always selects the fallback instead of
preserving that first alternative. The diagnostic identifies the exact value,
points at the complete expression, and recommends an ordinary if statement
assignment so the rewrite remains valid LuaJIT/Lua 5.1 output:
Truthy alternatives such as true and 0 remain valid. Parenthesizing the
left side, as in (condition and false) or fallback, records intentional use
of Lua's fallback behavior and silences this opinionated warning.
ComparisonPrecedence reports an unparenthesized not on the left side of a
comparison and comparison chains such as minimum <= value <= maximum.
Equality warnings suggest the inverse operator, while relational chains suggest
the explicit Lua form minimum <= value and value <= maximum. The complete
expression is highlighted. Comparing two explicitly negated values, such as
not left == not right, remains a valid intentional boolean comparison, and
parentheses silence the warning when the original grouping is deliberate.
TableLiteral compares literal keys by their LuaJIT runtime value, so escaped
strings and signed numeric spellings cannot hide a duplicate. It also reports
explicit numeric keys that overlap list entries and repeated named fields in
Tua table type shapes. Diagnostics identify the earlier field or the conflicting
list entry. The dedicated duplicate_keys setting enables this complete check
even when the standard rule set is disabled.
TableOperations checks calls to the resolved LuaJIT table and ipairs
builtins. It reports zero-based or redundant table.insert/table.remove
indices and a direct final table.insert call that can expand to multiple Lua
values; parentheses around the call explicitly keep its first result. With
known types, # and ipairs also report record tables without an array part
and dictionaries with string keys. Arrays, numeric dictionaries, empty tables,
any, and uncertain unions stay permissive. A known __len or __ipairs
metamethod enables its matching operation, and lexically shadowed builtins are
never inspected. LuaJIT/Lua 5.1 does not provide Luau's table.create or
table.move, so Tua does not invent guidance for them.
UninitializedLocal tracks each binding in a multi-local declaration
independently. Missing initializer slots remain uninitialized unless the final
expression is a direct function call or varargs, which can supply additional
Lua values. A plain assignment or function declaration initializes a local;
compound assignment does not, because it reads the previous value first. The
diagnostic points to the first read, identifies the declaration line, and can
be silenced by explicitly initializing the binding with nil.
LocalUnused treats reads in default-parameter expressions and nested closures
as uses, but does not report unused numeric or generic for bindings. Prefix an
intentionally unused local with _ to silence the warning. The editor only
offers to remove an unused declaration when its initializer is safe to discard;
calls and other potentially effectful expressions are preserved.
FunctionUnused reports direct local and unqualified user-global function
declarations that are never read. Nested declarations participate in their
lexical scope, while _-prefixed names remain intentionally unused. The editor
can remove the complete unused declaration, including a multiline body.
ImportUnused reports unused bindings initialized by a direct call to the
global require, including computed module names, or by Tua's static
import(...) form. Calls to a lexically shadowed require remain ordinary
local initializers. Qualified type references and nested value references count
as uses, while _-prefixed bindings remain intentionally unused. Import source
actions only rewrite compiler-resolved static bindings; dynamic require
calls and shadowed functions are preserved.
DeprecatedApi reports resolved uses of functions and fields explicitly marked
with ---@deprecated. Optional text after the tag becomes replacement guidance:
---@deprecated use newApi
local function oldApi() end
oldApi() -- TL4056: API `oldApi` is deprecated; use newApi
The metadata follows exports from required .lua and .tua modules, while
lexical shadowing prevents unrelated bindings with the same spelling from
warning. Hover presents the guidance, completion marks deprecated candidates,
and semantic tokens mark resolved declarations and uses. Tua does not infer
LOVE deprecations from prose in the generated API catalog.
Class correctness and class style are separate. Invalid inheritance, missing or
spurious override, incompatible override signatures, unknown members, and
uninitialized required fields remain always-on TL1008 errors. The following
class lints are suppressible standard rules and disappear when
diagnostics.rules = false:
ParentConstructorPath(TL4057) reports an explicit childinitthat can return without callingsuper(...). A child with no explicit constructor inherits the parent initializer automatically and remains quiet.VirtualCallInInit(TL4058) reports a directselfcall to an overridable method from an open class'sinit, because a subclass override can observe incomplete subclass state. Final methods, final classes, and calls inside deferred function bodies remain quiet.RedundantInheritedField(TL4059) reports a type-only child field declaration that repeats an inherited field's resolved type without providing a new default.
These rules analyze source class declarations, resolved inheritance, lexical bindings, and constructor flow. They do not inspect emitted Lua. Tua does not warn merely because an override omits its parent method, has an empty body, is exported but unused locally, or belongs to an open class with no known local subclass.
DeprecatedGlobal still depends on a host-provided deprecated-global catalog,
which Tua does not invent. Likewise, dialect-specific table APIs are ignored on
Tua's LuaJIT target. Tua does report unsupported --! directives so foreign
configuration comments never silently appear to affect Tua. In particular,
Luau's --!nolint reports TL4049 and points to Tua's header-only
-- tua-ignore-file TLxxxx syntax. Same-line and next-line forms use
tua-ignore-line and tua-ignore-next-line; every suppression requires an
exact diagnostic code.
In editors, diagnostics with eligible exact codes offer quick fixes to
suppress that code for the current line or the whole file. Tua preserves an
existing trailing comment by using a next-line directive, and extends an
existing file header instead of adding a second tua-ignore-file comment.
Suppression quick fixes respect inline_ignores, inline_ignore_scopes, and
inline_ignore_max_per_file, and are never included in safe fix-all.
The repository's examples/linter fixture contains an intentionally broken
case for every rule Tua can currently emit in the standard set. Running
tua lint there is expected to exit with status 1.
Formatter Boundary¶
SameLineStatement and MultiLineStatement remain lints because they identify
ambiguous statement structure. Whitespace, wrapping, quote style, indentation
width, and empty-line normalization belong only to the Formatter.