Analisi statica con AST: trovare i pattern ridondanti dell'IA in Python
Script Python · versione 1.0.0 · verificato
ai_code_detector.py
Script autonomo che gira solo sul tuo computer. Nessun upload, nessuna telemetria.
- Formato
- Script Python (.py)
- Dimensione
- 3,8 KB
- Python
- 3.9 o superiore
- Licenza
- MIT
- Richiede
- Nessuna
- Rete
- Offline
Legge file locali. Non crea, non rinomina e non cancella nulla. Il codice è leggibile riga per riga nel file scaricato.
Verifica integrità
SHA-256 di ai_code_detector.py
f235a6d9da0fc090f9f9fca6e85b8bd8d393ef7e9d4026bf70634f902d240712 Calcola lo SHA-256 del file scaricato e confrontalo con questo: Confronta hash · Come si verifica
Usa l'AST di Python per rilevare indizi di codice generato male dall'IA: except vuoti, funzioni giganti e import doppi. Script locale senza dipendenze.
Chi fa vibe coding — delegando gran parte del lavoro a Cursor, Copilot o ChatGPT — si scontra presto con codice che funziona ma è strutturalmente pigro: eccezioni mascherate, import ripetuti, funzioni chilometriche.
Nella guida madre sul refactoring con AST, Ruff e Rope pulisci e rinomini in modo deterministico. Prima, però, serve una mappa tattica: dove l’IA ha lavorato peggio.
Qui trovi uno script Python puro (solo libreria standard) che usa l’AST per segnalare indizi tipici da spaghetti LLM. Lo scarichi dalla scheda download in cima alla pagina o dal repository GitHub, oppure lo copi dal blocco qui sotto: sola lettura, da lanciare sul file appena accettato. Non è un linter e non sostituisce Ruff.
ℹ️ Nota: Questi pattern non sono la prova che il codice sia «scritto dall’IA». Anche gli umani lasciano
except: pass. Lo script è un evidenziatore, non un giudice — e non sostituisce Ruff né una review umana.
I pattern «IA-like» da cercare
I modelli tendono a ottimizzare per «far partire lo script» al primo tentativo. Da lì nascono scorciatoie ripetute.
| Pattern rilevato | Priorità | Perché compare e cosa comporta |
|---|---|---|
Except ampio + pass
except Exception: pass / except:
|
Alta | Maschera errori. Il debug diventa cieco; in produzione **può** nascondere fallimenti su file, rete o auth (vedi anche le [7 falle](/blog/7-falle-sicurezza-codice-ia/)). |
| Import duplicati Stesso modulo su righe diverse | Media | Tipico di merge da chat / Accept parziali. Rumore di lettura; Ruff lo sistema in un attimo. |
| Funzioni monstre > 50 righe (soglia configurabile) | Media | I LLM **spesso** non spezzano la logica. Difficili da testare; candidate a extract / split moduli. |
Condizioni statiche
if True: / if False:
|
Alta | Rimasugli di «disattiva questo pezzo». Da ripulire a mano. |
Come funziona l’AST (in sintesi)
Il modulo ast prende il testo del sorgente e costruisce un albero di nodi (import, try, def, if, …). Tu cammini sull’albero con ast.NodeVisitor senza far girare il programma.
🚨 Sicurezza dell’analisi:
ast.parse()non esegue il codice. Diverso daeval()o dall’import di moduli non fidati. Puoi scansionare anche file sospetti in sola lettura — restano altri rischi (es. aprire zip malevoli); qui parliamo solo del parse AST.
Lo script di rilevamento
Download: ai_code_detector.py
SHA-256: f235a6d9da0fc090f9f9fca6e85b8bd8d393ef7e9d4026bf70634f902d240712
Repository: lunoffice/analisi-statica-ast-pattern-ridondanti-ia (MIT, release v1.0.0)
Se preferisci incollarlo a mano, crea ai_code_detector.py (Python 3.9+, nessun pip install). Il file scaricabile è identico a questo blocco; per il controllo dello SHA-256 vedi la guida verificare un download con SHA-256.
import ast
import argparse
from pathlib import Path
class AILikePatternVisitor(ast.NodeVisitor):
def __init__(self, filename: str):
self.filename = filename
self.issues: list[str] = []
self.seen_imports: set[str] = set()
def report(self, line: int, message: str) -> None:
self.issues.append(f"{self.filename}:{line} -> {message}")
def visit_Import(self, node: ast.Import) -> None:
for alias in node.names:
if alias.name in self.seen_imports:
self.report(node.lineno, f"Import duplicato trovato: '{alias.name}'")
self.seen_imports.add(alias.name)
self.generic_visit(node)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
if node.module:
if node.module in self.seen_imports:
self.report(
node.lineno,
f"Import (from) duplicato trovato: '{node.module}'",
)
self.seen_imports.add(node.module)
self.generic_visit(node)
def visit_Try(self, node: ast.Try) -> None:
for handler in node.handlers:
is_broad = handler.type is None or (
isinstance(handler.type, ast.Name) and handler.type.id == "Exception"
)
if is_broad and len(handler.body) == 1 and isinstance(
handler.body[0], ast.Pass
):
self.report(
handler.lineno,
"Segnalazione: 'except' ampio con solo pass (silenzioso)",
)
self.generic_visit(node)
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
if node.end_lineno is not None:
length = node.end_lineno - node.lineno
if length > 50:
self.report(
node.lineno,
f"Funzione monstre '{node.name}' ({length} righe). Valuta il refactoring.",
)
self.generic_visit(node)
def visit_If(self, node: ast.If) -> None:
if isinstance(node.test, ast.Constant) and isinstance(node.test.value, bool):
self.report(
node.lineno,
f"Condizione statica trovata (if {node.test.value}:). Rimasuglio di debug?",
)
self.generic_visit(node)
def analyze_file(filepath: Path) -> list[str]:
try:
content = filepath.read_text(encoding="utf-8")
tree = ast.parse(content)
visitor = AILikePatternVisitor(str(filepath))
visitor.visit(tree)
return visitor.issues
except SyntaxError as e:
return [
f"{filepath} -> Errore di sintassi alla riga {e.lineno}. "
"L'AST richiede codice Python valido."
]
except OSError as e:
return [f"{filepath} -> Errore di lettura: {e}"]
def main() -> None:
parser = argparse.ArgumentParser(
description="Scansiona script Python per pattern tipici da LLM."
)
parser.add_argument("path", help="File o cartella da scansionare")
args = parser.parse_args()
target = Path(args.path)
if target.is_file() and target.suffix == ".py":
files = [target]
elif target.is_dir():
files = sorted(target.rglob("*.py"))
else:
print("Percorso non valido o nessun file Python trovato.")
return
# Evita di segnalare lo scanner stesso se sta nella cartella
files = [f for f in files if f.name != "ai_code_detector.py"]
total: list[str] = []
for py_file in files:
total.extend(analyze_file(py_file))
print(f"\n--- Scansione completata su {len(files)} file ---")
if not total:
print("Nessun pattern sospetto rilevato (con le euristiche attuali).")
else:
for issue in total:
print(issue)
print(f"\nTotale segnalazioni: {len(total)}")
if __name__ == "__main__":
main()
Lab: demo.py e output atteso
Crea demo.py. Importante: il padding deve stare dentro la funzione, altrimenti end_lineno non supera la soglia.
import os
import sys
import os # Import duplicato
def calcola_tutto():
# Funzione inutilmente lunga (simulazione lab)
x = 1
y = 2
p01 = 1
p02 = 2
p03 = 3
p04 = 4
p05 = 5
p06 = 6
p07 = 7
p08 = 8
p09 = 9
p10 = 10
p11 = 11
p12 = 12
p13 = 13
p14 = 14
p15 = 15
p16 = 16
p17 = 17
p18 = 18
p19 = 19
p20 = 20
p21 = 21
p22 = 22
p23 = 23
p24 = 24
p25 = 25
p26 = 26
p27 = 27
p28 = 28
p29 = 29
p30 = 30
p31 = 31
p32 = 32
p33 = 33
p34 = 34
p35 = 35
p36 = 36
p37 = 37
p38 = 38
p39 = 39
p40 = 40
p41 = 41
p42 = 42
p43 = 43
p44 = 44
if True:
print("Debug attivo")
try:
risultato = x / 0
except Exception:
pass # errore silenziato
python ai_code_detector.py demo.py
Output tipico (numeri di riga possono variare di 1–2 in base a blank line):
--- Scansione completata su 1 file ---
demo.py:3 -> Import duplicato trovato: 'os'
demo.py:6 -> Funzione monstre 'calcola_tutto' (53 righe). Valuta il refactoring.
demo.py:54 -> Condizione statica trovata (if True:). Rimasuglio di debug?
demo.py:58 -> Segnalazione: 'except' ampio con solo pass (silenzioso)
Totale segnalazioni: 4
Su una cartella: python ai_code_detector.py mio_progetto/ (recursive *.py).
Dopo il report: priorità
Lo script non modifica i file (solo read_text). Ordine pratico:
- Except ampi + pass — a mano: logga, restringi il tipo, o propaga. Non lasciare silenzi su I/O e auth.
if True/if False— integra o cancella il blocco.- Import duplicati —
ruff check --fix(pipeline madre). - Funzioni monstre — extract / split; prompt mirato all’IA dopo aver mappato i simboli, non «pulisci tutto».
Poi: Ruff/Rope e i 15 minuti di review.
Domande Frequenti (FAQ)
Cos’è l’analisi statica con AST?
Ispezionare il codice senza eseguirlo: Python valida la sintassi e costruisce la mappa dei nodi.
Che differenza c’è con Ruff?
Ruff è un linter/formattatore professionale (centinaia di regole, fix automatici). Questo script è una sonda personalizzabile sui pattern «LLM-like». Usali in sequenza: sonda → Ruff → review.
Lo script modifica i file?
No. Solo lettura e report a terminale.
Serve pip install?
No: ast, argparse, pathlib.
Funziona su Windows, macOS e Linux?
Sì. pathlib gestisce i path.
Ricevo SyntaxError / riga segnalata
Il file non è Python valido (spesso fence markdown restati nel .py). Sistema la sintassi, poi rilancia.
Posso cambiare la soglia delle 50 righe?
Sì: modifica il 50 in visit_FunctionDef. Alza su script CLI monolitici, abbassa su librerie piccole.
C’è un repository GitHub?
Sì: lunoffice/analisi-statica-ast-pattern-ridondanti-ia, con README, licenza MIT e release v1.0.0 che contiene ai_code_detector.py. Lo SHA-256 è lo stesso della scheda download di questa pagina.
Falsi positivi?
Sì, possono esserci (es. except Exception: pass voluto in uno shutdown). Tratta ogni riga come da leggere, non come bug certificato.
In sintesi
- Prima di Ruff/Rope, mappa gli indizi AST (except silenziosi, import doppi, funzioni lunghe,
if True). - Script in stdlib, sola lettura, offline.
- Poi passa alla pipeline di refactoring e alla review umana.