Integra l'analisi statica nel processo, invece di cercare bug con essa

La mia scrittura di questo articolo è stata stimolata da un gran numero di materiali sull'analisi statica, che sempre più frequentemente attiravano la mia attenzione. Prima di tutto, è il blog PVS-studio, che promuove attivamente se stesso su Habr tramite recensioni sugli errori trovati dai loro strumenti in progetti open-source. Recentemente PVS-studio ha implementato il supporto per Java, e naturalmente, gli sviluppatori di IntelliJ IDEA, il cui analizzatore integrato è attualmente, probabilmente, il più avanzato per Java, non potevano rimanere indifferenti.

Leggendo tali recensioni si ha l'impressione che si tratti di un elisir magico: premi il pulsante e voilà, ecco la lista dei difetti davanti a te. Sembra che con il perfezionamento degli analizzatori, i bug verranno trovati automaticamente in numero sempre maggiore, e i prodotti scansionati da questi robot diventeranno sempre migliori, senza alcuno sforzo da parte nostra.

Ma non ci sono elisir magici. Vorrei parlare di ciò di cui di solito non si discute nei post del tipo "ecco cosa può trovare il nostro robot": cosa non possono fare gli analizzatori, qual è il loro ruolo reale e il loro posto nel processo di consegna del software, e come implementarli correttamente.

Integra l'analisi statica nel processo, invece di cercare bug con essa
Ruota dentata (fonte: Wikipedia).

Cosa non potranno mai fare gli analizzatori statici

Cosa significa, dal punto di vista pratico, analizzare il codice sorgente? Forniamo alcuni sorgenti in input e in breve tempo (molto più breve rispetto all'esecuzione dei test) otteniamo alcune informazioni sul nostro sistema. La limitazione fondamentale e matematicamente insormontabile consiste nel fatto che possiamo ottenere in questo modo solo una classe piuttosto ristretta di informazioni.

Il più famoso esempio di un problema irrisolvibile tramite analisi statica è il problema dell'arresto: è un teorema che dimostra che non è possibile sviluppare un algoritmo generale in grado di determinare, a partire dal codice sorgente di un programma, se esso entrerà in un ciclo infinito o terminerà in un tempo finito. L'estensione di questo teorema è il teorema di Rice, che afferma che, per qualsiasi proprietà non banale delle funzioni calcolabili, la definizione di se un programma arbitrario calcola una funzione con tale proprietà è un problema algoritmicamente non risolvibile. Ad esempio, è impossibile scrivere un analizzatore che, dato qualsiasi codice sorgente, determini se il programma analizzato è l'implementazione di un algoritmo che calcola, per esempio, il quadrato di un intero.

Pertanto, la funzionalità degli analizzatori statici presenta limitazioni insormontabili. Un analizzatore statico non sarà mai in grado di determinare in tutti i casi cose come, ad esempio, l'insorgere di un «null pointer exception» nei linguaggi che ammettono un valore null, o di identificare in tutti i casi l'insorgere di un «attribute not found» nei linguaggi con tipizzazione dinamica. Tutto ciò che il più sofisticato analizzatore statico può fare è evidenziare casi particolari, il cui numero rispetto a tutti i problemi possibili nel vostro codice sorgente è, senza esagerare, una goccia nell'oceano.

L'analisi statica non è ricerca di bug.

Da quanto detto sopra si conclude: l'analisi statica non è uno strumento per ridurre il numero di difetti in un programma. Oso affermare che, quando applicata per la prima volta al vostro progetto, essa troverà nel codice dei punti 'interessanti', ma molto probabilmente non troverà difetti che influiscono sulla qualità del lavoro del vostro programma.

Esempi di difetti trovati automaticamente dagli analizzatori sono impressionanti, ma non bisogna dimenticare che questi esempi sono stati trovati grazie alla scansione di un ampio set di grandi basi di codice. Allo stesso modo, gli hacker che hanno la possibilità di provare diverse semplici password su un gran numero di account, alla fine trovano quegli account con password semplici.

Questo significa che non bisogna applicare l'analisi statica? Certo che no! E per lo stesso motivo per cui è utile controllare ogni nuova password per vedere se finisce nella lista nera delle 'password semplici'.

L'analisi statica è più della semplice ricerca di bug.

In realtà, i compiti risolvibili tramite analisi sono molto più ampi. Infatti, in generale, l'analisi statica è qualsiasi verifica del sorgente, effettuata prima della loro esecuzione. Ecco alcune cose che si possono fare:

  • Verifica dello stile di codifica in senso ampio. Questa include sia il controllo della formattazione, sia la ricerca di utilizzi di parentesi vuote/extra, l'impostazione di soglie per metriche come il numero di righe / complessità ciclomatica del metodo, ecc. — insomma, tutto ciò che potenzialmente ostacola la leggibilità e la manutenibilità del codice. In Java, uno di questi strumenti è Checkstyle, in Python — flake8. I programmi di questa categoria sono comunemente chiamati "linters".
  • Non solo il codice eseguibile può essere analizzato. I file di risorse, come JSON, YAML, XML, .properties possono (e devono!) essere automaticamente verificati per la validità. È meglio sapere che a causa di qualche virgolette non abbinate la struttura del JSON è rotta in un fase precoce di verifica automatica del Pull Request, piuttosto che durante l'esecuzione dei test o a Run time? Gli strumenti appropriati sono disponibili: ad esempio, YAMLlint, JSONLint.
  • La compilazione (o il parsing per i linguaggi di programmazione dinamici) è anch'essa una forma di analisi statica. Di solito, i compilatori sono in grado di emettere avvisi che segnalano problemi con la qualità del codice sorgente e non dovrebbero essere ignorati.
  • A volte la compilazione non è solo la compilazione del codice eseguibile. Ad esempio, se hai della documentazione in formato AsciiDoctor, al momento della sua trasformazione in HTML/PDF, il processore AsciiDoctor (Maven plugin) può emettere avvisi, ad esempio, su collegamenti interni interrotti. E questo è un motivo valido per non accettare un Pull Request con modifiche alla documentazione.
  • Il controllo ortografico è anch'esso una forma di analisi statica. L'utilità aspell è in grado di controllare l'ortografia non solo nella documentazione, ma anche nei codici sorgente dei programmi (commenti e letterali) in vari linguaggi di programmazione, tra cui C/C++, Java e Python. Un errore di ortografia in un'interfaccia utente o nella documentazione è anch'esso un difetto!
  • I test di configurazione (per cosa siano — vedi questo e questo relazioni), sebbene eseguiti in un ambiente di esecuzione di test unitari come pytest, sono in effetti una forma di analisi statica, poiché non eseguono i codici sorgente durante il loro funzionamento.

Come possiamo vedere, la ricerca di bug in questo elenco assume un ruolo di minor importanza, mentre tutto il resto è accessibile utilizzando strumenti open source gratuiti.

Quali di questi tipi di analisi statica dovresti applicare al tuo progetto? Certamente, tutti, più ce ne sono — meglio è! L'importante è implementare tutto correttamente, di cui parleremo più avanti.

Il processo di fornitura come filtro multistrato e l'analisi statica come il primo stadio

La metafora classica dell'integrazione continua è un tubo (pipeline), attraverso il quale scorrono le modifiche — dalla modifica del codice sorgente alla consegna in produzione. La sequenza standard delle fasi di questo processo è:

  1. analisi statica
  2. compilazione
  3. test unitari
  4. test di integrazione
  5. test UI
  6. verifica manuale

Le modifiche respinte alla fase N non vengono trasferite alla fase N+1.

Perché proprio in questo modo e non in un altro? Nella parte del processo che riguarda i test, i tester conoscono la famosa piramide dei test.

Integra l'analisi statica nel processo, invece di cercare bug con essa
La piramide dei test. Fonte: su Habr. Martin Fowler.

Nella parte inferiore di questa piramide si trovano i test più facili da scrivere, che vengono eseguiti più rapidamente e non tendono a falsi positivi. Pertanto, dovrebbero esserci di più, dovrebbero coprire più codice e vengono eseguiti per primi. Nella parte superiore della piramide, invece, la situazione è opposta; quindi, il numero di test di integrazione e UI dovrebbe essere ridotto al minimo necessario. L'essere umano in questa catena è la risorsa più costosa, lenta e inaffidabile, quindi si trova alla fine e lavora solo se le fasi precedenti non hanno rilevato difetti. Tuttavia, le stesse linee guida vengono utilizzate per costruire il processo anche nelle parti non direttamente legate ai test!

Vorrei proporre un'analogia con un sistema di filtraggio dell'acqua a più stadi. In ingresso viene fornita acqua sporca (modifiche con difetti), mentre in uscita dobbiamo ottenere acqua pulita, con tutte le impurità indesiderate filtrate.

Integra l'analisi statica nel processo, invece di cercare bug con essa
Filtro multistrato. Fonte: Wikimedia Commons

Come è noto, i filtri di pulizia sono progettati in modo che ogni successivo stadio possa separare frazioni sempre più sottili di contaminanti. A tal fine, gli stadi di depurazione più grossolani hanno una maggiore capacità di flusso e un costo inferiore. Nella nostra analogia, questo significa che i quality gates in ingresso hanno una maggiore reattività, richiedono meno sforzi per essere attivati e sono di per sé più semplici da gestire — e sono strutturati seguendo proprio questa sequenza. Il ruolo dell'analisi statica, che, come ora comprendiamo, è capace di filtrare solo i difetti più evidenti, è quello di una griglia «separatrice» all'inizio del cascata di filtri.

L'analisi statica di per sé non migliora la qualità del prodotto finale, proprio come un separatore non rende potabile l'acqua. Eppure, nella sua totale integrazione con altri elementi della catena di produzione, la sua importanza è evidente. Anche se in un filtro a più stadi gli stadi di uscita possono potenzialmente catturare tutto ciò che gli stadi di ingresso rilevano, è chiaro a quali conseguenze conduce il tentativo di affidarsi esclusivamente a stadi di depurazione fine, senza quelli d'ingresso.

L'obiettivo del separatore è quello di alleggerire gli stadi successivi dalla cattura di difetti davvero grossolani. Ad esempio, almeno la persona che esegue la revisione del codice non dovrebbe distogliere l'attenzione da un codice mal formattato o dalla violazione delle norme di codifica stabilite (come parentesi superflue o ramificazioni troppo approfondite). I bug come NPE dovrebbero essere catturati dai test di unità, ma se prima del test l'analizzatore ci indica che un bug accadrà inevitabilmente, questo accelererà significativamente la sua correzione.

Ritengo sia ora chiaro perché l'analisi statica non migliora la qualità del prodotto se applicata in modo episodico e deve essere utilizzata costantemente per filtrare i cambiamenti con difetti gravi. La domanda se l'uso di un analizzatore statico migliorerà la qualità del tuo prodotto è approssimativamente equivalente a chiedere: «la qualità potabile dell'acqua prelevata da una fonte inquinata migliorerà se la si passerà attraverso un colino?»

Implementazione nel progetto legacy

Una domanda pratica importante: come integrare l'analisi statica nel processo di integrazione continua come "quality gate"? Nel caso dei test automatici, tutto è chiaro: c'è un insieme di test, la mancata esecuzione di uno di essi è un motivo sufficiente per considerare che la build non ha superato il quality gate. Tentare di stabilire un gate basato sui risultati dell'analisi statica fallisce: nel codice legacy ci sono troppe avvertenze dell'analisi, non vogliamo ignorarle completamente, ma non possiamo fermare la consegna del prodotto solo perché ci sono avvertenze dell'analizzatore.

Quando viene applicato per la prima volta, l'analizzatore fornisce un'enorme quantità di avvertenze, la stragrande maggioranza delle quali non ha nulla a che fare con il corretto funzionamento del prodotto. Correggere immediatamente tutte queste osservazioni non è possibile, e molti non è nemmeno necessario. Dopotutto, sappiamo che il nostro prodotto in generale funziona, anche prima dell'integrazione dell'analisi statica!

Di conseguenza, molti si limitano a un uso episodico dell'analisi statica o la usano solo in modalità informativa, quando durante la build viene semplicemente emesso un rapporto dell'analizzatore. Questo è equivalente all'assenza di qualsiasi analisi, perché se abbiamo già un certo numero di avvertenze, l'emergere di un'altra (per quanto grave) durante la modifica del codice rimane inosservato.

Sono noti i seguenti metodi per implementare quality gates:

  • Impostazione di un limite sul numero totale di avvertenze o sul numero di avvertenze diviso per il numero di righe di codice. Questo funziona male, poiché tale gate consente liberamente modifiche con nuovi difetti finché il loro limite non viene superato.
  • La registrazione, in un dato momento, di tutti gli avvisi precedenti nel codice come ignorati, e il rifiuto della compilazione in caso di nuovi avvisi. Questa funzionalità è fornita da PVS-studio e alcune risorse online, come Codacy. Non ho avuto esperienza con PVS-studio, ma per quanto riguarda la mia esperienza con Codacy, il loro problema principale è che determinare cosa sia un errore "vecchio" e cosa sia un errore "nuovo" è un algoritmo piuttosto complesso e non sempre corretto, soprattutto se i file subiscono modifiche radicali o vengono rinominati. A quanto ricordo, Codacy poteva trascurare nuovi avvisi in una pull request, e allo stesso tempo non permettere una pull request a causa di avvisi non relativi alle modifiche nel codice di quella PR.
  • A mio avviso, la soluzione più efficace è quella descritta nel libro Continuous Delivery il "metodo a cricchetto". L'idea principale è che ogni rilascio è caratterizzato dal numero di avvisi di analisi statica, e sono consentite solo le modifiche che non aumentano il numero complessivo di avvisi.

Cricchetto

Funziona in questo modo:

  1. Nella fase iniziale, viene registrato nei metadati del rilascio il numero di avvisi nel codice trovati dagli analizzatori. In questo modo, durante la compilazione del ramo principale, il tuo gestore di repository registra non solo "rilascio 7.0.2", ma "rilascio 7.0.2, contenente 100500 avvisi di Checkstyle". Se utilizzi un gestore di repository avanzato (come Artifactory), è facile mantenere tali metadati sul tuo rilascio.
  2. Ora ogni pull request, durante la compilazione, confronta il numero di avvisi ottenuti con quello attualmente presente nel rilascio. Se il PR porta a un aumento di questo numero, il codice non supera il controllo qualità dell'analisi statica. Se il numero di avvisi diminuisce o rimane invariato, allora passa.
  3. Nel prossimo rilascio, il numero di avvisi ricalcolato sarà nuovamente registrato nei metadati del rilascio.

Così, poco a poco, ma con costanza (come nel lavoro di un cricchetto), il numero di avvisi tenderà a zero. Certo, il sistema può essere ingannato inserendo un nuovo avviso ma correggendo uno già esistente. Questo è normale, poiché a lungo termine porta risultati: gli avvisi vengono corretti non singolarmente, ma generalmente in gruppo per un certo tipo, e tutti gli avvisi facilmente risolvibili vengono eliminati piuttosto rapidamente.

Questo grafico mostra il numero totale di avvisi di Checkstyle per sei mesi di lavoro di questo "cricchetto" su uno dei nostri progetti OpenSource. Il numero di avvisi è diminuito di un ordine di grandezza, è avvenuto in modo naturale, parallelamente allo sviluppo del prodotto!

Integra l'analisi statica nel processo, invece di cercare bug con essa

Uso una versione modificata di questo metodo, contando separatamente gli avvisi suddivisi per moduli del progetto e strumenti di analisi. Il file YAML dei metadati di compilazione che si forma appare all'incirca come segue:

celesta-sql:
  checkstyle: 434
  spotbugs: 45
celesta-core:
  checkstyle: 206
  spotbugs: 13
celesta-maven-plugin:
  checkstyle: 19
  spotbugs: 0
celesta-unit:
  checkstyle: 0
  spotbugs: 0

In qualsiasi sistema CI avanzato, il "cricchetto" può essere implementato per qualsiasi strumento di analisi statica, senza dipendere da plugin e strumenti esterni. Ogni analizzatore produce il proprio rapporto in formato testo semplice o XML, facilmente analizzabile. Resta solo da scrivere la logica necessaria nello script CI. È possibile vedere come è implementato nei nostri progetti open source basati su Jenkins e Artifactory qui o qui. Entrambi gli esempi dipendono dalla libreria ratchetlib: il metodo countWarnings() conta normalmente i tag xml nei file generati da Checkstyle e Spotbugs, mentre compareWarningMaps() implementa proprio quel cricchetto, lanciando un errore nel caso in cui il numero di avvisi in una delle categorie aumenti.

Un'interessante variante di implementazione del «riavvio» è possibile per l'analisi dell'ortografia dei commenti, dei letterali testuali e della documentazione utilizzando aspell. Come è noto, durante il controllo ortografico non tutte le parole sconosciute al dizionario standard sono errate; possono essere aggiunte al dizionario personale. Se si fa del dizionario personale parte del codice sorgente del progetto, allora il quality gate per l'ortografia può essere formulato in questo modo: eseguire aspell con il dizionario standard e quello personale. non deve trovare errori di ortografia.

Sull'importanza di fissare la versione dell'analizzatore

In conclusione, è importante notare quanto segue: qualunque sia il modo in cui introducete l'analisi nel vostro pipeline di distribuzione, la versione dell'analizzatore deve essere fissata. Se si permette un aggiornamento spontaneo dell'analizzatore, durante la costruzione di una nuova pull request possono «emergere» nuovi difetti, non legati a modifiche nel codice, ma al fatto che il nuovo analizzatore è semplicemente in grado di trovare più difetti, e questo interromperà il vostro processo di accettazione delle pull request. L'aggiornamento dell'analizzatore deve essere un'azione consapevole. Comunque, la fissazione rigida della versione di ciascun componente della build è un requisito necessario e un tema per una conversazione separata.

Conclusioni

  • L'analisi statica non troverà bug e non migliorerà la qualità del vostro prodotto come risultato di un'applicazione occasionale. Un effetto positivo sulla qualità è dato solo dalla sua applicazione costante nel processo di distribuzione.
  • La ricerca di bug non è affatto l'obiettivo principale dell'analisi; la stragrande maggioranza delle funzioni utili è disponibile in strumenti open source.
  • Implementate quality gates in base ai risultati dell'analisi statica sin dal primo stadio del pipeline di distribuzione, utilizzando il «riavvio» per il codice legacy.

Link

  1. Continuous Delivery
  2. A. Kudrjavcev: Analisi dei programmi: come capire se sei un buon programmatore relazione su diversi metodi di analisi del codice (non solo statici!)

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster