glua command runs the same parser, analyser and formatter as the extension,
so a finding in CI is the same finding you saw in the editor. Use it to gate pull
requests or format a whole addon in one go.
Installing
Published on npm asglua-cli. The
analyser and the wiki dataset are bundled into it, so it pulls in no dependencies
of its own.
glua init
Writes .glua.json and .gluafmtrc.json, seeded from the defaults the server
already uses — so a fresh config describes what you have rather than changing
anything the moment it lands.
--force to replace one deliberately.
Both files carry a $schema pointing at
glua.bluejutzu.dev/schemas, so an editor
completes and validates them without needing node_modules — a GMod addon
usually has no Node project at all.
glua lint
Exits
1 when there are errors, or when --max-warnings is exceeded. Otherwise 0.
What it prints
Each finding comes with the line it is about and the part it is about underlined, because a line number alone is a lookup instruction and in a CI log there is no file to open:--no-code-frames goes back to one line per finding. The machine formats
(compact, github, json, sarif) are unaffected either way.
Where the time goes
Caching
Each file’s facts — the globals it defines, the hooks it adds, the net messages it sends, who calls what — are written to.glua-cache keyed by a hash of the
file’s contents. A later run reads them back instead of parsing the file again.
On a 300-file addon:
Linting one file gains most, because the other 299 are indexed purely so the
cross-file rules are right, and none of that work changed.
Findings are never cached, only facts.
net-never-received depends on every
other file in the project, so a cached finding would be wrong the moment an
unrelated file gained a net.Receive. Every finding is recomputed from the
whole set on every run, cache or no cache.touch is
not a reason to redo the work. The directory writes its own .gitignore, and
an upgrade of glua discards the cache rather than reading facts a different
build wrote. Anything that goes wrong with it — corrupt file, read-only
checkout — is a cache miss rather than an error.
--no-cache skips it in both directions. glua fmt never touches it.
Linting stdin
An editor integration usually has a buffer that has not been saved, or has been saved with contents different from what is on disk.--stdin-filepath lints
whatever comes in on stdin as though it were the file at that path:
net.Start typed into an unsaved sv_ file is still checked
against the project’s other net.Receive calls. It does not need to exist on
disk at all; the rest of the project is still indexed around it normally.
Code frames quote the piped text, not whatever the path happens to hold on
disk. --fix, --suppress-all and --prune-suppressions all write back to a
real file, so none of them can be combined with --stdin-filepath.
Fixing
--fix applies only the quick fixes with a single correct outcome, then reports
whatever is left:
util.AddNetworkString in a server file, a missing
AddCSLuaFile above an include, and a C-style compound assignment.
Safe and unsafe fixes
A fix is safe when the code does the same thing afterwards. It is unsafe when it very probably does what you wanted but the tool cannot promise it — the value now evaluates at a different moment, or the edit lands somewhere the tool had to guess. Hoisting aMaterial call out of HUDPaint is the clearest case:
it is the right change nearly every time, and it moves the lookup from every
frame to the moment the file loads.
--fix applies only the safe ones, and says what it left behind:
--fix runs with nobody watching — a pre-commit hook,
a CI job, format-on-save. In the editor every fix is offered normally; the split
is about what gets applied unattended.
--unsafe-fixes on its own exits 2. It only means something alongside
--fix or --fix-dry-run.It deliberately will not add a
net.Receive stub, rename an unused local,
wrap a call in a realm guard, or correct a hook name from a spelling
suggestion. Each of those either guesses, changes control flow, or leaves a
body for you to write — none of which should happen unattended.util.AddNetworkString line — is written once.
The whole project gets indexed even when you lint a single file. Cross-file rules — an unhandled net message, a duplicate hook identifier, a missing
AddCSLuaFile — are only correct once the index has seen everything.Output formats
glua fmt
glua rules
Lists every diagnostic code alongside its settings key. Worth knowing because
the two are different: net-payload-mismatch is what you suppress inline,
netReadWriteMismatch is what you set in .glua.json.
Each code has a section in the rule reference explaining
what it catches. Diagnostics link there directly, so in an editor you can click
the code in the Problems panel rather than looking it up.
glua explain
What one rule means, without leaving the terminal:
glua explain unusedLocal — it says
which code you meant (unused-local) rather than pretending nothing matched,
since that mix-up is the one unused-suppression
exists to catch. Exits 2 for anything it cannot resolve.
Configuration
The CLI reads exactly the same files as the editor —.glua.json,
.gluafmtrc.json, .editorconfig, .prettierrc — resolved from --root or the
working directory. See Configuration.
In GitHub Actions
Thegithub format emits workflow annotations, so findings appear inline on the
pull request diff.
Adopting on a codebase you inherited
Most Garry’s Mod code is code somebody else wrote. Running a linter over it for the first time produces hundreds of findings, none of which are the change you were making — and that is how a linter gets switched back off. A baseline draws a line under what already exists:baseline.schema.json reference for what this file actually contains.
Commit that file. From then on the rules are enforced on everything written
after it, and the backlog waits until you want it:
Pruning is a deliberate act, not an automatic one: a baseline quietly rewriting itself downward would let a rule silently stop being enforced. It is also worth running in CI as a check — a drifted baseline is a sign the code improved and nobody noticed.
Code scanning
--format sarif writes SARIF 2.1.0, which
GitHub code scanning ingests. Worth having over --format github: annotations
live and die with one workflow run, whereas uploaded findings get a history, a
place to be dismissed, and a diff between the pull request and the base branch.
Paths in the file are relative to
--root, or to the directory you ran from. Run it from the repository root, or pass --root, so the uploaded locations line up with the files on the diff. continue-on-error is there so findings still upload when the lint exits non-zero.Colour
HonoursNO_COLOR and turns itself off when piped.
--no-color disables it explicitly, FORCE_COLOR=1 forces it on through a pipe.