gdscript-assistant / validate.py
vivekchakraverty's picture
Auto-correct EVERY broken GDScript block in place (capped at MAX_FIX_PASSES)
635e6fb
"""Validate GDScript produced by the model using gdtoolkit (Scony's parser).
Pure-Python, CPU-only, Godot-4 (GDScript 2.0). Checks SYNTAX (gdparse) and STYLE
(gdlint); it does NOT check runtime/scene semantics (node paths, types against a
real project) — that needs the Godot engine.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
_FENCE_RE = re.compile(r"```(?:gdscript|gd|godot)?\s*\n(.*?)```", re.S | re.I)
@dataclass
class BlockResult:
code: str
ok: bool # parses (valid syntax)
error: str = "" # syntax error message (if any)
lint: list[str] = field(default_factory=list) # style/lint warnings
formatted: str = "" # gdformat output (if available)
def extract_gdscript_blocks(text: str) -> list[str]:
"""Pull fenced GDScript blocks from a model answer."""
blocks = [m.group(1).strip() for m in _FENCE_RE.finditer(text or "")]
return [b for b in blocks if b]
# ---------------------------------------------------------------------------
# gdtoolkit wrappers (imported lazily so the module loads even if absent)
# ---------------------------------------------------------------------------
def _parse(code: str) -> tuple[bool, str]:
try:
from gdtoolkit.parser import parser
except Exception as e: # gdtoolkit not installed
return True, f"(parser unavailable: {e})"
try:
parser.parse(code, gather_metadata=False)
return True, ""
except TypeError:
# older/newer signature without gather_metadata
try:
parser.parse(code)
return True, ""
except Exception as e:
return False, _fmt_err(e)
except Exception as e:
return False, _fmt_err(e)
def _fmt_err(e: Exception) -> str:
line = getattr(e, "line", None)
col = getattr(e, "column", None)
msg = str(e).strip().splitlines()[0] if str(e).strip() else type(e).__name__
if line is not None:
return f"line {line}:{col or 0}: {msg}"
return msg
def _lint(code: str) -> list[str]:
try:
from gdtoolkit.linter import lint_code
except Exception:
return []
try:
problems = lint_code(code)
except Exception:
return []
out = []
for p in problems:
line = getattr(p, "line", "?")
name = getattr(p, "name", "")
desc = getattr(p, "description", str(p))
out.append(f"line {line}: {desc}" + (f" [{name}]" if name else ""))
return out
def _format(code: str) -> str:
try:
from gdtoolkit.formatter import format_code
return format_code(code, max_line_length=100)
except Exception:
return ""
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def validate_code(code: str) -> BlockResult:
ok, err = _parse(code)
return BlockResult(
code=code, ok=ok, error=err,
lint=_lint(code) if ok else [],
formatted=_format(code) if ok else "",
)
def validate_answer(answer: str) -> list[BlockResult]:
return [validate_code(b) for b in extract_gdscript_blocks(answer)]
def gdscript_block_spans(text: str) -> list[tuple[str, int, int]]:
"""Each fenced GDScript block as (stripped_code, match_start, match_end), in
document order. The span covers the whole ```...``` fence so a caller can
splice a corrected block back in place of the original."""
out: list[tuple[str, int, int]] = []
for m in _FENCE_RE.finditer(text or ""):
code = m.group(1).strip()
if code:
out.append((code, m.start(), m.end()))
return out
def first_gdscript_block(text: str) -> str:
"""First fenced GDScript block (stripped), or '' if none — used to pull the
corrected code out of a fix generation."""
blocks = extract_gdscript_blocks(text)
return blocks[0] if blocks else ""
def render_report(results: list[BlockResult]) -> str:
"""Markdown summary for the UI."""
if not results:
return "_No GDScript code blocks detected to validate._"
lines = []
for i, r in enumerate(results, 1):
if r.ok:
badge = "✅ **valid GDScript** (syntax OK)"
if r.lint:
badge += f" · {len(r.lint)} lint note(s)"
else:
badge = f"❌ **syntax error** — {r.error}"
lines.append(f"**Block {i}:** {badge}")
for w in r.lint[:5]:
lines.append(f"- ⚠ {w}")
return "\n".join(lines)
def first_syntax_error(results: list[BlockResult]) -> tuple[str, str] | None:
"""Return (code, error) of the first block that failed to parse, else None."""
for r in results:
if not r.ok:
return r.code, r.error
return None
if __name__ == "__main__":
good = "extends Node\n\n@export var speed: float = 5.0\n\nfunc _ready() -> void:\n\tprint(speed)\n"
bad = "extends Node\n\nfunc _ready(\n\tprint('oops')\n"
for label, code in (("GOOD", good), ("BAD", bad)):
r = validate_code(code)
print(f"== {label} == ok={r.ok} error={r.error!r} lint={r.lint}")