Lab

Analisi statica con AST: trovare i pattern ridondanti dell'IA in Python

5 min di lettura
Laptop chiuso su scrivania scura accanto a foglio di codice Python con cerchi e bandierine terracotta
Windows macOS Linux
Foto: Alex Lunalbi · ai riparo · Proprietario

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 da eval() 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:

  1. Except ampi + pass — a mano: logga, restringi il tipo, o propaga. Non lasciare silenzi su I/O e auth.
  2. if True / if False — integra o cancella il blocco.
  3. Import duplicati — ruff check --fix (pipeline madre).
  4. 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

  1. Prima di Ruff/Rope, mappa gli indizi AST (except silenziosi, import doppi, funzioni lunghe, if True).
  2. Script in stdlib, sola lettura, offline.
  3. Poi passa alla pipeline di refactoring e alla review umana.

Letture opzionali

Alex Lunalbi

Alex Lunalbi

Security & privacy

Dona su PayPal

Scrivo di sicurezza digitale pratica e codice IA. Su ai riparo ogni procedura è verificata su macchine reali.

Tag
  • python,
  • ast,
  • vibe coding,
  • analisi statica