A fast GDScript formatter and linter, faithful to the official GDScript style guide.
Note
Complete but young. The parser, formatter and linter are done and tested,
every subcommand works, and configuration is read. It has not yet been used
in anger on anything but gdtoolkit's corpus, so run it alongside your
existing setup before you trust it with --fix. See Status.
gdck is a ground-up reimplementation of godot-gdscript-toolkit
in Rust. It is designed to install alongside the original rather than replace it
in place, so you can try it on a project without giving up a working setup.
Speed. Formatters and linters run on every save and every commit, so both startup cost and throughput are things you actually feel. Over a 321-file, 7,700-line corpus:
gdck |
gdtoolkit |
|
|---|---|---|
| parse | 4 ms | 70 ms |
| format, safety checks on | 56 ms | 802 ms |
| format, safety checks off | 24 ms | 716 ms |
For parsing, most of the gap is Python interpreter startup rather than
throughput — which still counts, since it is paid on every invocation. For
formatting it is mostly real work: both tools re-parse their own output to
verify it by default. Measured on an M-series Mac, averaged over 10 runs
(gdck) and 3 (gdformat); reproduce with the corpus described below.
Style-guide fidelity. The style guide says more than gdtoolkit enforces —
notably code order, quote style, number formatting, comment spacing,
and when to use := against an explicit type. gdck covers the guide, and
auto-fixes what can be fixed safely. The rule catalogue is in
docs/RULES.md.
A reusable parser. gdck-syntax is a standalone
lossless GDScript parser. Editors, doc generators and static analysers need one
too, and there is no reason for each to write its own.
cargo install --git https://github.com/eth0net/gdck gdck-cliThe binary is gdck. It does not install gdformat, gdlint or gdparse, so
it will never collide with gdtoolkit on your PATH.
One rule governs the whole interface:
Nothing is written to disk unless you pass
--fix.
gdck check . # report everything, read-only
gdck fix . # apply everything
gdck fix --fix-order . # and reorder declarations where that is provably safe
gdck format src/ # report formatting differences
gdck format --fix src/ # apply them
gdck lint src/ # report lint problems
gdck lint --fix src/ # apply the fixable ones
gdck parse src/ # report syntax errors
gdck parse --tree a.gd # dump the syntax tree
gdck parse --tokens a.gd # dump the token stream
gdck config # what settings this run would usecheck and fix are the everything-at-once verbs; format and lint are the
narrower ones. --check is accepted everywhere as a no-op, because people
coming from gdformat and black will type it out of habit.
Reports go to standard output and summaries to standard error, so standard
output carries only content — the diagnostics, the diff under --diff, or the
file itself when reading from -.
Exit codes: 0 clean, 1 problems found, 2 the run could not complete.
Every rule is listed in docs/RULES.md, along with how to turn one off for a line, a region or the project.
None is needed — every default is the style guide's. To depart from it, write a
gdck.toml:
[format]
line-length = 100
[lint]
max-returns = 6
disable = ["max-public-methods"]gdck searches upwards from the paths you gave it. With no gdck.toml it falls
back to gdtoolkit's own gdformatrc and gdlintrc, so a project already set
up for gdformat and gdlint keeps its settings without writing anything new.
gdck config prints what a run would actually use, and
docs/CONFIG.md documents every setting.
echo 'pass' | gdck parse -A few choices worth stating up front, because they differ from what you might expect.
Formatters conventionally write by default and linters conventionally report by
default. One tool doing both jobs cannot follow both conventions without
becoming a per-command fact you have to memorise, so gdck follows neither:
--fix writes, nothing else does. The cost is that gdck format a.gd does not
reformat a.gd — it tells you it would, and gdck fix . is the short way to
mean it.
Class-level initialisers run in declaration order, so reordering is not purely cosmetic:
var _config := preload("res://config.tres")
var speed := _config.default_speedThe style guide puts public variables before private ones, so a naive reorder
hoists speed above _config and it silently initialises from null.
gdck treats each file as all-or-nothing: either every required move is
provably safe and the file is reordered, or the file is left exactly as it was
and the problem is reported. A partially reordered file would satisfy neither
the original layout nor the style guide, and would still be flagged by the next
gdck check.
Safety is judged conservatively. Any initialiser that is not self-contained — it
calls a function defined in the file, touches self, or is otherwise opaque —
is treated as potentially reading every member above it. That occasionally
blocks a file that would have been fine, but it cannot be wrong in the direction
that breaks your game. Signals, enums, constants and functions carry no ordering
semantics at all, so files needing only those moved always sort cleanly.
What a reorder produces is a permutation of the source — the same bytes in a
different order — rather than a re-rendering of it. A declaration therefore
takes its comments and annotations with it exactly as written, and nothing can
be lost on the way. Its blank lines travel with it too, which the formatter then
settles; gdck fix --fix-order runs both.
Reordering is opt-in (--fix-order) until the analysis has more mileage.
Existing GDScript projects have already triaged gdlint's findings, so a
reimplementation that quietly widened a rule would present old code as newly
broken. Over gdtoolkit's own test corpus the two agree exactly on
unused-argument (270 findings), constant-name, trailing-whitespace,
function-name, enum-member-name, argument-name, enum-name and
max-arguments.
Where they differ, they differ on purpose, and
docs/RULES.md says why. The shortest
version: gdck measures line length in columns rather than characters, so a
tab-indented line counts the same to the linter as to the formatter; it reports
a pass beside a func that gdlint's statement test does not see; and it
checks a good deal more of the guide's declaration order.
gdtoolkit's rule names are accepted as aliases everywhere a rule is named, so
an existing gdlintrc and existing # gdlint: ignore= comments keep working.
The style guide shows var array = [1, 2, 3] and the same array spread over
four lines as both good, and marks an 83-column if as bad for not being
wrapped. Neither follows from a column limit, so gdck treats the author's own
line breaks as the deciding vote: a bracketed construct written across several
lines stays that way and gains its trailing comma, and one written on a single
line stays there if it fits. To collapse a construct, delete the newlines.
Before returning, the formatter re-parses its own output and verifies that it
parses, that the tree still means the same thing, that no comment was lost, and
that a second pass changes nothing. If any of those fail, the file is left alone
and the reason is reported. --fast turns them off.
These are not decoration. Every one of them caught real bugs while the formatter was being written, including a lexer bug where a single-line lambda inside a wrapped argument list swallowed its own closing bracket. A formatter that silently eats code is far worse than one that refuses to run.
The equivalence used is tree shape rather than token stream, since the guide
asks for rewrites that move tokens: hoisting an inner class's extends onto the
declaration line, and dropping redundant parentheses. Comparing shape is weaker
in exactly those places and stronger where it matters — grouping is encoded by
the nesting, so a parenthesis that actually mattered shows up as a differently
shaped expression rather than as two missing tokens.
Whitespace, comments and blank lines are all nodes in the syntax tree, and the source is always exactly recoverable from it. This is what lets the formatter rewrite one declaration while leaving the comments around it untouched, and it holds even for files with syntax errors — a formatter must never damage a file it merely failed to understand.
| Component | State |
|---|---|
gdck-syntax — lexer |
Done. Indentation, all literal forms, multi-line lambdas |
gdck-syntax — parser |
Done. Full declaration and expression grammar, error recovery |
gdck-config |
Done. gdck.toml, plus gdformatrc and gdlintrc for compatibility |
gdck-format |
Done. Wadler pretty printer with safety checks |
gdck-lint |
Done. 33 rules, 10 of them fixable. See docs/RULES.md |
| Every subcommand | Works |
The parser handles every file in gdtoolkit's corpus of valid GDScript — 324
files across its parser, formatter and gd2py test suites — and round-trips all
353 files there, including the deliberately invalid ones. The formatter formats
every one of those valid files with its safety checks passing.
The formatter is also tested against the style guide's own worked examples: all
56 usable code samples are extracted from the documentation and asserted on
directly, so the guide decides what correct output is. See
crates/gdck-format/tests/style_guide.rs
for the classification of every sample, including the three the formatter
knowingly does not reproduce and why.
The linter is held to the same corpus, on what it must never do rather than on
what it finds: --fix never turns a file that parsed into one that does not,
settles after one more pass, and never introduces a kind of problem the file did
not already have. --fix-order only ever permutes a file's bytes.
- One configuration governs a whole run, found from the directory the given paths have in common. A repository holding several Godot projects with different settings needs one invocation per project.
- Naming conventions are not configurable.
gdckchecks the style guide's conventions directly rather than with a regular-expression engine, so agdlintrcthat customised one is reported as not applied rather than honoured. See docs/CONFIG.md. code-ordercannot tell an overridden virtual method from a custom one, since that needs the whole inheritance chain. It orders the callbacks the guide names and leaves the rest as one group, rather than guessing. See docs/RULES.md.- Naming rules never offer a rename. A name is reached from scene files, from
call()with a string, and from signals connected in the editor, none of which one file can see. - CRLF and CR line endings are rewritten to LF, which the style guide mandates. There is no option to keep them.
- The formatter does not fill lines. Where the style guide hand-wraps a call or
a boolean chain several items per line,
gdckputs one per line. Both of the guide's samples of that are hand-formatted rather than derived from a column limit, so no deterministic rule reproduces them. - No
.gitignoreawareness when walking directories; only a fixed exclusion list (.git,.godot,.import,addons). - The parser is more permissive than Godot in places. It is built to understand well-formed code, not to reject every invalid program — Godot is the authority on what compiles.
- Three files in
gdtoolkit'spotential-godot-bugscorpus do not parse. They document cases where Godot's own behaviour is in question.
cargo test # unit and integration tests
cargo clippy --all-targets # lints, warning-free
cargo fmt --allThe parser is validated against an external corpus when you point it at one:
GDCK_CORPUS=../godot-gdscript-toolkit/tests \
cargo test -p gdck-syntax --test corpus -- --nocaptureThis checks that every .gd file under that directory round-trips through the
tree byte for byte. The formatter has a matching one:
GDCK_CORPUS=../godot-gdscript-toolkit/tests \
cargo test -p gdck-format --test corpus -- --nocaptureAnd so does the linter:
GDCK_CORPUS=../godot-gdscript-toolkit/tests \
cargo test -p gdck-lint --test corpus -- --nocaptureAll three are skipped when the variable is unset.
The style-guide fixtures are regenerated from a checkout of the documentation:
cargo build -p gdck-cli
tools/extract-style-guide-samples.py ../godot-docsSee docs/DESIGN.md for architecture, and CONTRIBUTING.md to get started.
godot-gdscript-toolkit by Paweł Lampe is the reference this
project measures itself against, and its test corpus has been invaluable for
finding the corners of the grammar. It is MIT licensed; any vendored fixtures
keep that license and attribution in licenses/.
The formatter's test fixtures are the code samples from the GDScript style
guide, part of the Godot documentation and licensed CC BY 3.0. Attribution is
in licenses/.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.