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

La mia decisione di scrivere questo articolo è stata ispirata dalla grande quantità di materiale sull'analisi statica che ho notato sempre più spesso. Innanzitutto, si tratta del blog PVS-studio, che promuove attivamente se stesso su Habr attraverso recensioni di errori trovati dal loro strumento 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.

Quando leggi tali recensioni, si ha l'impressione di avere a che fare con una pozione magica: premi un pulsante e voilà — un elenco di difetti davanti ai tuoi occhi. Sembra che con il miglioramento degli analizzatori, verranno trovati automaticamente sempre più bug, e i prodotti scansionati da questi robot diventeranno sempre migliori, senza alcuno sforzo da parte nostra.

Ma non esistono 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 e la loro posizione nel processo di fornitura del software e come implementarli correttamente.

Integra l'analisi statica nel tuo processo, invece di cercare bug con essa
Il riduttore (fonte: wikipedia).

Cosa non potranno mai fare gli analizzatori statici

Che cos'è, in termini pratici, l'analisi del codice sorgente? Forniamo alcuni sorgenti in input e in breve tempo (molto meno del tempo necessario per eseguire i test) otteniamo alcune informazioni sul nostro sistema. Il vincolo principale e matematicamente ineludibile è che possiamo ottenere solo una classe abbastanza ristretta di informazioni in questo modo.

Il più famoso esempio di problema non risolvibile tramite analisi statica è il problema dell'arresto: è un teorema che dimostra che non è possibile sviluppare un algoritmo generale che, dati il codice sorgente di un programma, possa determinare se 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, definire se un programma arbitrario calcola una funzione con tale proprietà è un problema algoritmicamente irrisolvibile. Ad esempio, è impossibile scrivere un analizzatore che, dato un codice sorgente qualsiasi, possa determinare se il programma analizzato è un'implementazione di un algoritmo che calcola, ad esempio, il quadrato di un numero intero.

Pertanto, la funzionalità degli analizzatori statici ha limiti ineludibili. Un analizzatore statico non potrà mai determinare in tutti i casi situazioni come, ad esempio, il verificarsi di un 'null pointer exception' in linguaggi che consentono il valore null, o nei casi in cui si verifica un 'attribute not found' in linguaggi con tipizzazione dinamica. Ciò che il più sofisticato degli analizzatori statici può fare è evidenziare casi particolari, il cui numero rispetto a tutti i possibili problemi con il tuo codice sorgente è, senza esagerare, una goccia nel mare.

L'analisi statica non è ricerca di bug

Dalle informazioni fornite, si può concludere che l'analisi statica non è un mezzo per ridurre il numero di difetti nel software. Mi permetto di affermare che, se applicata per la prima volta al tuo progetto, essa individuerà nel codice dei punti "interessanti", ma probabilmente non troverà difetti che influenzino la qualità del funzionamento del tuo programma.

Gli esempi di difetti automaticamente trovati dagli analizzatori sono impressionanti, ma non bisogna dimenticare che questi esempi sono stati scoperti tramite la scansione di un ampio set di grandi basi di codice. Allo stesso modo, i ladri informatici, che possono testare diverse semplici password su un gran numero di account, riescono infine a individuare quegli account che utilizzano una password semplice.

Ciò significa che l'analisi statica non deve essere applicata? Certamente no! E per lo stesso motivo per cui è utile controllare ogni nuova password contro la lista nera delle "password semplici".

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

In realtà, le problematiche affrontate dall'analisi statica sono decisamente più ampie. Infatti, l'analisi statica comprende qualsiasi verifica del codice sorgente effettuata prima della sua esecuzione. Ecco alcune delle operazioni che possono essere svolte:

  • Controllo dello stile di codifica nel senso più ampio del termine. Questo include non solo il controllo del formato, ma anche la ricerca di parentesi vuote o superflue, l'impostazione di soglie per metriche come il numero di righe o la complessità ciclica di un metodo, ecc. — tutto ciò che può potenzialmente ostacolare la leggibilità e la manutenibilità del codice. In Java, uno strumento di questo tipo è Checkstyle, mentre in Python si utilizza flake8. Programmi di questa categoria sono comunemente chiamati "linters".
  • Non solo il codice eseguibile può essere analizzato. I file di risorse, come JSON, YAML, XML e .properties, possono (e devono!) essere automaticamente verificati per la validità. È meglio scoprire che la struttura JSON è compromessa a causa di qualche virgolette non corrispondenti in una fase iniziale della verifica automatica del Pull Request, piuttosto che durante l'esecuzione dei test o in fase di runtime. Gli strumenti appropriati sono disponibili, ad esempio, YAMLlint, JSONLint.
  • La compilazione (o parsing per linguaggi di programmazione dinamici) è anch'essa una forma di analisi statica. In genere, i compilatori sono in grado di emettere avvisi che segnalano problemi di qualità nel codice sorgente, e non devono essere ignorati.
  • Talvolta la compilazione non è solo la generazione di codice eseguibile. Ad esempio, se hai documentazione in formato AsciiDoctor, al momento della conversione in HTML/PDF, il gestore AsciiDoctor (plugin Maven) può emettere avvisi, ad esempio, su riferimenti interni non validi. Questo è un motivo concreto per non accettare una Pull Request con modifiche alla documentazione.
  • Il controllo ortografico è anch'esso una forma di analisi statica. L'utilità aspell può verificare l'ortografia non solo nella documentazione, ma anche nel codice sorgente dei programmi (nei commenti e nelle stringhe) in vari linguaggi di programmazione, tra cui C/C++, Java e Python. Un errore di ortografia nell'interfaccia utente o nella documentazione è anch'esso un difetto!
  • I test di configurazione (per cosa sia - vedere questo e questo I report), anche se vengono eseguiti in un ambiente di test modulari tipo pytest, sono in realtà una forma di analisi statica, poiché non eseguono il codice sorgente durante la loro esecuzione.

Come possiamo vedere, la ricerca di bug in questo elenco gioca un ruolo minore, mentre tutto il resto è accessibile tramite strumenti open source gratuiti.

Quali di questi tipi di analisi statica dovreste applicare al vostro progetto? Certamente tutti, più ce ne sono, meglio è! L'importante è implementare correttamente, di cui parleremo di seguito.

Il pipeline di distribuzione come un filtro a più livelli e l'analisi statica come il suo primo strato

Una metafora classica dell'integrazione continua è il pipeline, attraverso cui scorrono le modifiche — dall'alterazione del codice sorgente fino alla distribuzione in produzione. La sequenza standard delle fasi di questo pipeline appare così:

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

Le modifiche bocciate nella fase N del pipeline non vengono trasferite alla fase N+1.

Perché proprio così, e non in un altro modo? Nella parte del pipeline dedicata ai test, i tester apprendono della ben nota piramide dei test.

Integra l'analisi statica nel tuo processo, invece di cercare bug con essa
Piramide dei test. Fonte: articolo Martin Fowler.

Nella parte inferiore di questa piramide si trovano i test più facili da scrivere, che si eseguono più rapidamente e hanno una minore tendenza ai falsi positivi. Perciò devono essere più numerosi, coprire più codice e essere eseguiti per primi. Nella parte superiore della piramide, la situazione è opposta, pertanto il numero di test di integrazione e UI deve essere ridotto al minimo necessario. L'elemento umano in questa catena è la risorsa più costosa, lenta e inaffidabile, quindi si trova alla fine e svolge il lavoro solo se le fasi precedenti non hanno rilevato difetti. Tuttavia, la pipeline è costruita secondo gli stessi principi anche nelle parti non direttamente collegate ai test!

Vorrei suggerire un'analogia basata su un sistema di filtraggio dell'acqua a più stadi. In ingresso, si introduce acqua sporca (modifiche con difetti), e in uscita dobbiamo ottenere acqua pulita, da cui sono stati eliminati tutti i contaminanti indesiderati.

Integra l'analisi statica nel tuo processo, invece di cercare bug con essa
Filtro a più stadi. Fonte: Wikimedia Commons

Come è noto, i filtri per la purificazione sono progettati in modo tale che ogni stadio successivo possa trattenere frazioni sempre più piccole di contaminanti. Gli stadi di filtraggio più grossolani hanno una maggiore capacità e un costo inferiore. Nella nostra analogia, questo significa che i gate di qualità in ingresso hanno una maggiore velocità, richiedono meno sforzo per l'attivazione e sono meno esigenti nel funzionamento — e sono disposti proprio in questo ordine. Il ruolo dell'analisi statica, che ora comprendiamo essere in grado di scartare solo i difetti più grossolani, è simile a quello della griglia «intrappolatrice» all'inizio del cascame di filtri.

L'analisi statica da sola non migliora la qualità del prodotto finale, così come un "filtraggio" non rende potabile l'acqua. Tuttavia, la sua importanza è evidente quando è combinata con altri elementi della catena di produzione. Anche se nei filtri multi-stadio i livelli di uscita possono teoricamente catturare tutto ciò che si trova nei livelli di ingresso, è chiaro quali conseguenze possano derivare dal tentativo di fare affidamento solo sui livelli di pulizia fine, senza i livelli di ingresso.

L'obiettivo del "filtraggio" è alleggerire i livelli successivi dalla cattura di difetti veramente grossolani. Ad esempio, almeno, la persona che esegue la revisione del codice non dovrebbe essere distratta da codice formattato in modo errato e da violazioni delle norme di codifica stabilite (come parentesi superflue o ramificazioni troppo profonde). I bug come NPE dovrebbero essere catturati dai test modulari, ma se già prima del test l'analizzatore indica che un bug è inevitabile, ciò accelererà notevolmente la sua correzione.

Credo che ora sia chiaro perché l'analisi statica non migliori la qualità del prodotto se utilizzata in modo episodico; deve essere applicata costantemente per escludere i cambiamenti con difetti gravi. La questione se l'uso di un analizzatore statico migliorerà la qualità del tuo prodotto è in gran parte equivalente alla domanda: "migliorerà la potabilità dell'acqua prelevata da uno stagno sporco se la si fa passare attraverso un colino?"

Integrazione in un progetto legacy

Un'importante questione pratica: 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 e il fallimento di uno di essi è una base sufficiente per ritenere che la build non abbia superato il quality gate. Tentare di stabilire un gate sui risultati dell'analisi statica fallisce: nel codice legacy ci sono troppi avvertimenti; non si vuole ignorarli completamente, ma non si può fermare la consegna del prodotto solo perché ci sono avvertimenti dell'analizzatore.

Quando viene utilizzato per la prima volta, l'analizzatore fornisce un'enorme quantità di avvisi, la maggior parte dei quali non sono rilevanti per il corretto funzionamento del prodotto. Non è possibile correggere immediatamente tutti questi commenti e in molti casi non è nemmeno necessario. Dopotutto, sappiamo che il nostro prodotto funziona complessivamente, e anche prima di implementare l'analisi statica!

Di conseguenza, molti si limitano a un uso sporadico dell'analisi statica, oppure la utilizzano solo in modalità di segnalazione, quando durante la compilazione viene semplicemente generato un rapporto dall'analizzatore. Questo è equivalente all'assenza di qualsiasi analisi, poiché se abbiamo già un certo numero di avvisi, l'emergere di un altro (per quanto grave) in seguito a modifiche del codice rimane inosservato.

Sono noti i seguenti modi per introdurre quality gates:

  • Impostazione di un limite sul numero totale di avvisi o sul numero di avvisi diviso per il numero di righe di codice. Questo metodo funziona male, poiché un tale gate consente liberamente modifiche con nuovi difetti, finché il loro limite non viene superato.
  • La registrazione, in un determinato momento, di tutti i vecchi avvisi 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 principale problema è che determinare cosa sia un errore "vecchio" e cosa sia "nuovo" è un algoritmo piuttosto complesso e non sempre funziona correttamente, specialmente se i file vengono modificati o rinominati significativamente. Ricordo che Codacy poteva ignorare nuovi avvisi in una pull request, e allo stesso tempo non approvare una pull request a causa di avvisi non relativi alle modifiche in quel codice.
  • A mio avviso, la soluzione più efficace è quella descritta nel libro Continuous Delivery il «metodo dell'arresto» («ratcheting»). L'idea principale è che la caratteristica di ogni rilascio è il numero di avvisi di analisi statica, e sono consentite solo quelle modifiche che non aumentano il numero totale di avvisi.

Arresto

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, quando viene assemblata la branch principale, nel tuo gestore di repository non viene indicato 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 risultanti con quello presente nell'attuale rilascio. Se la PR porta a un incremento di questo numero, il codice non supera il quality gate dell'analisi statica. Se il numero di avvisi diminuisce o rimane invariato, allora passa.
  3. Con il prossimo rilascio, il numero ricalcolato di avvisi sarà nuovamente registrato nei metadati del rilascio.

Così, poco a poco, ma inesorabilmente (come quando si utilizza un meccanismo a cricchetto), il numero di avvisi tende verso zero. Certo, il sistema può essere ingannato introducendo un nuovo avviso e correggendo uno esistente. Questo è normale, in quanto a lungo termine produce risultati: gli avvisi vengono corretti, di norma, non singolarmente, ma tutti insieme in base a un certo tipo, e tutti gli avvisi facilmente risolvibili vengono rapidamente rimossi.

Questo grafico mostra il numero totale di avvisi Checkstyle in sei mesi di funzionamento di un tale «meccanismo a cricchetto» su uno dei nostri progetti OpenSource. Il numero di avvisi è diminuito drasticamente, e ciò è avvenuto in modo naturale, parallelamente allo sviluppo del prodotto!

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

Utilizzo una versione modificata di questo metodo, contando separatamente gli avvisi divisi per moduli del progetto e strumenti di analisi; il file YAML generato con i metadati sulla build appare all'incirca nel seguente modo:

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 any advanced CI system, a 'ratchet' can be implemented for any static analysis tools without relying on plugins or third-party tools. Each analyzer produces its report in a simple text or XML format, which is easy to analyze. You just need to script the necessary logic in the CI script. You can see how this is implemented in our open-source projects based on Jenkins and Artifactory. qui o qui. Both examples depend on the library ratchetlib: the method countWarnings() counts xml tags in files generated by Checkstyle and Spotbugs in the usual way, while compareWarningMaps() implements the ratchet itself, throwing an error if the number of warnings in any of the categories increases.

Una realizzazione interessante di un «catalizzatore» è possibile per analizzare l'ortografia di commenti, letterali di testo e documentazione utilizzando aspell. Come noto, durante il controllo ortografico non tutte le parole sconosciute al dizionario standard sono errate; possono essere aggiunte al dizionario personalizzato. Se si rende il dizionario personalizzato parte del codice sorgente del progetto, la quality gate per l'ortografia può essere formulata come segue: eseguire aspell con il dizionario standard e quello personalizzato. non deve trovare errori di ortografia.

Sull'importanza di registrare la versione dell'analizzatore.

In conclusione, è importante notare che, qualunque sia il modo in cui implementate l'analisi nel vostro processo di consegna, la versione dell'analizzatore deve essere fissa. Se consentite un aggiornamento automatico dell'analizzatore, potrebbero emergere nuovi difetti durante la creazione di un pull request, non legati a modifiche del codice, ma al fatto che il nuovo analizzatore è semplicemente in grado di trovare più difetti, e questo romperà il vostro processo di accettazione dei pull request. L'aggiornamento dell'analizzatore deve essere un'azione consapevole. Tuttavia, il fissare rigidamente la versione di ciascun componente della build è in generale un requisito necessario e un tema di cui parlare separatamente.

Conclusioni

  • L'analisi statica non troverà bug per voi e non migliorerà la qualità del vostro prodotto come risultato di un'applicazione una tantum. Solo un'applicazione costante nel processo di consegna porta effetti positivi sulla qualità.
  • La ricerca dei bug non è affatto l'obiettivo principale dell'analisi; la stragrande maggioranza delle funzioni utili è disponibile negli strumenti open source.
  • Implementate quality gates sulla base dei risultati dell'analisi statica sin dalle prime fasi del processo di consegna, utilizzando un 'ratchet' per il codice legacy.

Link

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

Fonte: habr.com

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