""" ast_check.py — P45: AST syntax check pre-esecuzione codice. Usa ast.parse() stdlib Python (zero dipendenze) per Python. Per JS/TS: esprima se disponibile, fallback balanced-bracket heuristic. Integra P17-B1: sostituisce routing regex E2B per validazione sintattica. Utilizzo: from tools.ast_check import check_code_syntax result = check_code_syntax(code, "python") if not result.ok: return f"SyntaxError: {result.error}" """ import ast import re import logging from dataclasses import dataclass _logger = logging.getLogger("tools.ast_check") @dataclass class SyntaxCheckResult: ok: bool error: str | None = None language: str = "" line: int | None = None col: int | None = None def __str__(self): if self.ok: return f"[AST] {self.language}: OK" loc = f" (line {self.line})" if self.line else "" return f"[AST] {self.language}{loc}: {self.error}" def check_python_syntax(code): """AST check Python con ast.parse() stdlib. Zero dipendenze.""" try: ast.parse(code, mode="exec") return SyntaxCheckResult(ok=True, language="python") except SyntaxError as e: return SyntaxCheckResult( ok=False, language="python", error=f"{type(e).__name__}: {e.msg}", line=e.lineno, col=e.offset, ) except Exception as e: return SyntaxCheckResult(ok=False, language="python", error=str(e)) def _balanced_brackets(code): """Heuristica bilanciamento parentesi — fallback JS senza esprima.""" stack = [] pairs = {")": "(", "}": "{", "]": "["} in_str = None i = 0 while i < len(code): ch = code[i] if in_str: if ch == "\\" and i + 1 < len(code): i += 2 continue if ch == in_str: in_str = None elif ch in ('"', "'", '`'): in_str = ch elif ch in "({[": stack.append(ch) elif ch in ")}]": if not stack or stack[-1] != pairs[ch]: return SyntaxCheckResult(ok=False, language="js/ts", error=f"Unmatched '{ch}' at pos {i}") stack.pop() i += 1 if stack: return SyntaxCheckResult(ok=False, language="js/ts", error=f"Unclosed '{stack[-1]}'") return SyntaxCheckResult(ok=True, language="js/ts") def check_js_syntax(code): """AST check JS/TS: esprima se disponibile, fallback balanced-bracket.""" try: import esprima esprima.parseScript(code, tolerant=False) return SyntaxCheckResult(ok=True, language="js/ts") except ImportError: pass except Exception as e: msg = str(e) m = re.search(r"Line (\d+)", msg) line = int(m.group(1)) if m else None return SyntaxCheckResult(ok=False, language="js/ts", error=msg[:200], line=line) return _balanced_brackets(code) _LANG_MAP = { "python": "python", "py": "python", "javascript": "js", "js": "js", "jsx": "js", "typescript": "js", "ts": "js", "tsx": "js", } def check_code_syntax(code, language): """ Dispatch AST check per linguaggio. Fail-open su linguaggi non supportati. Returns SyntaxCheckResult(ok, error, language, line, col). """ if not code or not code.strip(): return SyntaxCheckResult(ok=True, language=language) lang = _LANG_MAP.get(language.lower().strip(), "") if lang == "python": result = check_python_syntax(code) elif lang == "js": result = check_js_syntax(code) else: return SyntaxCheckResult(ok=True, language=language) if not result.ok: _logger.warning("[AST-P45] %s syntax error: %s", language, result.error) return result def extract_code_blocks(text): """Estrae blocchi codice da markdown. Ritorna list[(language, code)].""" blocks = [] pattern = re.compile(r'```(\w+)?\n([\s\S]*?)```') for m in pattern.finditer(text): lang = (m.group(1) or "").strip() code = m.group(2) blocks.append((lang, code)) return blocks def check_all_code_blocks(text): """Verifica tutti i blocchi codice in markdown. Ritorna solo errori.""" errors = [] for lang, code in extract_code_blocks(text): result = check_code_syntax(code, lang) if not result.ok: errors.append(result) return errors