A statikus elemzést alkalmazza a folyamatban, ahelyett, hogy hibákat keresne

Ennek a cikknek a megírásához a statikus elemzéssel kapcsolatos számos anyag inspirált, amelyek egyre gyakrabban jelennek meg. Először is ezt PVS-stúdió blog, amely aktívan reklámozza magát a Habrén az eszközük által a nyílt forráskódú projektekben talált hibák áttekintésével. A PVS-stúdió nemrégiben került bevezetésre Java támogatás, és természetesen az IntelliJ IDEA fejlesztői, akiknek a beépített elemzője valószínűleg ma a legfejlettebb Java számára, nem tudott távol maradni.

Az ilyen értékelések olvasása során az embernek az az érzése, hogy varázselixírről beszélünk: nyomja meg a gombot, és itt van - a hibák listája a szeme előtt. Úgy tűnik, ahogy az analizátorok javulnak, automatikusan egyre több lesz a hiba, és az ezek által a robotok által szkennelt termékek egyre jobbak lesznek, minden erőfeszítés nélkül.

De nincsenek varázselixírek. Arról szeretnék beszélni, amiről általában nem esik szó az olyan posztokban, mint „itt van, amit a robotunk megtalál”: mire nem képesek az elemzők, mi a valódi szerepük és helyük a szoftverszállítási folyamatban, és hogyan lehet ezeket helyesen megvalósítani.

A statikus elemzést alkalmazza a folyamatban, ahelyett, hogy hibákat keresne
Racsnis (forrás: wikipedia).

Amit a statikus elemzők soha nem tudnak megtenni

Mit jelent gyakorlati szempontból a forráskód elemzés? Egyes forrásokból táplálkozunk, és rövid időn belül (sokkal rövidebb, mint a tesztek futtatása) kapunk némi információt a rendszerünkről. Az alapvető és matematikailag áthidalhatatlan korlát az, hogy így az információknak csak egy meglehetősen szűk osztályához juthatunk hozzá.

A statikus elemzéssel nem megoldható probléma leghíresebb példája az stop probléma: ez egy tétel, amely azt bizonyítja, hogy nem lehet olyan általános algoritmust kidolgozni, amely a program forráskódjából meghatározná, hogy véges időn belül ciklusba kerül-e vagy befejeződik. Ennek a tételnek a kiterjesztése az Rice tétele, amely kimondja, hogy a kiszámítható függvények bármely nem triviális tulajdonsága esetén annak meghatározása, hogy egy tetszőleges program kiértékel-e egy ilyen tulajdonságú függvényt, algoritmikusan megoldhatatlan probléma. Például lehetetlen olyan elemzőt írni, amely bármilyen forráskódból meg tudja állapítani, hogy az elemzett program egy olyan algoritmus implementációja-e, amely mondjuk egy egész számot négyzetre emel.

Így a statikus analizátorok működésének leküzdhetetlen korlátai vannak. A statikus elemző soha nem lesz képes meghatározni minden esetben, például a nulla mutató kivételének előfordulását nullázható nyelvekben, vagy minden esetben meghatározni a "nem található attribútum" előfordulását olyan nyelvekben, amelyek dinamikus gépelés. A legfejlettebb statikus analizátor csak különleges eseteket tud kiemelni, amelyek száma a forráskóddal kapcsolatos összes lehetséges probléma között túlzás nélkül csepp a tengerben.

A statikus elemzés nem a hibák keresését jelenti

A fentiekből következik a következtetés: a statikus elemzés nem eszköz a programhibák számának csökkentésére. Megkockáztatom, hogy amikor először alkalmazzák a projektjére, "szórakoztató" helyeket fog találni a kódban, de nagy valószínűséggel nem talál olyan hibákat, amelyek befolyásolnák a program minőségét.

Az elemzők által automatikusan talált hibák példái lenyűgözőek, de nem szabad elfelejtenünk, hogy ezeket a példákat nagy kódbázisok nagy halmazának vizsgálatával találtuk meg. Ugyanezen elv alapján azok a feltörők, akik több egyszerű jelszót is képesek kipróbálni nagyszámú fiókon, végül megtalálják azokat a fiókokat, amelyek egyszerű jelszóval rendelkeznek.

Ez azt jelenti, hogy nem szabad statikus elemzést használni? Természetesen nem! És pontosan ugyanazért, amiért érdemes minden új jelszót ellenőrizni, hogy bekerüljön az „egyszerű” jelszavak stoplistájába.

A statikus elemzés több, mint hibák keresése

Valójában az elemzéssel gyakorlatilag megoldott problémák sokkal tágabbak. Végtére is, általában a statikus elemzés a forráskódok minden olyan ellenőrzése, amelyet az indítás előtt hajtanak végre. Íme néhány teendő:

  • A kódolási stílus ellenőrzése a szó legtágabb értelmében. Ez magában foglalja a formázás ellenőrzését és az üres/extra zárójelek használatának keresését, küszöbértékek beállítását olyan metrikákra, mint a sorok száma / ciklomatikus metódusok bonyolultsága stb. – mindent, ami a kódot olvashatóbbá és karbantarthatóbbá teszi. Java-ban ez az eszköz Checkstyle, Pythonban flake8. Az ebbe az osztályba tartozó programokat általában "lintereknek" nevezik.
  • Nem csak a végrehajtható kódot lehet elemezni. Az olyan erőforrásfájlok, mint a JSON, YAML, XML, .properties, automatikusan ellenőrizhetők (és kell is!) az érvényesség szempontjából. Nem jobb kideríteni, hogy néhány párosítatlan idézet miatt a JSON-struktúra az automatikus Pull Request érvényesítés korai szakaszában megsérül, mint a tesztek végrehajtásakor vagy a futásidőben? Megfelelő eszközök állnak rendelkezésre: pl. YAMLlint, JSONLint.
  • A fordítás (vagy dinamikus programozási nyelvek esetén az elemzés) szintén egyfajta statikus elemzés. A fordítók általában képesek figyelmeztetéseket kiadni, amelyek a forráskód minőségével kapcsolatos problémákat jeleznek, és ezeket nem szabad figyelmen kívül hagyni.
  • Néha a fordítás nem csak a végrehajtható kód fordítását jelenti. Például, ha ilyen formátumú dokumentációval rendelkezik AsciiDoctor, majd a HTML/PDF kezelővé való átalakítás pillanatában AsciiDoctor (maven plugin) figyelmeztetéseket adhat ki például a meghibásodott belső hivatkozásokról. És ez jó ok arra, hogy ne fogadja el a lehívási kérelmet a dokumentáció módosításával.
  • A helyesírás-ellenőrzés is egyfajta statikus elemzés. Hasznosság egy varázslat nem csak a dokumentációban, hanem a programok forráskódjaiban (kommentárokban és literálokban) is képes a helyesírást ellenőrizni különböző programozási nyelveken, beleértve a C/C++, Java és Python nyelveket is. Hibának számít a kezelőfelület vagy a dokumentáció helyesírási hibája is!
  • Konfigurációs tesztek (hogy mi ez, lásd ezt и ezt riportok), bár egységteszt futási környezetben futnak le, mint például a pytest, valójában egyfajta statikus elemzést is jelentenek, mivel nem hajtanak végre forráskódokat a végrehajtásuk során.

Amint láthatja, a hibák megtalálása ebben a listában a legkevésbé fontos szerepet tölti be, és minden más elérhető ingyenes nyílt forráskódú eszközök használatával.

Az alábbi típusú statikus elemzések közül melyiket kell használni a projektben? Persze minél több, annál jobb! A lényeg az, hogy helyesen hajtsák végre, amelyet a továbbiakban tárgyalunk.

Szállítási csővezeték többlépcsős szűrőként és statikus elemzés első kaszkádjaként

A folyamatos integráció klasszikus metaforája a folyamat (pipeline), amelyen keresztül a változások folynak – a forráskód megváltoztatásától a szállításon át a gyártásig. Ennek a folyamatnak a szabványos szakaszai így néznek ki:

  1. statikus elemzés
  2. összeállítás
  3. egységtesztek
  4. integrációs tesztek
  5. UI tesztek
  6. kézi ellenőrzés

A folyamat N-edik szakaszában elutasított változtatások nem kerülnek tovább az N+1 szakaszba.

Miért pont így, és miért nem másként? A csővezeték tesztelési részében a tesztelők felismerik a jól ismert tesztelési piramist.

A statikus elemzést alkalmazza a folyamatban, ahelyett, hogy hibákat keresne
Teszt piramis. Forrás: cikk Martin Fowler.

Ennek a piramisnak az alján olyan tesztek találhatók, amelyek könnyebben írhatók, gyorsabban futnak, és nem hajlamosak hamis pozitív eredményre. Ezért több legyen belőlük, több kódot kell fedniük, és először kell végrehajtani. A piramis tetején ennek az ellenkezője igaz, ezért az integrációs és UI tesztek számát a szükséges minimumra kell csökkenteni. Ebben a láncban az ember a legdrágább, leglassabb és legmegbízhatatlanabb erőforrás, ezért a legvégén van, és csak akkor végez munkát, ha az előző szakaszok nem találtak hibát. Ugyanezen elvek szerint azonban a csővezeték olyan részekből épül fel, amelyek nem kapcsolódnak közvetlenül a teszteléshez!

Egy analógiát szeretnék ajánlani egy többlépcsős vízszűrő rendszer formájában. Szennyezett víz kerül a bemenetre (hibákkal változik), a kimeneten tiszta vizet kell kapnunk, melyben minden nem kívánt szennyeződést eltávolítanak.

A statikus elemzést alkalmazza a folyamatban, ahelyett, hogy hibákat keresne
Többlépcsős szűrő. Forrás: Wikimedia Commons

Mint ismeretes, a tisztítószűrőket úgy tervezték, hogy minden következő kaszkád a szennyeződések egyre kisebb hányadát tudja kiszűrni. Ugyanakkor a durvább tisztítás kaszkádjai nagyobb áteresztőképességgel és alacsonyabb költséggel rendelkeznek. A mi analógiánkban ez azt jelenti, hogy a bemeneti minőségi kapuk gyorsabbak, kevesebb erőfeszítést igényelnek az indításhoz, és önmagukban is szerényebbek a működésben – és pontosan ez a sorrend, amibe be vannak építve. A statikus analízis szerepe, amely, mint ma már értjük, csak a legdurvább hibákat képes kiszűrni, a rács-"sár" szerepe a szűrőkaszkád legelején.

A statikus elemzés önmagában nem javítja a végtermék minőségét, mint ahogy az iszapcsapda sem teszi ihatóvá a vizet. És mégis, a szállítószalag többi eleméhez hasonlóan fontossága nyilvánvaló. Bár egy többlépcsős szűrőben a kimeneti fokozatok potenciálisan képesek mindent megragadni, amit a bemeneti fokozatok csinálnak, egyértelmű, hogy milyen következményekkel jár, ha a finomszűrési fokozatokat egyedül, bemeneti fokozatok nélkül próbálják megtenni.

A "sárgyűjtő" célja, hogy a következő kaszkádokat tehermentesítse a nagyon durva hibák rögzítéséből. Például a kódellenőrt legalább nem szabad elvonnia a helytelenül formázott kódtól és a megállapított kódolási normák megsértésétől (például extra zárójelek vagy túl mélyen beágyazott ágak). Az olyan hibákat, mint az NPE, az egységteszteknek kell elkapniuk, de ha már a teszt előtt az analizátor jelzi nekünk, hogy a hiba elkerülhetetlenül bekövetkezik, az jelentősen felgyorsítja a javítást.

Azt hiszem, most már világos, hogy a statikus elemzés miért nem javítja a termék minőségét, ha alkalmanként alkalmazzák, és folyamatosan arra kell használni, hogy kiszűrje a durva hibákkal járó változásokat. Az a kérdés, hogy a statikus elemző használata javítja-e a termék minőségét, nagyjából egyenértékű azzal a kérdéssel, hogy „Javul-e a piszkos tóból vett víz ivási minősége, ha átengedik a szűrőedényen?”

Megvalósítás örökölt projektben

Fontos gyakorlati kérdés: hogyan lehet a statikus elemzést „minőségkapuként” bevezetni a folyamatos integráció folyamatába? Az automata tesztek esetében minden nyilvánvaló: van tesztsorozat, bármelyik meghibásodása elegendő ok arra, hogy azt hidd, a szerelvény nem ment át a minőségi kapun. A statikus elemzés eredményei alapján egy kapu telepítésére tett kísérlet meghiúsul: túl sok elemzési figyelmeztetés van a régebbi kódban, nem akarja teljesen figyelmen kívül hagyni őket, de lehetetlen leállítani a kézbesítést. termék csak azért, mert elemzőre vonatkozó figyelmeztetéseket tartalmaz.

Az első használatkor az analizátor hatalmas számú figyelmeztetést generál bármely projektnél, amelyek túlnyomó többsége nem a termék megfelelő működéséhez kapcsolódik. Ezeket a megjegyzéseket lehetetlen egyszerre kijavítani, és sok közülük nem is szükséges. Hiszen tudjuk, hogy termékünk összességében működik, már a statikus elemzés bevezetése előtt is!

Emiatt sokan a statikus elemzés epizodikus használatára korlátozódnak, vagy csak tájékoztató módban használják, amikor az analizátor jelentését egyszerűen kiadják az összeszerelés során. Ez egyenértékű az elemzés hiányával, mert ha már sok figyelmeztetésünk van, akkor egy újabb (bármilyen súlyos) előfordulása a kód megváltozásakor észrevétlen marad.

A minőségi kapuk bevezetésének következő módjai ismertek:

  • Korlátozza a figyelmeztetések számát, vagy a figyelmeztetések számát osztva a kódsorok számával. Ez nem működik jól, mert egy ilyen kapu szabadon átugorja a változtatásokat az új hibákkal, amíg a határt át nem lépik.
  • Egy bizonyos ponton a kódban lévő összes régi figyelmeztetés figyelmen kívül hagyásának javítása, és az összeállítás meghiúsítása új figyelmeztetések megjelenésekor. Ezt a funkciót a PVS-studio és néhány online forrás, például a Codacy biztosítja. Nem volt lehetőségem a PVS-studióban dolgozni, mivel a Codacy-vel kapcsolatos tapasztalataim szerint a fő problémájuk az, hogy a „régi” és mi az „új” hiba meghatározása egy meglehetősen bonyolult algoritmus, amely nem mindig megfelelően működnek, különösen akkor, ha a fájlokat erősen módosították vagy átnevezték. Emlékeim szerint a Codacy ki tudta hagyni az új figyelmeztetéseket egy lehívási kérésben, ugyanakkor nem hagyhatja ki a lehívási kérelmet olyan figyelmeztetések miatt, amelyek nem kapcsolódnak a jelen PR kódjának módosításához.
  • Véleményem szerint a leghatékonyabb megoldást a könyv írja le Folyamatos szállítás „racsnis” módszer. A fő ötlet az, hogy az egyes kiadások tulajdonsága a statikus elemzési figyelmeztetések száma, és csak olyan változtatások engedélyezettek, amelyek nem növelik a figyelmeztetések számát.

Racsnis

Ez így működik:

  1. A kezdeti szakaszban az elemzők által talált kódban található figyelmeztetések számának rögzítése történik a kiadási metaadatokban. Így az upstream építésekor a tárkezelő nem csak "7.0.2-es kiadás", hanem "7.0.2 Checkstyle figyelmeztetést tartalmazó 100500-es kiadás" lesz írva. Ha fejlett adattárkezelőt (például az Artifactoryt) használ, könnyen tárolhatja az ilyen metaadatokat a kiadásról.
  2. Mostantól minden összeállítási kérés összehasonlítja a kapott figyelmeztetések számát az aktuális kiadás számával. Ha a PR ennek a számnak a növekedéséhez vezet, akkor a kód nem lépi át a minőségi kaput a statikus elemzésben. Ha a figyelmeztetések száma csökken vagy nem változik, akkor elmúlik.
  3. A következő kiadásnál a figyelmeztetések újraszámított száma vissza lesz írva a kiadás metaadatai közé.

Tehát apránként, de folyamatosan (mint egy racsnis esetén) a figyelmeztetések száma nullára fog csökkenni. Természetesen a rendszert meg lehet téveszteni, ha új figyelmeztetést vezetünk be, de kijavítjuk valaki másét. Ez normális, mert hosszú távon meg is adja az eredményt: a figyelmeztetéseket általában nem egyenként, hanem azonnal egy bizonyos típusú csoport rögzíti, és minden könnyen kiküszöbölhető figyelmeztetés gyorsan megszűnik.

Ez a grafikon a Checkstyle figyelmeztetések teljes számát mutatja egy ilyen „racsnis” hat hónapos működése során egyik nyílt forráskódú projektünk. A figyelmeztetések száma nagyságrenddel csökkent, és ez természetesen, a termék fejlesztésével párhuzamosan történt!

A statikus elemzést alkalmazza a folyamatban, ahelyett, hogy hibákat keresne

Ennek a módszernek egy módosított változatát használom, külön számolom a figyelmeztetéseket projektmodulonként és elemzőeszközönként, így egy összeállítási metaadatokkal rendelkező YAML-fájlt kapok, amely valahogy így néz ki:

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

Bármely fejlett CI rendszerben a racsnis beépíthető bármilyen statikus elemző eszközhöz anélkül, hogy beépülő modulokra és harmadik féltől származó eszközökre támaszkodna. Mindegyik elemző egyszerű szöveges vagy XML formátumban készíti el a jelentését, amely könnyen értelmezhető. Csak a szükséges logikát kell regisztrálnia a CI-szkriptben. Megnézheti, hogyan valósítják meg ezt a Jenkins és az Artifactory alapú nyílt forráskódú projektjeinkben itt vagy itt. Mindkét példa könyvtárfüggő ratchetlib: módszer countWarnings() a Checkstyle és Spotbugs által generált fájlok xml címkéit a szokásos módon számolja, és compareWarningMaps() ugyanazt a racsnit valósítja meg, hibát dobva, ha bármelyik kategóriában megnő a figyelmeztetések száma.

Érdekes racsnis megvalósítás lehetséges a megjegyzések, szöveges literálok és dokumentációk helyesírási elemzéséhez az aspell használatával. Mint tudják, a helyesírás-ellenőrzés során nem minden, a szabványos szótárban ismeretlen szó hibás, hanem hozzáadható a felhasználói szótárhoz. Ha a felhasználói szótárat a projekt forráskód részévé teszi, akkor a helyesírási minőségi kapu a következőképpen fogalmazható meg: aspell végrehajtás a szabványos és felhasználói szótárral nem szabad nem talál helyesírási hibát.

Az analizátor verzió javításának fontosságáról

Összegzésképpen meg kell jegyezni a következőket: függetlenül attól, hogy hogyan építi be az elemzést a szállítási folyamatba, az analizátor verzióját rögzíteni kell. Ha engedélyezi az analizátor spontán frissítését, akkor a következő lehívási kérés felépítésekor olyan új hibák „bukkanhatnak fel”, amelyek nem a kódváltozásokhoz kapcsolódnak, hanem azzal a ténnyel kapcsolatosak, hogy az új analizátor egyszerűen képes több hibát találni - és ez megszakítja a lehívási kérések elfogadásának folyamatát. Az analizátor korszerűsítésének tudatos cselekvésnek kell lennie. Az egyes összeállítási komponensek verziójának kemény rögzítése azonban általánosságban szükséges követelmény, és külön megbeszélés témája.

Álláspontja

  • A statikus elemzés nem talál hibákat az Ön számára, és nem javítja a termék minőségét egyetlen alkalmazás eredményeként. A minőségre gyakorolt ​​egyetlen pozitív hatása annak folyamatos használata a szállítási folyamat során.
  • A hibák keresése egyáltalán nem az elemzés fő feladata, a hasznos funkciók túlnyomó többsége az opensource eszközökben elérhető.
  • A statikus elemzés eredményein alapuló minőségi kapuk megvalósítása a szállítási folyamat legelső szakaszában, egy racsnis segítségével az örökölt kódhoz.

referenciák

  1. Folyamatos szállítás
  2. A. Kudrjavcev: Programelemzés: hogyan lehet megérteni, hogy jó programozó vagy jelentés a kódelemzés különböző módszereiről (nem csak statikus!)

Forrás: will.com

Hozzászólás