Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Claude Code per la code review automatica sulle PR

Ho usato Claude Code per fare code review automatica su ogni PR: ecco cosa ha trovato che i miei colleghi non avevano visto
Ho usato Claude Code per fare code review automatica su ogni PR: ecco cosa ha trovato che i miei colleghi non avevano visto
Link copiato negli appunti

Nell'esperimento che proponiamo in questa analisi, il team era composto da cinque sviluppatori su un progetto React con backend Node.js e PostgreSQL. Il processo di code review funzionava ragionevolmente bene, due approvazioni obbligatorie prima del merge, turni di review distribuiti, nessun senior che faceva il collo di bottiglia, ma con un problema ricorrente che chiunque abbia lavorato in un team di quella dimensione riconosce.

Le review tendevano infatti a concentrarsi sulla logica applicativa e sul rispetto delle convenzioni di team, e sistematicamente mancavano una categoria di problemi più sottili. Race condition, query N+1 nei layer ORM, import circolari che rallentavano il bundle, gestione degli errori asincroni con Promise non rejected correttamente. Non erano problemi frequenti, ma quando emergevano in produzione costavano molto di più di quanto sarebbe costato trovarli in fase di review.

Un esperimento con Claude Code

L'esperimento consisteva nell'integrare Claude Code nel processo di review tramite GitHub Actions. Parliamo di un workflow che si attivava su ogni pull request, passava il diff all'agente e pubblicava il risultato come commento sulla PR prima che i reviewer umani iniziassero la loro analisi. Non in sostituzione della review umana, ma come primo filtro automatico che cercava specificamente la categoria di problemi che i reviewer tendevano a saltare.

L'esperimento è durato quattro settimane, su quarantasette pull request. Quello che segue è un resoconto di cosa ha funzionato, cosa no, e cosa ha cambiato in modo permanente nel processo di review del team.

L'integrazione tecnica: come funzionava il workflow

Prima di entrare nel merito dei risultati, vale la pena descrivere l'architettura dell'integrazione perché le scelte tecniche hanno avuto un impatto diretto sulla qualità dell'output. Il workflow GitHub Actions che attivava la review era questo:

# .github/workflows/ai-review.yml
name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize]
jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Generate diff
        id: diff
        run: |
          git diff origin/${{ github.base_ref }}...HEAD \
            --unified=5 \
            --diff-filter=ACMR \
            -- '*.ts' '*.tsx' '*.js' '*.jsx' \
            > diff.txt
          echo "size=$(wc -c > $GITHUB_OUTPUT

      - name: Run AI Review
        if: steps.diff.outputs.size != '0'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          node scripts/ai-review.js \
            --diff diff.txt \
            --pr ${{ github.event.pull_request.number }} \
            --repo ${{ github.repository }}

Lo script ai-review.js leggeva il diff, costruiva il prompt per Claude, e pubblicava il risultato come commento sulla PR tramite l'API GitHub. La parte più importante, quella che ha richiesto più iterazioni, non era la chiamata all'API ma la costruzione del prompt. La versione finale, dopo due settimane di affinamento, aveva una caratteristica che si è rivelata decisiva: conteneva istruzioni esplicite non solo su cosa cercare, ma su cosa non segnalare.

const systemPrompt = `Sei un senior developer che fa code review su un
progetto Node.js/React con TypeScript. Il tuo compito è identificare
problemi tecnici reali nel diff, con focus su:

- Race condition e problemi di concorrenza nelle operazioni asincrone
- Query N+1 o accessi inefficienti al database nei layer ORM
- Import circolari che aumentano il bundle size inutilmente
- Promise non gestite correttamente o error handling incompleto
- Violazioni di type safety che TypeScript non intercetta a runtime

NON segnalare: preferenze di stile, convenzioni di naming,
suggerimenti di refactoring opzionali, o problemi già coperti da ESLint.
Ogni commento deve indicare file e riga esatti, spiegare perché è un
problema reale, e proporre una correzione concreta.
Se non trovi problemi reali, rispondi solo con: "Nessun problema rilevato".`

I pattern che l'agente ha trovato per primo

Nelle prime due settimane, Claude Code ha identificato su undici PR distinte problemi reali che i reviewer umani avevano approvato senza segnalare. Non tutti erano critici, ma nessuno era banale. I due casi più significativi meritano di essere documentati nel dettaglio perché mostrano la categoria di problemi in cui l'agente aggiunge valore reale.

La race condition nel custom hook React

Il primo problema è emerso su una PR che aggiungeva un custom hook per il fetching dei dati utente. Il codice era scritto in modo pulito, i reviewer avevano approvato la logica e i test passavano. Claude Code aveva prodotto questo commento sulla PR:

// useUserData.ts — versione originale
const useUserData = (userId: string) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId).then((user) => {
      setData(user);      // riga 18: aggiornamento dopo possibile unmount
      setLoading(false);
    });
  }, [userId]);
  return { data, loading };
};

Era un problema reale. In produzione, su connessioni lente con utenti che navigavano rapidamente tra le pagine, questo hook generava warning intermittenti che nessuno aveva ancora collegato a una causa specifica. La correzione con AbortController era quella corretta:

// useUserData.ts — dopo la correzione
const useUserData = (userId: string) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();
    fetchUser(userId, { signal: controller.signal })
      .then((user) => {
        setData(user);
        setLoading(false);
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          setLoading(false);
        }
      });

    return () => controller.abort();
  }, [userId]);
  return { data, loading };
};

Questo tipo di problema è esattamente quello che i reviewer umani tendono a non vedere perché richiede di ragionare sul ciclo di vita del componente nel contesto di una navigazione rapida. Uno scenario assente nei test unitari e che richiede uno sforzo mentale specifico per visualizzare durante una review. L'agente lo ha segnalato perché riconosce il pattern strutturale, non perché abbia simulato il comportamento dell'applicazione.

La query N+1 nel controller Express

Il secondo problema rilevante è emerso su una PR che aggiungeva un endpoint per esportare ordini con i relativi prodotti. Il codice usava Prisma in modo idiomatico per il resto del progetto, i test di integrazione passavano, e nessuno dei reviewer aveva sollevato osservazioni. Il database di test aveva tre record, quindi troppo pochi per rendere evidente qualsiasi problema di performance.

Il codice incriminato

// orderController.ts — versione con N+1
const getOrdersWithProducts = async (req: Request, res: Response) => {
  const orders = await prisma.order.findMany({
    where: { userId: req.user.id }
  });
  // una query separata per ogni ordine
  const ordersWithProducts = await Promise.all(
    orders.map(async (order) => ({
      ...order,
      products: await prisma.product.findMany({
        where: { orderId: order.id }
      })
    }))
  );

  res.json(ordersWithProducts);
};

La correzione riportava il codice al pattern corretto di Prisma:

// orderController.ts — con include Prisma
const getOrdersWithProducts = async (req: Request, res: Response) => {
  const orders = await prisma.order.findMany({
    where: { userId: req.user.id },
    include: {
      products: true
    }
  });
  res.json(orders);
};

Anche questo era un problema che i reviewer avevano visto senza segnalare. Il pattern con Promise.all sembra efficiente a prima vista perché le query vengono eseguite in parallelo. Parallele, ma comunque N query invece di una con JOIN.

i falsi positivi e il problema del contesto mancante

La terza settimana ha portato dati meno confortanti: su quattordici PR analizzate, sei commenti di Claude Code erano falsi positivi. In genere problemi segnalati che non erano problemi nel contesto specifico del progetto. Analizzare i falsi positivi è stato più istruttivo che analizzare i veri positivi, perché ha chiarito dove i limiti strutturali dell'agente si manifestano concretamente.

Il falso positivo sull'import circolare che non era circolare. Claude Code aveva segnalato un import circolare tra due moduli. Guardando il diff in isolamento la segnalazione aveva senso: il modulo A importava da B, e nel diff era visibile che B importava da A. Quello che l'agente non poteva vedere era che quell'import di B da A era già presente nel codebase da mesi, e che il bundler (configurato con un plugin specifico) lo gestiva senza problemi. L'agente aveva ragionato sul diff senza il contesto dell'intero grafo delle dipendenze del progetto.

Il falso positivo sull'error handling intenzionale

Claude Code aveva segnalato una Promise non gestita in un file di utility. Il codice era questo:

// utils/cache.ts
export const warmupCache = () => {
  fetchCriticalData().then(storeToCache);
  // nessun .catch() — segnalato dall'agente come errore
};

La segnalazione era tecnicamente corretta in senso generale: una Promise senza .catch() inghiotte gli errori silenziosamente. Ma nel contesto del progetto, warmupCache veniva chiamata al bootstrap dell'applicazione in un contesto fire-and-forget intenzionale. Il riscaldamento della cache era un'ottimizzazione opzionale il cui fallimento non doveva bloccare l'avvio. La gestione dell'errore era poi assente per design.

Questo è il tipo di falso positivo più insidioso. L'agente ha ragione in senso tecnico generale, ma torto nel contesto specifico. Un reviewer inesperto che seguisse il commento avrebbe aggiunto un error handler che avrebbe cambiato silenziosamente il comportamento atteso dell'applicazione.

Il prompt affinato e i risultati finali

Alla fine delle quattro settimane, il dato grezzo era che su quarantasette PR Claude Code aveva prodotto ventitré commenti classificati come problema reale confermato. Quindi circa la metà del totale con l'altra metà distribuita tra falsi positivi e osservazioni corrette ma irrilevanti nel contesto. Dei problemi confermati, undici erano stati mancati dai reviewer umani nella stessa PR.

Undici problemi reali su quarantasette PR in quattro settimane. Non è una percentuale enorme, ma sul tipo di problemi intercettati (race condition, query N+1, Promise non gestite) la media storica del team era di trovarne uno ogni due o tre sprint, spesso già in produzione. Il valore non era nel volume ma nella categoria.

La modifica più importante al processo era stata al prompt stesso. Invece di categorie generiche, la versione finale conteneva pattern specifici con esempi dal codebase reale e un elenco esplicito delle assunzioni da non fare:

// estratto dal prompt affinato dopo quattro settimane
`Pattern specifici da cercare in questo progetto:

1. useEffect con fetch asincrono senza cleanup function o AbortController
2. Loop su array con await dentro (pattern N+1 con Prisma o query dirette)
3. .then() senza .catch() su chiamate che non siano fire-and-forget
(le funzioni fire-and-forget sono documentate con il commento
// intentional: fire-and-forget)
4. Import da '@/store' dentro file sotto '@/components/ui'
(viola la dipendenza unidirezionale stabilita nell'architettura)
5. setState chiamato dopo await senza verifica che il componente
sia ancora montato

Assunzioni da NON fare:
- Non segnalare import circolari tra moduli sotto '@/lib/cache'
(gestiti dal plugin webpack configurato in webpack.config.ts)
- Non segnalare Promise non gestite in file con suffisso .warmup.ts`

Questa specificità ha ridotto i falsi positivi dal 48% al 19% nell'ultima settimana. Non è un numero definitivo perché un prompt con pattern specifici del progetto deve essere aggiornato ogni volta che l'architettura evolve. È però abbastanza basso da rendere i commenti dell'agente utili senza generare il rumore che insegna ai reviewer a ignorarli.

Quello che l'agente non ha mai trovato

È importante essere espliciti sui confini reali di questa integrazione, perché definiscono cosa rimane interamente territorio della review umana indipendentemente da quanto si affina il prompt.

Claude Code non ha mai identificato un problema di logica di business. Ha trovato race condition, query inefficienti, Promise mal gestite. Tutti problemi tecnici con una struttura riconoscibile. Ma non ha mai segnalato che un calcolo era sbagliato rispetto ai requisiti, che una condizione if gestiva un caso limite in modo contrario a quello atteso, o che un'API restituiva dati corretti tecnicamente ma semanticamente sbagliati rispetto al dominio applicativo. Quei problemi richiedono di capire cosa il codice dovrebbe fare, una distinzione che rimane nel dominio della review umana.

Non ha mai segnalato una vulnerabilità di sicurezza non banale. Pattern ovvi come concatenazione diretta di input in una query SQL o token esposti nei log sì, ma vulnerabilità che richiedono di ragionare sul flusso di autenticazione nell'intera applicazione, o timing attack su confronti di stringhe, o problemi di autorizzazione che dipendono dalla semantica del dominio. La sicurezza applicativa profonda rimane territorio umano.

E non ha mai trovato un problema che richiedeva di confrontare due PR diverse, come regressioni introdotte da una modifica che ripristinava accidentalmente codice rimosso in una PR precedente, o comportamenti che diventavano problematici solo in combinazione con modifiche fatte in un altro branch. Il diff di una singola PR è il limite del contesto, e non c'è modo di estenderlo senza cambiare radicalmente l'architettura dell'integrazione.

Il rischio della delega cognitiva

C'è un effetto collaterale di questa integrazione che il team ha discusso esplicitamente e che vale la pena nominare con chiarezza. La presenza di commenti automatici sulla PR rischia di creare nei reviewer umani una forma di delega cognitiva. Se l'AI non ha segnalato nulla, il codice è probabilmente a posto. Quella conclusione è sbagliata perché l'agente copre al massimo il 20% della superficie di una review seria, ma è psicologicamente plausibile, specialmente quando si è sotto pressione di delivery.

Gestire questo rischio ha richiesto due scelte esplicite nel processo. I commenti dell'agente vengono pubblicati in un thread collassato di default, etichettati chiaramente come output automatico da validare, in modo che i reviewer li leggano consapevolmente come input e non come segnalazioni autorevoli. Il team ha poi documentato in modo esplicito nel README del progetto cosa l'agente fa e cosa non fa, in modo che i nuovi membri non sviluppino aspettative sbagliate sullo strumento.

Ti consigliamo anche