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>
90 lines
3.8 KiB
GDScript
90 lines
3.8 KiB
GDScript
extends SceneTree
|
|
|
|
## Parse-sweep: open EVERY project .gd so the engine reports the broken ones.
|
|
##
|
|
## Why this exists. `tooling/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"`, an autoload-vs-class_name registration ORDER bug), but it is
|
|
## far narrower than its name suggests, and most of the codebase is invisible
|
|
## to it. Verified 2026-07-27 by deliberately breaking a non-startup UI script
|
|
## and a test file in turn: cold-parse reported `clean`, exit 0, for both.
|
|
##
|
|
## That gap cost real time. A parse error in `test_step_canvas_annotation_layer.gd`
|
|
## went unnoticed for hours because it passed cold-parse AND because gdUnit4
|
|
## reports the suites that DID load as a clean pass (fixed separately by
|
|
## `tests/run-godot`'s harness guards). Two gates, one blind spot: neither
|
|
## verified that a file it never opened was openable.
|
|
##
|
|
## **This script deliberately makes no verdict of its own.** It opens every
|
|
## script and lets Godot's own front-end print the diagnosis; the wrapper
|
|
## (`tooling/godot-parse-sweep`) greps for that. Two rejected alternatives,
|
|
## both found the hard way while building this:
|
|
##
|
|
## - `ResourceLoader.load(path, "GDScript", CACHE_MODE_IGNORE)` **segfaults
|
|
## the engine** (signal 11, Godot 4.6) on a script that fails to parse —
|
|
## i.e. it dies on exactly the input this 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 lies — but it returns a
|
|
## NON-null object for a broken script, so the return value cannot be trusted
|
|
## either. The engine's stderr is the only honest signal, which is why the
|
|
## verdict lives in the wrapper.
|
|
##
|
|
## Run via `tooling/godot-parse-sweep` (never directly — this half cannot fail).
|
|
|
|
## Vendor and generated trees are not ours to verify. `addons/` is gdUnit4,
|
|
## whose own test corpus deliberately contains broken fixtures — sweeping it
|
|
## would report failures that are the vendor's intended state.
|
|
const SKIP_PREFIXES: PackedStringArray = ["res://addons/", "res://.godot/"]
|
|
|
|
var _checked: int = 0
|
|
var _unreadable_dirs: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
var scripts: PackedStringArray = []
|
|
_collect("res://", scripts)
|
|
scripts.sort() # deterministic ordering, so output diffs cleanly run to run
|
|
|
|
for path in scripts:
|
|
_checked += 1
|
|
# Return value deliberately discarded — see the header. Touching the
|
|
# file is the whole job; the engine prints the diagnosis.
|
|
ResourceLoader.load(path)
|
|
|
|
# stdout only. A non-zero verdict is the wrapper's to make, from stderr.
|
|
print("parse-sweep: opened %d scripts (%d unreadable dirs)" % [_checked, _unreadable_dirs])
|
|
quit(0)
|
|
|
|
|
|
## Recursive walk of the VIRTUAL filesystem. DirAccess rather than a shell
|
|
## `find`, because `res://` is what the engine can actually open — a source-tree
|
|
## walk would include files the engine ignores and miss the distinction.
|
|
func _collect(dir_path: String, out: PackedStringArray) -> void:
|
|
for prefix in SKIP_PREFIXES:
|
|
if dir_path.begins_with(prefix):
|
|
return
|
|
|
|
var dir := DirAccess.open(dir_path)
|
|
if dir == null:
|
|
_unreadable_dirs += 1
|
|
return
|
|
|
|
dir.list_dir_begin()
|
|
var name := dir.get_next()
|
|
while name != "":
|
|
if name.begins_with("."):
|
|
name = dir.get_next()
|
|
continue
|
|
var full := dir_path.path_join(name) if dir_path != "res://" else "res://" + name
|
|
if dir.current_is_dir():
|
|
_collect(full, out)
|
|
elif name.ends_with(".gd"):
|
|
out.append(full)
|
|
name = dir.get_next()
|
|
dir.list_dir_end()
|