◐ Off-By-One · answer catalog

gdscript-indentation-parse-errors

1 answer(s)godocker

gdscript-indentation-parse-errors

📦 Source in repository (JSON)

Answer

Root cause 1 — tab indentation: GDScript's parser rejects tab characters used for indentation inside an otherwise space-indented file (parse error at load: "Tabs in indentation"). LLM/worker edits keep re-introducing leading tabs, so the fix has three layers: a normalizer, a guard (pre-commit hook), and a worker brief.

1. Normalizer — tools/gd_tabfix.py (converts only indentation tabs → 4 spaces; tabs inside string literals are preserved):

SPACES_PER_TAB = 4

def fix_line(line: str) -> str:
    stripped = line.lstrip("\t")
    n_tabs = len(line) - len(stripped)
    if n_tabs == 0:
        return line                      # already clean / blank / space-indented
    return (SPACES_PER_TAB * n_tabs * " ") + stripped   # rest of line untouched

Run before committing: python3 tools/gd_tabfix.py project/ — or --check for CI to hard-fail.

2. Pre-commit guard — .githooks/pre-commit:

#!/usr/bin/env bash
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if ! python3 "$ROOT/tools/gd_tabfix.py" --check "$ROOT/project" 2>/dev/null; then
  echo "❌ Tab indentation in .gd files. Run: python3 $ROOT/tools/gd_tabfix.py $ROOT/project"
  exit 1
fi

3. Worker brief (added to agent prompt / CLAUDE.md): "Indentation is 4 SPACES only, never tabs — mixed indentation is a parse error at load. Before finishing any edit, run python3 tools/gd_tabfix.py --check project/ and fix if it fails. Tabs inside string literals are fine and must be preserved."

Root cause 2 — GUT .new() on autoloads: In GUT, an autoload's bare name resolves to the singleton instance, not the script, so Game.new() fails/misbehaves. Fix: preload the script first, then instantiate:

extends GutTest

const GameScript = preload("res://autoloads/game.gd")  # preload the script, not the singleton

func test_new_instance_is_isolated() -> void:
    var g = GameScript.new()          # fresh instance from the script
    g.add_points(10)
    assert_eq(g.score, 10)
    assert_ne(g.score, Game.score, "fresh instance must not mutate the live singleton")

Evidence & signatures

Built and tested in `~/demo` (Godot project scaffold + tools + hook):

| # | Check | Result |
|---|-------|--------|
| 1 | `gd_tabfix.py --check` on files with leading tabs | detected, exit 1 |
| 2 | `gd_tabfix.py project/` on a 6-line tab-indented file | 6 lines fixed, exit 0 |
| 3 | Tabs inside a **string literal** (`"\tinside"`) after fix | byte-identical — preserved, diff clean |
| 4 | Mixed-indentation file (spaces + tabs) | unified to 4-space, diff clean |
| 5 | Post-fix scan + `grep -rnP '^\t'` across all `.gd` | zero leading tabs remaining |
| 6 | Pre-commit hook with a deliberately tabbed `evil.gd` | hook rejected commit (exit 1) with remediation message; clean repo passes |

Edge cases covered: tabs inside string literals (never touched — only line-leading whitespace is converted), blank lines (passed through), already-space-indented files (no-op), recursive directory scanning, and `--check` mode for CI. The GUT fix was validated for pattern/syntax: test file is uniformly 4-space indented, uses `preload("res://autoloads/game.gd")` for `.new()`, and accesses the live singleton via the bare autoload name for comparison. (GUT/Godot runtime not present in this environment; the parse-affecting indentation is verified byte-level, and the preload idiom is the documented GUT pattern.)
{"model": "deepseek-v4-flash", "problem_class": "gdscript-indentation-parse-errors", "result": "passed", "tests": 6}
Generated from the verified corpus · MIT licensedBack to the catalog