Implementați analiza statică în proces, mai degrabă decât să o utilizați pentru a găsi erori

Am fost îndemnat să scriu acest articol de cantitatea mare de materiale de analiză statică care îmi atrag tot mai mult atenția. În primul rând, aceasta PVS-studio blog, care se promovează activ pe Habré cu ajutorul recenziilor erorilor găsite de instrumentul lor în proiecte open source. PVS-studio implementat recent Suport Javași, desigur, dezvoltatorii IntelliJ IDEA, al cărui analizor încorporat este probabil cel mai avansat pentru Java astăzi, nu putea sta departe.

Când citiți astfel de recenzii, aveți senzația că vorbim despre un elixir magic: apăsați butonul și iată-l - o listă de defecte în fața ochilor tăi. Se pare că, pe măsură ce analizatorii se îmbunătățesc, se vor găsi automat tot mai multe bug-uri, iar produsele scanate de acești roboți vor deveni din ce în ce mai bune, fără niciun efort din partea noastră.

Dar nu există elixiruri magice. Aș dori să vorbesc despre ceea ce de obicei nu se vorbește în postări precum „iată ce le poate găsi robotul nostru”: ce nu pot face analizatorii, care este rolul și locul lor real în procesul de livrare a software-ului și cum să le implementeze corect .

Implementați analiza statică în proces, mai degrabă decât să o utilizați pentru a găsi erori
Clichet (sursa: wikipedia).

Ce nu pot face niciodată analizoarele statice

Ce este analiza codului sursă, din punct de vedere practic? Oferim cod sursă ca intrare, iar ca ieșire, într-un timp scurt (mult mai scurt decât rularea testelor) obținem câteva informații despre sistemul nostru. Limitarea fundamentală și de nedepășit din punct de vedere matematic este că putem obține doar o clasă destul de restrânsă de informații în acest fel.

Cel mai faimos exemplu de problemă care nu poate fi rezolvată folosind analiza statică este problema de inchidere: Aceasta este o teoremă care demonstrează că este imposibil să se dezvolte un algoritm general care să poată determina din codul sursă al unui program dacă se va bucla sau se va termina într-un timp finit. O extensie a acestei teoreme este Teorema lui Rice, care afirmă că pentru orice proprietate netrivială a funcțiilor calculabile, determinarea dacă un program arbitrar evaluează o funcție cu o astfel de proprietate este o problemă insolubilă din punct de vedere algoritmic. De exemplu, este imposibil să scrieți un analizor care să poată determina din orice cod sursă dacă programul analizat este o implementare a unui algoritm care calculează, să zicem, pătratul unui număr întreg.

Astfel, funcționalitatea analizoarelor statice are limitări de netrecut. Un analizor static nu va putea niciodată să detecteze în toate cazurile lucruri precum, de exemplu, apariția unei „excepții de nul pointer” în limbile care permit valoarea null sau, în toate cazurile, să determine apariția unui „ atribut nu a fost găsit" în limbile tip dinamic. Tot ceea ce poate face cel mai avansat analizor static este să evidențieze cazuri speciale, numărul cărora, printre toate problemele posibile cu codul sursă, este, fără exagerare, o picătură în ocean.

Analiza statică nu este despre găsirea erorilor

Din cele de mai sus rezultă concluzia: analiza statică nu este un mijloc de reducere a numărului de defecte dintr-un program. M-aș îndrăzni să spun: atunci când este aplicat la proiectul tău pentru prima dată, acesta va găsi locuri „interesante” în cod, dar, cel mai probabil, nu va găsi niciun defect care să afecteze calitatea programului tău.

Exemplele de defecte găsite automat de analizoare sunt impresionante, dar nu trebuie să uităm că aceste exemple au fost găsite prin scanarea unui set mare de baze de coduri mari. După același principiu, hackerii care au posibilitatea de a încerca mai multe parole simple pe un număr mare de conturi găsesc în cele din urmă acele conturi care au o parolă simplă.

Înseamnă asta că analiza statică nu trebuie utilizată? Desigur că nu! Și din exact același motiv pentru care merită să verificați fiecare parolă nouă pentru a vă asigura că este inclusă în lista de oprire a parolelor „simple”.

Analiza statică este mai mult decât găsirea erorilor

De fapt, problemele rezolvate practic prin analiză sunt mult mai largi. La urma urmei, în general, analiza statică este orice verificare a codurilor sursă efectuată înainte de a fi lansate. Iată câteva lucruri pe care le puteți face:

  • Verificarea stilului de codare în cel mai larg sens al cuvântului. Aceasta include atât verificarea formatării, căutarea folosirii parantezelor goale/extra, stabilirea pragurilor pentru metrici precum numărul de linii/complexitatea ciclomatică a unei metode etc. - orice poate împiedica lizibilitatea și mentenabilitatea codului. În Java, un astfel de instrument este Checkstyle, în Python - flake8. Programele din această clasă sunt de obicei numite „linters”.
  • Nu numai codul executabil poate fi analizat. Fișierele de resurse precum JSON, YAML, XML, .properties pot (și ar trebui!) să fie verificate automat pentru validitate. La urma urmei, este mai bine să aflați că structura JSON este ruptă din cauza unor ghilimele nepereche într-un stadiu incipient al verificării automate a cererii de tragere decât în ​​timpul execuției testului sau al timpului de execuție? Sunt disponibile instrumente adecvate: de ex. YAMLint, JSONLint.
  • Compilarea (sau analizarea pentru limbaje de programare dinamică) este, de asemenea, un tip de analiză statică. În general, compilatoarele sunt capabile să producă avertismente care indică probleme cu calitatea codului sursă și nu trebuie ignorate.
  • Uneori compilarea este mai mult decât compilarea codului executabil. De exemplu, dacă aveți documentație în format AsciiDoctor, apoi în momentul transformării în HTML/PDF, handlerul AsciiDoctor (Plugin Maven) poate emite avertismente, de exemplu, despre legăturile interne întrerupte. Și acesta este un motiv bun pentru a nu accepta cererea de tragere cu modificări ale documentației.
  • Verificarea ortografică este, de asemenea, un tip de analiză statică. Utilitate o vrajă este capabil să verifice ortografia nu numai în documentație, ci și în codurile sursă ale programului (comentarii și literale) în diferite limbaje de programare, inclusiv C/C++, Java și Python. O greșeală de ortografie în interfața de utilizator sau în documentație este, de asemenea, un defect!
  • Teste de configurare (despre ce sunt acestea - vezi. acest и acest rapoarte), deși executate într-un timp de execuție de test unitar precum pytest, sunt de fapt și un tip de analiză statică, deoarece nu execută coduri sursă în timpul execuției lor.

După cum puteți vedea, căutarea erorilor din această listă joacă cel mai puțin important rol, iar orice altceva este disponibil folosind instrumente open source gratuite.

Pe care dintre aceste tipuri de analiză statică ar trebui să utilizați în proiectul dvs.? Desigur, cu cât mai multe, cu atât mai bine! Principalul lucru este să-l implementați corect, ceea ce va fi discutat în continuare.

Conducta de livrare ca filtru în mai multe etape și analiza statică ca primă etapă

Metafora clasică pentru integrarea continuă este o conductă prin care curge schimbările, de la modificările codului sursă la livrare la producție. Secvența standard de etape din această conductă arată astfel:

  1. analiza statica
  2. compilare
  3. teste unitare
  4. teste de integrare
  5. teste UI
  6. verificare manuală

Modificările respinse în etapa a N-a a conductei nu sunt transferate în etapa N+1.

De ce exact așa și nu altfel? În partea de testare a conductei, testerii vor recunoaște binecunoscuta piramidă de testare.

Implementați analiza statică în proces, mai degrabă decât să o utilizați pentru a găsi erori
Testează piramida. Sursă: articol Martin Fowler.

În partea de jos a acestei piramide se află teste care sunt mai ușor de scris, mai rapid de executat și nu au tendința de a eșua. Prin urmare, ar trebui să fie mai multe, ar trebui să acopere mai mult cod și să fie executate mai întâi. În vârful piramidei, opusul este adevărat, astfel încât numărul de teste de integrare și UI ar trebui redus la minimum necesar. Persoana din acest lanț este cea mai scumpă, lentă și nesigură resursă, așa că este la sfârșit și efectuează munca doar dacă etapele anterioare nu au găsit defecte. Cu toate acestea, aceleași principii sunt folosite pentru a construi o conductă în părți care nu sunt direct legate de testare!

Aș dori să ofer o analogie sub forma unui sistem de filtrare a apei în mai multe etape. Apa murdară (se schimbă cu defecte) este furnizată la intrare; la ieșire trebuie să primim apă curată, în care toți contaminanții nedoriți au fost eliminați.

Implementați analiza statică în proces, mai degrabă decât să o utilizați pentru a găsi erori
Filtru cu mai multe etape. Sursă: Wikimedia Commons

După cum știți, filtrele de curățare sunt proiectate astfel încât fiecare cascadă ulterioară să poată filtra o parte din ce în ce mai fină a contaminanților. În același timp, cascadele de purificare mai grosiere au un randament mai mare și costuri mai mici. În analogia noastră, aceasta înseamnă că porțile de calitate de intrare sunt mai rapide, necesită mai puțin efort pentru a porni și sunt ele însele mai nepretențioase în funcționare - și aceasta este secvența în care sunt construite. Rolul analizei statice, care, după cum înțelegem acum, este capabilă să îndepărteze doar cele mai grosolane defecte, este rolul grilei „noroiului” de la începutul cascadei de filtrare.

Analiza statică în sine nu îmbunătățește calitatea produsului final, la fel cum un „filtru de noroi” nu face apa potabilă. Și totuși, împreună cu alte elemente ale conductei, importanța sa este evidentă. Deși într-un filtru cu mai multe etape etapele de ieșire sunt potențial capabile să capteze tot ceea ce fac etapele de intrare, este clar ce consecințe vor rezulta dintr-o încercare de a se descurca numai cu etapele de purificare fină, fără etape de intrare.

Scopul „capcanei de noroi” este de a scuti cascadele ulterioare de la capturarea defectelor foarte grave. De exemplu, cel puțin, persoana care efectuează revizuirea codului nu ar trebui să fie distrasă de codul formatat incorect și de încălcările standardelor de codare stabilite (cum ar fi parantezele suplimentare sau ramurile imbricate prea adânc). Erori precum NPE-urile ar trebui să fie detectate de testele unitare, dar dacă chiar înainte de testare, analizorul ne indică că se va întâmpla o eroare, acest lucru va grăbi semnificativ remedierea acestuia.

Cred că acum este clar de ce analiza statică nu îmbunătățește calitatea produsului dacă este folosită ocazional și ar trebui utilizată în mod constant pentru a filtra modificările cu defecte grave. Întrebarea dacă folosirea unui analizor static va îmbunătăți calitatea produsului dvs. este aproximativ echivalentă cu întrebarea: „Se va îmbunătăți calitatea băutării apei dintr-un iaz murdar dacă este trecută printr-o strecurătoare?”

Implementarea într-un proiect moștenit

O întrebare practică importantă: cum să implementăm analiza statică în procesul de integrare continuă ca o „poartă a calității”? În cazul testelor automate, totul este evident: există un set de teste, eșecul oricăreia dintre ele este un motiv suficient pentru a crede că ansamblul nu a trecut de poarta calității. O încercare de a instala o poartă în același mod pe baza rezultatelor unei analize statice eșuează: există prea multe avertismente de analiză în codul moștenit, nu doriți să le ignorați complet, dar este și imposibil să opriți livrarea unui produs doar pentru că conține avertismente ale analizorului.

Când este utilizat pentru prima dată, analizorul produce un număr mare de avertismente la orice proiect, marea majoritate dintre acestea nefiind legate de buna funcționare a produsului. Este imposibil să corectezi toate aceste comentarii simultan și multe nu sunt necesare. La urma urmei, știm că produsul nostru în ansamblu funcționează, chiar înainte de a introduce analiza statică!

Ca urmare, multe sunt limitate la utilizarea ocazională a analizei statice sau o folosesc doar în modul de informare, atunci când un raport al analizorului este pur și simplu emis în timpul asamblarii. Acest lucru echivalează cu absența oricărei analize, deoarece dacă avem deja multe avertismente, atunci apariția altuia (oricât de grav ar fi) la schimbarea codului trece neobservată.

Sunt cunoscute următoarele metode de introducere a porților de calitate:

  • Stabilirea unei limite a numărului total de avertismente sau a numărului de avertismente împărțit la numărul de linii de cod. Acest lucru funcționează prost, deoarece o astfel de poartă permite în mod liber trecerea modificărilor cu noi defecte, atâta timp cât limita lor nu este depășită.
  • Remedierea, la un moment dat, a tuturor avertismentelor vechi din cod ca fiind ignorate și refuzul de a construi atunci când apar avertismente noi. Această funcționalitate este oferită de PVS-studio și de unele resurse online, de exemplu, Codacy. Nu am avut ocazia să lucrez în PVS-studio, în ceea ce privește experiența mea cu Codacy, principala lor problemă este că determinarea a ceea ce este o eroare „veche” și ce este o eroare „nouă” este un algoritm destul de complex care nu funcționează întotdeauna corect, mai ales dacă fișierele sunt puternic modificate sau redenumite. Din experiența mea, Codacy ar putea ignora noile avertismente dintr-o cerere de extragere, în același timp, să nu treacă o cerere de extragere din cauza avertismentelor care nu erau legate de modificările codului unui anumit PR.
  • După părerea mea, cea mai eficientă soluție este cea descrisă în carte Livrarea continuă „metoda cu clichet”. Ideea de bază este că numărul de avertismente de analiză statică este o proprietate a fiecărei ediții și sunt permise numai modificări care nu cresc numărul total de avertismente.

Clichet

Funcționează așa:

  1. În etapa inițială, se face o înregistrare în metadate despre eliberarea numărului de avertismente din codul găsit de analizoare. Deci, atunci când construiți în amonte, managerul dvs. de depozit scrie nu doar „versiunea 7.0.2”, ci „versiunea 7.0.2 care conține 100500 de avertismente de stil de verificare”. Dacă utilizați un manager avansat de depozit (cum ar fi Artifactory), stocarea unor astfel de metadate despre versiunea dvs. este ușoară.
  2. Acum, fiecare cerere de extragere, când este construită, compară numărul de avertismente rezultate cu numărul de avertismente disponibile în versiunea curentă. Dacă PR duce la o creștere a acestui număr, atunci codul nu trece de poarta calității pentru analiza statică. Dacă numărul de avertismente scade sau nu se modifică, atunci trece.
  3. La următoarea lansare, numărul recalculat de avertismente va fi înregistrat din nou în metadatele versiunii.

Deci, încet, dar constant (ca atunci când funcționează un clichet), numărul de avertismente va tinde spre zero. Desigur, sistemul poate fi înșelat prin introducerea unui nou avertisment, dar corectând-o pe a altcuiva. Acest lucru este normal, deoarece pe o distanță lungă dă rezultate: avertismentele sunt corectate, de regulă, nu individual, ci într-un grup de un anumit tip simultan și toate avertismentele ușor de îndepărtat sunt eliminate destul de repede.

Acest grafic arată numărul total de avertismente Checkstyle pentru șase luni de funcționare a unui astfel de „clichet” activat unul dintre proiectele noastre OpenSource. Numărul de avertismente a scăzut cu un ordin de mărime, iar acest lucru s-a întâmplat firesc, în paralel cu dezvoltarea produsului!

Implementați analiza statică în proces, mai degrabă decât să o utilizați pentru a găsi erori

Folosesc o versiune modificată a acestei metode, numărând separat avertismentele în funcție de modulul de proiect și instrumentul de analiză, rezultând un fișier YAML cu metadate de compilare care arată cam așa:

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

În orice sistem CI avansat, clichetul poate fi implementat pentru orice instrumente de analiză statică fără a vă baza pe pluginuri și instrumente terțe. Fiecare analizor produce propriul său raport într-un text simplu sau format XML care este ușor de analizat. Tot ce rămâne este să scrieți logica necesară în scriptul CI. Puteți vedea cum este implementat acest lucru în proiectele noastre open source bazate pe Jenkins și Artifactory aici sau aici. Ambele exemple depind de bibliotecă ratchetlib: metoda countWarnings() contorizează etichetele xml în fișierele generate de Checkstyle și Spotbugs în mod obișnuit și compareWarningMaps() implementeaza acelasi clichet, aruncand o eroare cand creste numarul de avertismente in oricare dintre categorii.

O implementare interesantă a „clichet” este posibilă pentru analizarea ortografiei comentariilor, a literalelor textului și a documentației folosind aspell. După cum știți, atunci când verificați ortografia, nu toate cuvintele necunoscute în dicționarul standard sunt incorecte; ele pot fi adăugate la dicționarul utilizatorului. Dacă faceți un dicționar personalizat parte a codului sursă al proiectului, atunci poarta de calitate a ortografiei poate fi formulată astfel: rulați aspell cu un dicționar standard și personalizat nu ar trebui nu găsiți greșeli de ortografie.

Despre importanța reparării versiunii de analizor

În concluzie, trebuie remarcat faptul că, indiferent de modul în care implementați analiza în conducta de livrare, versiunea analizorului trebuie remediată. Dacă permiteți analizorului să se actualizeze spontan, atunci când asamblați următoarea cerere de extragere, pot apărea noi defecte care nu sunt legate de modificările codului, dar sunt legate de faptul că noul analizor este pur și simplu capabil să găsească mai multe defecte - iar acest lucru vă va întrerupe procesul de acceptare a solicitărilor de extragere. Actualizarea unui analizor ar trebui să fie o acțiune conștientă. Cu toate acestea, fixarea rigidă a versiunii fiecărei componente a ansamblului este, în general, o cerință necesară și un subiect pentru o discuție separată.

Constatări

  • Analiza statică nu va găsi erori pentru dvs. și nu va îmbunătăți calitatea produsului dvs. ca urmare a unei singure aplicații. Un efect pozitiv asupra calității poate fi obținut numai prin utilizarea constantă a acestuia în timpul procesului de livrare.
  • Găsirea erorilor nu este deloc sarcina principală a analizei; marea majoritate a funcțiilor utile sunt disponibile în instrumentele opensource.
  • Implementați porți de calitate bazate pe rezultatele analizei statice chiar în prima etapă a conductei de livrare, folosind un „clichet” pentru codul moștenit.

referințe

  1. Livrarea continuă
  2. A. Kudryavtsev: Analiza programului: cum să înțelegi că ești un bun programator raportați despre diferite metode de analiză a codului (nu numai statică!)

Sursa: www.habr.com

Adauga un comentariu