gdstyle 0.1.7

A fast, opinionated linter and formatter for GDScript (Godot 4.x)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
# gdstyle

[![Crates.io](https://img.shields.io/crates/v/gdstyle.svg)](https://crates.io/crates/gdstyle)
[![docs.rs](https://img.shields.io/docsrs/gdstyle)](https://docs.rs/gdstyle)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Build](https://github.com/atelico/gdstyle/actions/workflows/release.yml/badge.svg)](https://github.com/atelico/gdstyle/actions/workflows/release.yml)

<video src="https://github.com/user-attachments/assets/314e0f55-33e2-4365-bef8-87cf4fdaaa1e" controls autoplay loop muted playsinline width="900">
  gdstyle running in the Godot editor: linting, format-on-save, and right-click single-fix on the bottom panel.
</video>

A fast, opinionated linter and formatter for GDScript (Godot 4.x), built in Rust.

gdstyle catches style violations, naming inconsistencies, and common code-quality issues, and auto-formats GDScript to the [official Godot style guide](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html). Many of the conventions are taken from Nathan Lovato and [GDQuest's GDScript style guide](https://gdquest.gitbook.io/gdquests-guidelines).

## Features

- 54 lint rules across syntax, naming, formatting, ordering, and code quality.
- Formatter (`gdstyle fmt`) that's in-place and idempotent, and reorders class members into the canonical Godot order.
- Auto-fix: `--fix` for the safe ones, `--unsafe-fix` for renames. Renames follow into other `.gd` files and into the `.tscn`/`.tres` scene wiring.
- Single static binary. No Python, no Rust toolchain, no Godot install required to run it.
- Optional Godot editor plugin: bottom panel with clickable diagnostics, single-click fixes, Lint/Format on save. Uses the GDExtension when present, falls back to the CLI binary otherwise.
- [pre-commit]https://pre-commit.com framework integration out of the box (`gdstyle` and `gdstyle-fmt` hooks).
- Text and JSON output with configurable exit codes, so it slots into CI.
- TOML config (`gdstyle.toml`) with per-rule overrides and CLI flags for one-off tweaks.
- Per-line (`# gdstyle:ignore`) and per-file (`# gdstyle:ignore-file`) suppression comments, both with optional rule-list narrowing.
- Usable as a Rust library, not just a CLI.

## Installation

Pre-built binaries are available for all major platforms. You don't need a Rust toolchain unless you want to build from source.

### Pre-built binaries (recommended)

1. Go to the [latest release]https://github.com/atelico/gdstyle/releases/latest
2. Download the archive for your platform:
   - **Linux**: `gdstyle-x86_64-unknown-linux-gnu.tar.gz`
   - **macOS (Intel)**: `gdstyle-x86_64-apple-darwin.tar.gz`
   - **macOS (Apple Silicon)**: `gdstyle-aarch64-apple-darwin.tar.gz`
   - **Windows**: `gdstyle-x86_64-pc-windows-msvc.zip`
3. Extract the `gdstyle` binary and place it somewhere on your `PATH`:

```bash
# Example for Linux / macOS
tar xzf gdstyle-*.tar.gz
cp gdstyle ~/.local/bin/   # or /usr/local/bin/, or anywhere on your PATH
```

### Building from source

To build from source you need a [Rust toolchain](https://rustup.rs/).

```bash
git clone https://github.com/atelico/gdstyle.git
cd gdstyle
cargo build --release

# The binary is at target/release/gdstyle
cp target/release/gdstyle ~/.local/bin/
```

### From crates.io

```bash
cargo install gdstyle
```

## Quick start

```bash
# Lint the current directory (recursively finds all .gd files)
gdstyle

# Lint specific files or directories
gdstyle check src/player.gd src/enemies/

# Auto-fix safe violations
gdstyle check --fix

# Auto-fix all violations including unsafe ones
gdstyle check --unsafe-fix

# Format all GDScript files in place
gdstyle fmt

# Check formatting without modifying files (exit 1 if changes needed)
gdstyle fmt --check

# Show formatting diff without modifying files
gdstyle fmt --diff

# Output lint results as JSON (for CI integration)
gdstyle check --format json

# List all available rules
gdstyle rules

# Generate a starter config file
gdstyle init

# Only check naming rules
gdstyle check --select naming

# Ignore specific rules
gdstyle check --ignore "format/max-line-length,format/double-quotes"

# Override max line length
gdstyle check --max-line-length 120
```

## Rules

gdstyle ships with 54 rules organized into five categories. Most rules are enabled by default (a few advisory rules are opt-in).

### Syntax (1 rule)

| Rule | Description | Fixable |
|------|-------------|---------|
| `syntax/lex-error` | Report lexer errors: unterminated strings, invalid numbers, unexpected characters | - |

### Naming (11 rules)

| Rule | Description | Fixable |
|------|-------------|---------|
| `naming/class-name-pascal-case` | Class names must use `PascalCase` | unsafe |
| `naming/function-name-snake-case` | Function names must use `snake_case` | unsafe |
| `naming/variable-name-snake-case` | Variable names must use `snake_case` | unsafe |
| `naming/constant-name-screaming-case` | Constants must use `SCREAMING_SNAKE_CASE` (or `PascalCase` for preloads) | unsafe |
| `naming/signal-name-snake-case` | Signal names must use `snake_case` | unsafe |
| `naming/enum-name-pascal-case` | Enum type names must use `PascalCase` | unsafe |
| `naming/enum-member-screaming-case` | Enum members must use `SCREAMING_SNAKE_CASE` | unsafe |
| `naming/file-name-snake-case` | File names must use `snake_case` | - |
| `naming/signal-past-tense` | Signal names should use past tense (handles irregular verbs, gerunds, nouns) | unsafe |
| `naming/private-underscore-prefix` | Private members with `_` should not have `@export` | - |
| `naming/node-name-pascal-case` | `$NodePath` references should use `PascalCase` | unsafe |

### Formatting (18 rules)

| Rule | Description | Fixable |
|------|-------------|---------|
| `format/max-line-length` | Lines must not exceed the configured max length (default: 100) | fmt |
| `format/trailing-whitespace` | No trailing whitespace on any line | safe |
| `format/trailing-newline` | Files must end with a newline character | safe |
| `format/no-tabs-as-spaces` | Indentation must use tabs (configurable to spaces) | safe |
| `format/boolean-operators` | Use `and`/`or`/`not` instead of `&&`/`\|\|`/`!` | safe |
| `format/double-quotes` | Prefer double quotes for strings | safe |
| `format/comment-spacing` | Comments must have a space after `#` | safe |
| `format/no-unnecessary-parens` | No unnecessary parentheses in `if`/`while`/`elif` conditions | safe |
| `format/number-literals` | Hex digits must be lowercase (`0xff`, not `0xFF`) | safe |
| `format/one-statement-per-line` | One statement per line (no semicolons to separate statements) | safe |
| `format/blank-lines` | Collapse 3+ blank lines to 2 | safe |
| `format/trailing-comma` | Trailing comma on last item of multi-line collections | safe |
| `format/operator-spacing` | One space around binary operators | safe |
| `format/colon-spacing` | No space before `:`, one space after (except `:=` and end of line) | safe |
| `format/comma-spacing` | No space before `,`, one space after (except newline / closing bracket) | safe |
| `format/float-literal-zeros` | Float literals need leading/trailing zeros (`0.5`, not `.5`) | safe |
| `format/large-number-underscores` | Large numbers (>=10000) should use underscores | safe |
| `format/enum-one-per-line` | Each enum member on its own line | safe |

### Ordering (1 rule)

| Rule | Description | Fixable |
|------|-------------|---------|
| `order/class-member-order` | Class members must follow the canonical Godot ordering | fmt |

The canonical ordering enforced by `order/class-member-order` is:

1. `@tool`
2. `@icon`
3. `class_name`
4. `extends`
5. Doc comments (`##`)
6. Signals
7. Enums
8. Constants
9. Static variables
10. `@export` variables
11. Regular variables
12. `@onready` variables
13. Virtual methods (`_init`, `_ready`, `_process`, etc.)
14. Regular methods
15. Inner classes

### Quality (23 rules)

| Rule | Description | Default | Fixable |
|------|-------------|---------|---------|
| `quality/max-function-length` | Functions must not exceed the configured max body length (default: 50 lines) | on | - |
| `quality/max-file-length` | Files must not exceed the configured max length (default: 1000 lines) | on | - |
| `quality/max-parameters` | Functions must not have more than the configured max parameters (default: 5) | on | - |
| `quality/unnecessary-pass` | `pass` alongside other statements is unnecessary | on | - |
| `quality/no-debug-print` | Debug `print()`/`prints()`/`printerr()` calls left in code | **off** | - |
| `quality/self-comparison` | Comparing a value with itself (`x == x`) | on | - |
| `quality/no-self-assign` | Self-assignment (`x = x`) | on | - |
| `quality/duplicate-dict-key` | Duplicate keys in dictionary literals | on | - |
| `quality/duplicated-load` | Same path passed to `load()`/`preload()` multiple times | on | - |
| `quality/type-hint` | Missing type hints on variables, parameters, and return types | **off** | - |
| `quality/empty-function` | Empty or pass-only functions | **off** | - |
| `quality/max-class-variables` | Too many class-level variables (default: 15) | on | - |
| `quality/max-public-methods` | Too many public methods per class (default: 20) | on | - |
| `quality/max-inner-classes` | Too many inner classes per file (default: 5) | on | - |
| `quality/no-else-return` | Unnecessary `else`/`elif` after `return` | on | - |
| `quality/unreachable-code` | Code after `return`, `break`, or `continue` | on | - |
| `quality/await-in-loop` | `await` used inside a `for`/`while` loop | on | - |
| `quality/allocation-in-loop` | Object allocation (`.new()`) inside a loop | on | - |
| `quality/process-get-node` | Node lookups (`$`, `get_node()`) in `_process`/`_physics_process` | on | - |
| `quality/max-nesting-depth` | Nesting depth exceeds limit (default: 4) | on | - |
| `quality/max-returns` | Too many `return` statements per function (default: 6) | on | - |
| `quality/max-branches` | Too many branches (`if`/`elif`/`match`) per function (default: 8) | on | - |
| `quality/max-local-variables` | Too many local variables per function (default: 10) | on | - |

## Auto-fix

gdstyle can automatically fix many violations:

```bash
# Fix safe violations only (formatting, naming conventions)
gdstyle check --fix

# Fix all violations including unsafe ones (signal renaming, member reordering)
gdstyle check --unsafe-fix
```

> **Fixes are written to disk in place.** There's no backup and no
> confirmation prompt. Commit or stash before running `--fix` or
> `--unsafe-fix`, then review the diff. (`gdstyle fmt --diff` previews
> formatter changes without writing.)

Safe fixes preserve behavior. Unsafe fixes can change semantics (renaming signals, variables, and other identifiers), so review the diff before committing.

When `--unsafe-fix` renames an identifier, gdstyle follows the rename across every `.gd` file in the project and into the `.tscn`/`.tres` scene files that wire signals or methods to that name. Anything it can't safely rewrite is reported as a warning so you can fix it by hand.

## Formatter

The `fmt` subcommand reformats GDScript files in a single pass:

```bash
# Format all .gd files in place
gdstyle fmt

# Check if files are already formatted (exit 1 if not)
gdstyle fmt --check

# Show a diff of what would change
gdstyle fmt --diff
```

The formatter normalizes indentation, trailing whitespace, blank lines (including the spacing between class members), boolean operators (`&&`/`||`/`!` to `and`/`or`/`not`), string quotes, comment spacing, colon and comma spacing, float and hex literals, trailing newlines, and single-line enums (expanded to multi-line). It reorders class members to match the canonical style guide order, and wraps long lines at commas inside parentheses, brackets, and braces. Running it twice produces the same output.

> **Note:** Line wrapping breaks at comma boundaries inside delimiters (parentheses, brackets, braces), `and`/`or` operators in `if`/`elif`/`while` conditions, and word boundaries in long comments. Lines without any breakable pattern (e.g., long strings, property chains, continuation lines without enclosing delimiters) are left alone.

## Configuration

gdstyle looks for a config file named `gdstyle.toml` or `.gdstyle.toml` in the current directory and walks up the directory tree until it finds one. You can also specify a config file explicitly with `--config`, or generate a starter one:

```bash
gdstyle init
```

### Example config file

```toml
# gdstyle.toml

# Maximum line length (default: 100)
max_line_length = 100

# Use tabs for indentation (default: true)
use_tabs = true

# Maximum function body length in lines (default: 50)
max_function_length = 50

# Maximum file length in lines (default: 1000)
max_file_length = 1000

# Maximum number of function parameters (default: 5)
max_parameters = 5

# Maximum return statements per function (default: 6)
max_returns = 6

# Maximum nesting depth inside a function (default: 4)
max_nesting_depth = 4

# Maximum local variables per function (default: 10)
max_local_variables = 10

# Maximum branches (if/elif/match) per function (default: 8)
max_branches = 8

# Maximum class-level variables (default: 15)
max_class_variables = 15

# Maximum public methods per class (default: 20)
max_public_methods = 20

# Maximum inner classes per file (default: 5)
max_inner_classes = 5

# File/directory patterns to exclude
exclude = [".godot", "addons"]

# Per-rule severity overrides
# Values: "off", "warn", "error"
[rules]
"format/double-quotes" = "off"             # Disable a rule entirely
"naming/class-name-pascal-case" = "error"   # Escalate to error
"quality/max-function-length" = "warn"      # Keep as warning
"quality/no-debug-print" = "warn"          # Enable an off-by-default rule
```

### Default configuration

When no config file is found, gdstyle uses these defaults:

| Setting | Default |
|---------|---------|
| `max_line_length` | 100 |
| `use_tabs` | `true` |
| `max_function_length` | 50 |
| `max_file_length` | 1000 |
| `max_parameters` | 5 |
| `max_returns` | 6 |
| `max_nesting_depth` | 4 |
| `max_local_variables` | 10 |
| `max_branches` | 8 |
| `max_class_variables` | 15 |
| `max_public_methods` | 20 |
| `max_inner_classes` | 5 |
| `exclude` | `[".godot", "addons"]` |

Most rules are enabled by default with `warn` severity. Three advisory rules (`quality/type-hint`, `quality/empty-function`, `quality/no-debug-print`) are off by default and must be explicitly enabled.

## Suppressing diagnostics

gdstyle has two ways to silence a warning from source: **per-line** for
spot exemptions and **per-file** for class- or file-scope rules. Both
use a `# gdstyle:ignore` comment, with the same `=rule1,rule2` syntax
for narrowing to specific rules.

| Directive | Scope | Form |
|---|---|---|
| `# gdstyle:ignore` | the **next** code line | standalone (own line) |
| `# gdstyle:ignore` | the **same** code line | inline (end of a code line) |
| `# gdstyle:ignore-file` | every diagnostic in the file | anywhere in the file |

Add `=rule1,rule2` to any of the above to narrow the suppression to a
comma-separated list of rule IDs. Without the `=...` suffix, every rule
is suppressed within that scope. Rule IDs are the full
`category/rule-name` shown in the diagnostic output (e.g.
`naming/variable-name-snake-case`).

### Per-line suppression

Standalone (applies to the next code line):

```gdscript
# gdstyle:ignore=naming/variable-name-snake-case
var BadName: int = 5
```

Inline (applies to the same line):

```gdscript
var BadName: int = 5  # gdstyle:ignore=naming/variable-name-snake-case
```

Bare (suppresses every rule on the target line):

```gdscript
# gdstyle:ignore
var BadName: int = 5
```

Multiple rules in one directive:

```gdscript
# gdstyle:ignore=naming/variable-name-snake-case,format/max-line-length
var SomeReallyLongVariableNameThatExceedsTheMaxLineLengthAndAlsoUsesTheBadNamingConvention: int = 5
```

### Per-file suppression

Use `# gdstyle:ignore-file` when the diagnostic isn't attached to a
single line, or when you genuinely want a whole-file exemption.
Anchor it at the top of the file by convention so future readers see
it immediately, but the parser accepts it anywhere.

```gdscript
# gdstyle:ignore-file=quality/max-public-methods
class_name OrchestrationFacade
extends Node
# ... 25 public methods follow
```

Multiple rules on one directive, or several directives stacked, both
work:

```gdscript
# gdstyle:ignore-file=quality/max-public-methods,quality/max-class-variables
# gdstyle:ignore-file=quality/max-inner-classes
class_name BigConfigDocument
```

A bare `# gdstyle:ignore-file` (no `=...`) disables *every* rule in
the file. Useful for generated code or third-party drops you don't
own:

```gdscript
# gdstyle:ignore-file
# This file is generated by build/gen.py, do not edit.
```

#### When to use which

- **Per-line** is the right tool for one-off exemptions where the
  diagnostic clearly belongs to a single line (a deliberately weird
  variable name, a long string literal, a public field you want to
  keep snake_case-violating because it mirrors a JSON key).
- **Per-file** is the right tool for rules that report against the
  whole class or file rather than a specific statement:
  - `quality/max-public-methods` (reports at the class header)
  - `quality/max-class-variables` (same)
  - `quality/max-inner-classes` (same)
  - `quality/max-file-length` (reports at line 1)
  - Generated files where any rule is moot.

  Trying to suppress these per-line is awkward at best (you have to
  put the comment inline on the `class_name` line, which makes the
  signature noisy) and impossible at worst (generated files have no
  natural spot for 50 inline comments).

#### Project-wide silencing

If you want to disable a rule across the entire project rather than
file-by-file, set it to `"off"` in `gdstyle.toml` instead:

```toml
[rules]
"quality/max-public-methods" = "off"
```

The TOML config and inline suppressions are independent; either one
silencing a rule is enough to drop the diagnostic.

## CLI reference

```
gdstyle [COMMAND] [OPTIONS] [PATHS]...
```

### Subcommands

| Command | Description |
|---------|-------------|
| `check` | Lint files (default when no subcommand given) |
| `fmt` | Format files in place |
| `rules` | List all available lint rules |
| `init` | Generate a starter `gdstyle.toml` configuration file |

### `check` options

| Option | Description |
|--------|-------------|
| `--fix` | Auto-fix safe violations |
| `--unsafe-fix` | Auto-fix all violations including unsafe ones |
| `--format <FORMAT>` | Output format: `text` (default) or `json` |
| `-c, --config <PATH>` | Path to configuration file |
| `--select <RULES>` | Only check specific rules (comma-separated, supports partial matching) |
| `--ignore <RULES>` | Ignore specific rules (comma-separated) |
| `--max-line-length <N>` | Override the maximum line length |
| `--no-color` | Disable colored output |

### `fmt` options

| Option | Description |
|--------|-------------|
| `--check` | Dry-run: exit 1 if any file would change |
| `--diff` | Print a diff of what would change |
| `-c, --config <PATH>` | Path to configuration file |
| `--no-color` | Disable colored output |

### `init` options

| Option | Description |
|--------|-------------|
| `--force` | Overwrite existing config file |

### Exit codes

| Code | Meaning |
|------|---------|
| `0` | Linting/formatting completed (warnings only, or no issues) |
| `1` | Linting completed with errors, or `fmt --check` found changes |
| `2` | Configuration error |

## CI/CD integration

### GitHub Actions

```yaml
name: GDScript Style

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install gdstyle
        run: cargo install gdstyle

      - name: Check formatting
        run: gdstyle fmt --check

      - name: Lint GDScript files
        run: gdstyle check
```

### Pre-commit hook

gdstyle ships hooks for the [pre-commit](https://pre-commit.com) framework.
Add the following to your project's `.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/atelico/gdstyle
    rev: v0.1.7   # pin to a released tag; bump with `pre-commit autoupdate`
    hooks:
      - id: gdstyle          # lint (fails the commit on diagnostics)
      - id: gdstyle-fmt      # format in place
```

Then install the git hook with `pre-commit install`. The first run builds
gdstyle from source via cargo, so the user needs a Rust toolchain on their
machine; subsequent runs are cached.

If you'd rather not depend on the pre-commit framework, a minimal raw git
hook also works:

```bash
#!/bin/bash
# .git/hooks/pre-commit

GD_FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.gd')

if [ -n "$GD_FILES" ]; then
    gdstyle check $GD_FILES
    if [ $? -ne 0 ]; then
        echo "GDScript lint failed. Fix with 'gdstyle check --fix' or suppress with '# gdstyle:ignore'."
        exit 1
    fi
fi
```

### JSON output format

When using `--format json`, gdstyle outputs a JSON array of diagnostics:

```json
[
  {
    "rule": "naming/variable-name-snake-case",
    "message": "Variable 'BadName' should use snake_case: 'bad_name'",
    "severity": "warn",
    "span": {
      "line": 5,
      "column": 1
    },
    "file": "src/player.gd"
  }
]
```

## Using as a library

You can also use gdstyle as a Rust library. Full API docs live at
[**docs.rs/gdstyle**](https://docs.rs/gdstyle).

```rust
use gdstyle::config::Config;
use gdstyle::linter;
use gdstyle::formatter;

fn main() {
    let config = Config::default();

    // Lint a file
    let diagnostics = linter::lint_file(
        std::path::Path::new("player.gd"),
        &config,
    ).unwrap();

    for d in &diagnostics {
        println!("line {}: [{}] {}", d.span.line, d.rule, d.message);
    }

    // Format a source string
    let source = "var x = 'hello'\n";
    let formatted = formatter::format_source(source, &config);
    assert!(formatted.contains("\"hello\""));
}
```

## Project structure

```
gdstyle/
├── src/
│   ├── main.rs              # CLI entry point (clap subcommands)
│   ├── lib.rs               # Library root
│   ├── token.rs             # Token types (Span, TokenKind, Token)
│   ├── lexer.rs             # Tokenizer (indentation-aware, GDScript 4.x)
│   ├── ast.rs               # AST node types for linting
│   ├── parser.rs            # Lightweight parser (just enough for linting)
│   ├── diagnostic.rs        # Diagnostic, Fix, and Replacement types
│   ├── config.rs            # TOML configuration loading
│   ├── linter.rs            # Main lint pipeline (tokenize -> parse -> rules -> filter)
│   ├── reporter.rs          # Text and JSON output formatting
│   ├── fixer.rs             # Auto-fix engine (applies replacements)
│   ├── formatter.rs         # Multi-pass formatter
│   └── rules/
│       ├── mod.rs           # Rule dispatcher
│       ├── naming.rs        # 11 naming convention rules
│       ├── formatting.rs    # 18 formatting rules
│       ├── ordering.rs      # Class member ordering rule
│       └── quality.rs       # 23 code quality rules
├── gdstyle-gdext/           # GDExtension wrapper (exposes linter/formatter to Godot)
│   ├── Cargo.toml
│   └── src/lib.rs
├── godot-plugin/            # Godot 4.x editor plugin
│   └── addons/gdstyle/
│       ├── plugin.cfg
│       ├── plugin.gd
│       ├── gdstyle_panel.gd
│       └── gdstyle.gdextension
├── tests/
│   ├── integration_test.rs  # End-to-end integration tests
│   └── fixtures/            # GDScript test fixtures
├── .github/workflows/
│   └── release.yml          # CI: builds CLI + GDExtension for all platforms
├── examples/                # Example GDScript files for trying out gdstyle
├── Cargo.toml
└── gdstyle.example.toml
```

## Examples

The `examples/` directory contains sample GDScript files you can use to try out linting and formatting:

```bash
# Lint the examples. Expect warnings about naming, formatting, and quality.
gdstyle check examples/

# See what the formatter would change
gdstyle fmt --diff examples/

# Auto-fix all safe violations
gdstyle check --fix examples/

# Format everything
gdstyle fmt examples/
```

## Testing

gdstyle has 383 tests: 161 unit tests, 219 integration tests, and 3 doctests.

```bash
cargo test           # Run all tests
cargo test --lib     # Unit tests only
cargo test --test integration_test  # Integration tests only
cargo clippy         # Lint check
```

## Godot editor plugin

A Godot 4.x editor plugin lives in `godot-plugin/`. It adds a bottom panel that runs gdstyle and shows clickable diagnostics inside the editor.

The plugin supports two backends:

- **GDExtension (native).** If the GDExtension library is present, it calls into Rust directly with no process overhead. Requires Godot 4.6+.
- **CLI fallback.** Spawns the `gdstyle` binary. If it isn't on `PATH`, a **Download** button fetches the right release from GitHub.

You can switch between backends at any time from the mode dropdown in the toolbar.

### Plugin features

- **Lint Project / Lint File.** Run the linter on every script in the project, or just the one you have open.
- **Fix File.** Apply all available auto-fixes to the current script in one click.
- **Format Project / Format File.** Same split for the formatter.
- **Lint on Save.** Lint after every save (on by default).
- **Format on Save.** Format before linting on save.
- **Right-click Fix.** Right-click any diagnostic with an auto-fix to apply it in place.
- **Click to navigate.** Double-click a diagnostic to jump to the source line.
- **In-memory editing.** Lint, fix, and format work directly on the editor buffer, no disk I/O.

### Installation (pre-built plugin)

1. Download `gdstyle-godot-plugin.zip` from the [latest release]https://github.com/atelico/gdstyle/releases
2. Extract the `addons/gdstyle/` folder into your Godot project
3. Enable the plugin in **Project > Project Settings > Plugins**

### GDExtension API

When using the GDExtension backend, you can use `GdStyle` directly in GDScript:

```gdscript
var style = GdStyle.new()

# Lint a file.
var diagnostics = style.lint_res_file("res://player.gd")
for d in diagnostics:
    print("Line %d: [%s] %s" % [d["line"], d["rule"], d["message"]])
    if d["has_fix"]:
        print("  (auto-fixable, safe=%s)" % d["is_safe_fix"])

# Format a source string.
var formatted = style.format_source(source_code)

# Auto-fix violations.
var fixed = style.fix_source(source_code, "player.gd")

# Fix a single diagnostic by line and rule.
style.fix_at_line("res://player.gd", 12, "naming/variable-name-snake-case")

# Configure.
style.set_max_line_length(120)
style.disable_rule("format/double-quotes")
style.load_config_res("res://gdstyle.toml")
```

## Contributing

1. Fork the repository
2. Create a feature branch: `git checkout -b my-feature`
3. Write tests first, then implement
4. Run the full test suite: `cargo test`
5. Run clippy: `cargo clippy`
6. Commit and push
7. Open a pull request

## License

MIT