bifroest/app/changes/checks.py
2026-10-27 08:34:17 +01:00

87 lines
2.7 KiB
Python

"""Statische Prüfung beim Commit, ohne Code auszuführen (→ Phase 3).
Python (ast), JSON, TOML, XML (Standardbibliothek) und YAML (PyYAML). Fehler sind
Warnungen: Der Commit entsteht trotzdem, die KI bekommt sie zurück, und sie stehen
am Commit in der Ansicht „Änderungen“.
"""
import ast
import json
import tomllib
import warnings as py_warnings
import xml.etree.ElementTree as ElementTree
from pathlib import PurePosixPath
import yaml
MAX_WARNINGS = 20
def python(text: str) -> str | None:
try:
with py_warnings.catch_warnings():
py_warnings.simplefilter("ignore") # z. B. SyntaxWarning zu Escape-Sequenzen
ast.parse(text)
except SyntaxError as e:
return f"{e.lineno or '?'}: Python-Syntaxfehler: {e.msg}"
return None
def json_(text: str) -> str | None:
try:
json.loads(text)
except ValueError as e:
return f"{getattr(e, 'lineno', '?')}: ungültiges JSON: {getattr(e, 'msg', str(e))}"
return None
def toml(text: str) -> str | None:
try:
tomllib.loads(text)
except tomllib.TOMLDecodeError as e:
return f"?: ungültiges TOML: {e}"
return None
def xml(text: str) -> str | None:
# ElementTree löst keine externen Entitäten auf; Expat ab 2.4 begrenzt Entitäts-Expansion
try:
ElementTree.fromstring(text)
except ElementTree.ParseError as e:
return f"{e.position[0]}: ungültiges XML: {e}"
return None
def yaml_(text: str) -> str | None:
# Nur die Struktur prüfen (compose): keine Objekte bauen, unbekannte Tags wie !Ref (CloudFormation) erlaubt;
# mehrere Dokumente (---) sind zulässig
loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
try:
for _ in yaml.compose_all(text, Loader=loader):
pass
except yaml.YAMLError as e:
mark = getattr(e, "problem_mark", None)
problem = getattr(e, "problem", None) or str(e).splitlines()[0]
return f"{mark.line + 1 if mark else '?'}: ungültiges YAML: {problem}"
return None
CHECKS = {".py": python, ".pyi": python, ".json": json_, ".ipynb": json_, ".toml": toml, ".xml": xml,
".svg": xml, ".xsd": xml, ".pom": xml, ".yaml": yaml_, ".yml": yaml_}
def check(files: dict[str, bytes]) -> list[str]:
found = []
for path, content in files.items():
checker = CHECKS.get(PurePosixPath(path).suffix.lower())
if checker is None:
continue
try:
text = content.decode("utf-8")
except UnicodeDecodeError:
found.append(f"{path}: kein gültiges UTF-8")
continue
problem = checker(text)
if problem:
found.append(f"{path}:{problem}")
return found[:MAX_WARNINGS]