"""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]