Më shtyu të shkruaj këtë artikull një sasi e madhe materiali mbi analizën statike që po dalin gjithnjë e më shumë para syve. Së pari, është , i cili e promovon aktivisht veten në Habr duke ofruar pasqyrime të gabimeve të zbuluara nga instrumenti i tyre në projekte me kod të hapur. Së fundmi, PVS-studio implementoi , dhe, sigurisht, zhvilluesit e IntelliJ IDEA, i cili përfshin një analizues të integruar që është sot, ndoshta, më i avancuari për Java, .
Kur lexoni të tilla pasqyrime, ndiheni sikur flitet për një eliksir magjik: goditni butonin, dhe ja, lista e defekteve para syve tuaj. Duket se me përmirësimin e analizuesve, gabimet do të gjejnë gjithnjë e më shumë dhe produktet e skanuara nga këta robota do të bëhen gjithnjë e më të mira, pa ndonjë angazhim nga ana jonë.
Por nuk ka eliksirë magjike. Dëshiroj të flas për atë që zakonisht nuk diskutohet në postime si «ja çfarë mund të gjejë roboti ynë»: çfarë nuk mund të realizojnë analizuesit, çfarë është roli dhe vendi i tyre të vërtetë në procesin e dorëzimit të softuerit, dhe si t'i implementojmë ata siç duhet.

Mekanikë (burimi: ).
Çfarë nuk do të mund të realizojnë kurrë analizuesit statikë
Çfarë është, nga një këndvështrim praktik, analiza e kodit burimor? Ne i dorëzojmë disa burime kodimi, dhe brenda një kohe të shkurtër (duke qenë shumë më e shkurtër se sa për të kaluar testet) marrim disa informacione mbi sistemin tonë. Kufizimi themelor dhe matematikor është, që si rezultat mund të marrim vetëm një klasë të ngushtë informacioni.
Shembulli më i njohur i një problematike që nuk zgjidhet përmes analizës statike është : kjo është një teoremë që provon se është e pamundur të zhvillohet një algoritëm tërësor që do të përcaktonte, përmes kodit burimor të programit, nëse ajo do të bënte një cikël apo do të përfundonte në një kohë të kufizuar. Zgjatja e këtij teoremi është , e cila thotë se për çdo pronë jo triviale të funksioneve të llogaritshme, përcaktimi i asaj që do të llogariste një program i caktuar me atë pronë është një detyrë algoritmikisht e pakapshme. Për shembull, është e pamundur të shkruash një analizues që, për çdo kod burimor, të përcaktojë nëse programi i analizuar është një implementim i një algoritmi që llogarit, le të themi, ngritjen në katër të një numri të plotë.
Prandaj, funksionaliteti i analizuesve statikë ka kufizime të pakapshme. Një analizues statik kurrë nuk do të mund të përcaktojë në të gjitha rastet gjëra si, për shembull, shfaqja e «null pointer exception» në gjuhët që lejojnë vlerën null, ose në të gjitha rastet të përcaktojë shfaqjen e «attribute not found» në gjuhët me tipizim dinamik. E gjithë ajo që mund të bëjë një analizues statik më i avancuar është të theksojë raste të veçanta, numri i të cilave midis të gjitha problemeve të mundshme me kodin tuaj burimor është, pa e tepruar, një pikë në det.
Analiza statike nuk është gjetje e gabimeve
Nga e gjithë kjo mund të përfundojmë se analiza statike nuk është një mjet për të zvogëluar numrin e defekteve në program. Do të guxoja të thoja: kur aplikoni për herë të parë në projektin tuaj, do të gjejë vende «interesante» në kod, por, për siguri, nuk do të gjejë asnjë defekt që ndikon në cilësinë e funksionimit të programit tuaj.
Shembujt e defekteve të gjendur automatikisht nga analizuesit janë mbresëlënës, por nuk duhet të harrojmë se këta shembuj janë gjetur përmes skanimit të një grupi të madh të bazave të kodit. Po ashtu, hakerat që kanë mundësinë të provonin disa fjalëkalime të thjeshta në një numër të madh llogarish, përfundimisht gjejnë ato llogari ku ka një fjalëkalim të thjeshtë.
A do të thotë kjo se analiza statike nuk duhet aplikuar? Sigurisht që jo! Dhe për të njëjtën arsye, për të cilën duhet kontrolluar çdo fjalëkalim të ri për të parë nëse ai përfshihet në listën e «fjalëkalimeve të zakonshme».
Analiza statike është më shumë se gjetja e gabimeve
Në të vërtetë, detyrat që mund të analizohet janë shumë më të gjera. Sepse në përgjithësi, analiza statike është çdo kontroll i burimeve që bëhet para nisjes së tyre. Këtu janë disa gjëra që mund të bëjmë:
- Kontrolli i stilit të kodimit në një kuptim të gjerë. Kjo përfshin si verifikimin e formatimit ashtu edhe kërkimin e përdorimeve të parashkruara/tepruara, vendosjen e pragjeve për metrikat si numri i linjave / kompleksiteti ciklomatik i metodës, etj. — gjithçka që mund të ndihmojë në lexueshmërinë dhe mirëmbajtjen e kodit. Në Java, një mjet i tillë është Checkstyle, në Python — flake8. Programet e këtij lloji zakonisht quhen "linters".
- Analiza mund të përfshijë jo vetëm kodin e ekzekutueshëm. Skedarët e burimeve, si JSON, YAML, XML, .properties mund (dhe duhet!) të kontrollohen automatikisht për vlefshmëri. Pse është më mirë të dihet në një fazë të hershme të verifikimit automatike të Pull Request se struktura e JSON është prishur për shkak të ndonjë thyerjeje çift, sesa kur të ekzekutohen testet ose në kohë ekzekutimi? Mjetet përkatëse ekzistojnë: për shembull, , .
- Kompilimi (ose analizimi për gjuhët e programimit dinamik) është gjithashtu një lloj analize statike. Në përgjithësi, kompiluesit janë në gjendje të japin paralajmërime që sinjalizojnë probleme me cilësinë e kodit burimor, dhe ato nuk duhet të injorohen.
- Ndonjëherë kompilimi nuk është vetëm kompilimi i kodit të ekzekutueshëm. Për shembull, nëse keni dokumentacionin në formatin , në momentin e konvertimit të saj në HTML/PDF, përpunuesi AsciiDoctor () mund të japë paralajmërime, për shembull, për lidhjet e brendshme të prishura. Dhe kjo është një arsye e rëndësishme për të mos pranuar Pull Request me ndryshimet në dokumentacion.
- Kontrolli i ortografisë është gjithashtu një lloj analize statike. Mjeti është në gjendje të kontrollojë ortografinë jo vetëm në dokumentacion, por edhe në kodet burimore të programeve (komentet dhe literalet) në gjuhë të ndryshme programimi, duke përfshirë C/C++, Java dhe Python. Një gabim ortografie në ndërfaqen e përdoruesit ose në dokumentacion është gjithashtu një defekt!
- Testet e konfigurimit (për informacion se çfarë është kjo — shih dhe raportet), megjithatë kryhen në mjedisin e ekzekutimit të testeve moduluese të tipit pytest, në të vërtetë janë gjithashtu një lloj analize statike, pasi nuk ekzekutojnë kodet burimore gjatë zhvillimit të tyre.
Siç e shohim, kërkimi i gabimeve në këtë listë luan rol më pak të rëndësishëm, ndërsa gjithçka tjetër është e aksesueshme përmes përdorimit të mjeteve open source falas.
Cilat nga këto lloje analize statike duhet të aplikoni në projektin tuaj? Sigurisht, të gjitha; sa më shumë, aq më mirë! E rëndësishme është ta implementoni siç duhet, për çfarë do të flasim më tej.
Tubi i shpërndarjes si një filtrues me shumë hapa dhe analiza statike si kaskadën e saj të parë
Metafora klasike e integrimit continu është tubi (pipeline), nëpër të cilin kalojnë ndryshimet — nga ndryshimi i kodit burimor deri në dorëzimin në prodhim. Sekuenca standarde e fazave të këtij tubi duket kështu:
- analiza statike
- kompilimi
- testet moduluese
- testet integruese
- testet UI
- kontrolli manual
Ndryshimet e refuzuara në hapin N të tubit nuk kalojnë në hapin N+1.
Pse kështu, e jo ndryshe? Në atë pjesë të tubit që ka të bëjë me testimin, testuesit njohin piramidën e testimit.

Pirouza e testeve. Burimi: Martin Fowler.
Në pjesën e poshtme të kësaj piramide ndodhen testet, të cilat janë më të lehta për t'u shkruar, që ekzekutohen më shpejt dhe nuk kanë tendencë për falsifikim. Prandaj, duhet të jenë më të shumtë, duhet të mbulojnë më shumë kod dhe të ekzekutohen të parat. Në pjesën e sipërme të piramidës, situata është e kundërt, prandaj numri i testeve integruese dhe UI duhet të zvogëlohet në minimumin e nevojshëm. Njeriu në këtë zinxhir është burimi më i shtrenjtë, më i ngadalshëm dhe më i pasigurt, prandaj ai ndodhet në fund dhe bën punën vetëm në rast se fazat e mëparshme nuk zbuluan asnjë defekt. Megjithatë, sipas të njëjtave parime ndërtohet tubi edhe në pjesë që nuk lidhen drejtpërdrejt me testimin!
Do doja të sugjeroja një analogji në formën e një sistemi filtrimi me shumë hapa. Në hyrje hyn një ujë i ndotur (ndryshime me defekte), dhe në dalje duhet të marrim ujë të pastër, të gjithë ndotjet e padëshiruara të cilave janë filtra.

Filtri me shumë hapa. Burimi:
Si dihet, filtrat e pastruesve projektohen në mënyrë që çdo kaskad e mëtejshme të mund të filtrojë fraksione gjithnjë e më të imta të ndotjes. Kjo do të thotë se kaskadat më të gjera të pastrimit kanë kapacitet më të madh kalues dhe kosto më të ulët. Në analogjinë tonë, ky do të thotë se portat e cilësisë hyrëse kanë performancë më të lartë, kërkojnë më pak përpjekje për t'u aktivizuar dhe vetë janë më të thjeshta në funksionim — dhe ato janë të radhitura në këtë mënyrë. Roli i analizës statike, e cila, siç e kuptojmë tani, mund të filtrojë vetëm defektet më të dukshme, është roli i grilës "ndotës" në fillim të kaskadës së filtrave.
Analiza statike vetë nuk përmirëson cilësinë e produktit përfundimtar, ashtu si "ndotësi" nuk e bën ujën të pijshme. Megjithatë, në lidhje me elementët e tjerë të procesit, rëndësia e saj është e qartë. Edhe pse në filtrin me shumë kaskada, kaskadat e daljes potencialisht mund të kapin të njëjtat gjëra si kaskadat hyrëse — është e qartë se çfarë pasojash do të kishte përpjekja për t'u mbështetur vetëm në kaskadat e pastrimit të hollë, pa kaskadat hyrëse.
Qëllimi i "ndotësit" është të lehtësojë kaskadat e mëvonshme nga kapja e defekteve shumë të dukshme. Për shembull, të paktën, një njeri që bën rishikimin e kodit nuk duhet të shqetësohet për kodin e keqformatuar dhe shkeljet e standardeve të kodimit (siç janë skedarët e tepërt ose rrjedhat e thella të të dhënave). Gabimet si NPE duhet të kapen nga testet modulare, por nëse analisti na tregon se një gabim do të ndodhë para se të testojmë — kjo e përshpejton ndjeshëm rregullimin e tij.
Mendoj se tani është e qartë pse analiza statike nuk përmirëson cilësinë e produktit, kur aplikohet episodikisht, dhe duhet të aplikohet vazhdimisht për të filtruar ndryshimet me defekte të dukshme. Pyetja, nëse përdorimi i analizës statike do të përmirësojë cilësinë e produktit tuaj, është afërsisht ekuivalente me pyetjen "a do të përmirësohen cilësitë e ujit të pijshëm, nëse e kalojmë atë përmes një sita?"
Integrimi në projektin legacy
Një pyetje praktike e rëndësishme: si të integrohet analiza statike në procesin e integrimit të vazhdueshëm si "porta cilësie"? Në rastin e testeve automatike, gjithçka është e qartë: ka një grup testesh, rënia e ndonjërit prej tyre është një arsye e mjaftueshme për të konsideruar se ndërtimi nuk ka kaluar portën e cilësisë. Përpjekja për të vendosur një portë në përfundimet e analizës statike dështon: në kodin legacy ka shumë paralajmërime dhe nuk dëshirohet të injorohen plotësisht ato, por gjithashtu nuk është e mundur të ndalet shpërndarja e produktit vetëm sepse ka paralajmërime nga analisti.
Duke u aplikua për herë të parë, në çdo projekt analisti jep një sasi të madhe paralajmërimesh, shumica prej të cilave nuk kanë lidhje me funksionimin e duhur të produktit. Nuk është e mundur të rregullohen menjëherë të gjitha këto vërejtje, dhe për disa — nuk është e nevojshme. Në fund të fundit, ne e dimë se produkti ynë në përgjithësi funksionon, madje edhe para aplikimit të analizës statike!
Si rezultat, shumë njerëz kufizohen në përdorimin episodik të analizës statike, ose e përdorin atë vetëm në mënyrë informuese, kur gjatë ndërtimit thjesht jepet një raport i analistit. Kjo është ekuivalente me mungesën e çdo analize, sepse nëse kemi tashmë shumë paralajmërime, atëherë shfaqja e një tjetër (sa do e rëndësishme) gjatë ndryshimit të kodit mbetet e papërceptuar.
Metodat e njohura për vendosjen e portave të cilësisë janë:
- Vendosja e një kufiri për numrin total të paralajmërimeve ose numrin e paralajmërimeve të ndara nga numri i rreshtave të kodit. Kjo punon keq, sepse një e tillë portë lehtë kalon ndryshimet me defekte të reja derisa kufiri i saj të tejkalohet.
- Identifikimi, në një moment të caktuar, i të gjitha paralajmërimeve të vjetra në kod si të injoruara, dhe ndalimi i ndërtimit në rast të shfaqjes së paralajmërimeve të reja. Kjo funksionalitet ofrohet nga PVS-studio dhe disa burime online, si p.sh. Codacy. Nuk kam pasur mundësinë të punoj në PVS-studio, sa i përket përvojës time me Codacy, problemi kryesor është se përcaktimi i asaj që është "gabimi i vjetër", dhe çfarë është "gabimi i ri" — është një algoritëm mjaft i komplikuar dhe jo gjithmonë funksionon siç duhet, sidomos nëse skedarët ndryshojnë ose rinovohen shumë. Në kujtesën time, Codacy mund të kishte lënë të kalonin paralajmërime të reja në një kërkesë tërheqëse, dhe në të njëjtën kohë nuk mund të kalonte një kërkesë tërheqëse për shkak të paralajmërimeve që nuk lidhen me ndryshimet në kodin e këtij PR.
- Në pikëpamjen time, zgjidhja më efektive është ajo e përshkruar në libër Metoda e xhamit të zorrës ("ratcheting"). Ideja kryesore është se çdo version ka një numër paralajmërimesh nga analiza statike dhe lejojnë vetëm ato ndryshime që nuk e rrisin numrin e përgjithshëm të paralajmërimeve.
Ratcheting
Funksionon kështu:
- Në fazën fillestare, regjistrohet në metadata për versionin numri i paralajmërimeve në kod që janë gjetur nga analizatorët. Në këtë mënyrë, kur ndodhet ndërtimi i degës kryesore, në menaxherin tuaj të repo regjistrohet jo thjesht "versioni 7.0.2", por "versioni 7.0.2, që përmban 100500 paralajmërime Checkstyle". Nëse përdorni një menaxher të avancuar repo (si Artifactory), ruajtja e këtyre metadatas për versionin tuaj është e lehtë.
- Tani, çdo pull request gjatë ndërtimit krahason numrin e paralajmërimeve që rezultojnë me atë që është aktualisht në versionin e tanishëm. Nëse PR shkakton një rritje të këtij numri, atëherë kodi nuk kalon cilësinë përmes analizës statike. Nëse numri i paralajmërimeve zvogëlohet ose nuk ndryshon - atëherë kalon.
- Në versionin e ardhshëm, numri i ripërllogaritur i paralajmërimeve do të regjistrohet përsëri në metadatën e versionit.
Kështu, ngadalë por sigurt (ashtu siç funksionon xhami i zorrës), numri i paralajmërimeve do të ketë tendencë të shkojë drejt zero. Natyrisht, sistemi mund të mashtrohet duke shtuar një paralajmërim të ri, por duke rregulluar një të huaj. Kjo është në rregull, pasi në një distancë të gjatë jep rezultat: paralajmërimet shpesh rregullohen jo individualisht, por menjëherë në një grup të caktuar, dhe të gjitha paralajmërimet që janë të lehta për t'u eliminuar shpejt bëhen të eliminuara.
Në këtë grafik tregohet numri total i paralajmërimeve Checkstyle për gjashtë muaj punë të këtij "ratcheting" në . Numri i paralajmërimeve u zvogëlua ndjeshëm, dhe kjo ndodhi natyrshëm, paralelisht me zhvillimin e produktit!

Unë aplikoj një version të modifikuar të kësaj metode, duke numëruar veçmas paralajmërimet sipas moduleve të projektit dhe mjeteve të analizës, skedari YAML i formuar me metadatën për ndërtimin duket pak a shumë kështu:
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ë çdo sistem CI të avancuar, "ratcheting" mund të realizohet për të gjitha mjetet e analizës statike, pa mbështetje në pluginë dhe mjete të jashtme. Çdo një nga analizatorët jep raportin e tij në një format të thjeshtë tekstual ose XML, që është lehtë i analizueshëm. Ngeli të shkruhet vetëm logjika e nevojshme në skriptin CI. Mund të shikoni se si kjo është realizuar në projektet tona open source mbi bazën e Jenkins dhe Artifactory ose . Të dy shembujt varen nga biblioteka : metoda countWarnings() numëron në mënyrë të zakonshme xml-të in file që gjenerohen nga Checkstyle dhe Spotbugs, ndërsa compareWarningMaps() implementon atë xham, duke hapur një gabim në rast se numri i paralajmërimeve në ndonjë nga kategoritë rritet.
Një variant interesant i implementimit të "ratcheting" është i mundshëm për analizimin e shkrimeve në komente, literale tekstuale dhe dokumentacionin me ndihmën e aspell. Siç dihet, gjatë kontrollit të shkrimit jo të gjitha fjalët që nuk njihen nga fjalori standard janë të gabuara, ato mund të shtohen në fjalorin e përdoruesit. Nëse e bëni fjalorin e përdoruesit pjesë të kodit burimor të projektit, atëherë porta e cilësisë për shkrimin mund të formulohet në këtë mënyrë: ekzekutimi i aspell me fjalorin standard dhe atë të përdoruesit të gjejë asnjë gabim në shkrim.
Rëndësia e fiksezës së versionit të analizatorit
Në përfundim, duhet theksuar se: pa marrë parasysh se si e futni analizën në konvjerin tuaj të dorëzimit, versioni i analizatorit duhet të jetë i fiksuar. Nëse lejoni që analizatori të përditësohet spontanisht, atëherë gjatë ndërtimit të çdo pull request mund të "shfaqen" defekte të reja, të cilat nuk janë të lidhura me ndryshimin e kodit, por të lidhura me faktin se analizatori i ri thjesht është në gjendje të gjejë më shumë defekte - dhe kjo do të prishë procesin tuaj të pranimit të pull request-ëve. Përditësimi i analizatorit duhet të jetë një veprim i ndërgjegjshëm. Megjithatë, fikseza e fortë e versionit të çdo komponente të ndërtimit është gjithashtu një kërkesë e nevojshme dhe një temë për një bisedë të veçantë.
Përfundimet
- Analiza statike nuk do t'ju gjejë gabitë dhe nuk do të përmirësojë cilësinë e produktit tuaj si rezultat i një aplikimi një herë. Efekti pozitiv për cilësinë rezulton vetëm nga përdorimi i saj të vazhdueshëm gjatë procesit të dorëzimit.
- Kërkimi i gabive, gjithashtu, nuk është detyra kryesore e analizës, shumica e funksioneve të dobishme janë të disponueshme në mjete opensource.
- Implementoni quality gates bazuar rezultatet e analizës statike në fazën e parë të procesit të dorëzimit, duke përdorur "ratchet" për kodin legacy.
Linke
- paraqitje mbi metoda të ndryshme të analizës së kodit (jo vetëm analizë statike!)
Burimi: habr.com
