Files
jpmschweitzerandClaude a005e48405 feat(config): parse sweep — verify every project script parses, not just the startup path
godot-cold-parse only ever sees scripts on the STARTUP path: autoloads and
the main scene chain. That is the correct scope for the job it was built for
(Sprint 36's `Could not find base class "MetaScreen"`, a registration-ORDER
bug), but it is far narrower than the name suggests, and most of the codebase
is invisible to it. Verified by deliberately breaking a non-startup UI script
and a test file in turn: cold-parse reported "clean", exit 0, for both.

That is the second half of today's false green. A parse error in
test_step_canvas_annotation_layer.gd survived cold-parse AND survived
gdUnit4, which reports the suites that DID load as a clean pass. Two gates,
one blind spot: neither verified that a file it never opened was openable.

godot-parse-sweep opens every .gd in the project (226 today, addons and
.godot excluded) and fails on any that will not parse.

The split between the two halves is forced, not stylistic. No Godot API
reports GDScript parse failure reliably:

  - ResourceLoader.load(path, "GDScript", CACHE_MODE_IGNORE) SEGFAULTS the
    engine on a script that fails to parse — it dies on exactly the input the
    tool exists to find.
  - GDScript.new() + source_code + reload() returns a clean error code but
    detaches the script from its resource_path, so class_name, preload() and
    relative extends stop resolving: it reported 150 of 226 healthy scripts
    as broken.
  - Plain ResourceLoader.load() neither crashes nor false-positives, but
    returns a NON-null object for a broken script, so its return value is
    useless.

The engine's own stderr is the only honest signal. So the GDScript half just
opens files and makes no verdict; the wrapper scrapes the diagnosis. The
wrapper also refuses to pass unless the sweep reported completion, so a
future break in the walk cannot itself become a false green.

Unlike cold-parse, "Cannot infer the type" is NOT filtered. That filter is
precisely why cold-parse stayed silent about the file below.

First run found a real one: client/tests/util/scene_helper.gd has not parsed
since 2026-02-25 — five months — because `func(a := null, ...)` cannot infer
a type from null. Fixed with explicit `: Variant` params. Blast radius is
zero (the helper has no importers, so nothing else was taken out with it),
but it went unseen by two gates for five months, which is the point.

Full suite green at 3660.

Pair session with Jeroen, 2026-07-27.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 00:32:42 +02:00

65 lines
2.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# tooling/godot-parse-sweep — open every project .gd and fail on any that won't parse.
#
# Complements `godot-cold-parse`, which does NOT cover this. That script only
# ever sees scripts on the STARTUP path (autoloads + the main scene chain) —
# correct for the registration-ORDER bug it was built for, far narrower than
# its name implies. Verified 2026-07-27 by breaking a non-startup UI script
# and a test file in turn: cold-parse reported "clean", exit 0, for both.
#
# Division of labour, deliberate:
# godot-cold-parse — cold-cache STARTUP ordering (class_name/autoload race)
# godot-parse-sweep — does every file in the project parse at all
#
# The GDScript half (client/tools/parse_sweep.gd) only opens files; it makes no
# verdict, because no Godot API reports GDScript parse failure reliably (see
# that file's header — one segfaults, one false-positives 150/226, and plain
# load() returns non-null for a broken script). The engine's own stderr is the
# only honest signal, so the verdict is made here by scraping it.
#
# Exit 0 + "clean" if every script parsed. Exit 1 + the offending lines if not.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
set +e
RAW=$(godot --headless --path "$REPO_ROOT/client" -s res://tools/parse_sweep.gd 2>&1)
GODOT_EXIT=$?
set -e
if [ "$GODOT_EXIT" -ne 0 ]; then
echo "godot-parse-sweep: godot itself exited $GODOT_EXIT — not a parse verdict" >&2
printf '%s\n' "$RAW" | tail -20 >&2
exit "$GODOT_EXIT"
fi
# The sweep must actually have run. Without this, a future change that breaks
# the walk (or renames the script) would produce zero error lines and read as
# a clean sweep — the same false-green shape this tool exists to close.
if ! printf '%s\n' "$RAW" | grep -q '^parse-sweep: opened'; then
echo "godot-parse-sweep: the sweep did not report completion — no verdict possible" >&2
printf '%s\n' "$RAW" | tail -20 >&2
exit 1
fi
SUMMARY=$(printf '%s\n' "$RAW" | grep '^parse-sweep: opened')
# NOTE: unlike godot-cold-parse, "Cannot infer the type" is NOT filtered here.
# That filter is why cold-parse stayed silent about tests/util/scene_helper.gd,
# which genuinely does not parse — the suppressed class was hiding a real
# failure, not noise.
MATCHES=$(printf '%s\n' "$RAW" \
| grep -E 'Parse Error|Failed to load script' \
| grep -v "Failed loading resource: res://assets" || true)
if [ -n "$MATCHES" ]; then
echo "PARSE SWEEP FAILED — at least one script does not parse." >&2
printf '%s\n' "$MATCHES" | head -40 >&2
echo "" >&2
echo " ${SUMMARY}" >&2
echo " An unparseable file cannot run. If it is a test suite, it did not" >&2
echo " execute, and any pass count reported elsewhere excludes it." >&2
exit 1
fi
echo "godot-parse-sweep: clean — ${SUMMARY#parse-sweep: }"