File size: 4,388 Bytes
28a08e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""
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