Integrálja a statikus elemzést a folyamatába, ne csak hibák keresésére használja

Ezt a cikket a statikus elemzésről szóló számos anyag ihlette, amelyek egyre gyakrabban tűnnek fel. Először is, ez a PVS-studio blogja, amely aktívan reklámozza magát a Habr-on a hibák áttekintésein keresztül, amelyeket az ő eszközük fedezett fel nyílt forráskódú projektekben. Nemrég a PVS-studio támogatást valósított meg Java-hoz, és természetesen az IntelliJ IDEA fejlesztői, akinek beépített elemzője valószínűleg a legfejlettebb a Java számára, nem maradhattak kívül.

Az ilyen áttekintések olvasásakor olyan érzésünk van, mintha egy varázslatos főzet volna: csak nyomj meg egy gombot, és itt van — a hibák listája a szemed előtt. Úgy tűnik, hogy ahogy az elemzők fejlettebbé válnak, egyre több hibát találnak automatikusan, és azok a termékek, amelyeket ezek a robotok átvizsgálnak, egyre jobbak lesznek, anélkül, hogy bármilyen erőfeszítést tennénk.

De varázslatos főzetek nem léteznek. Szeretnék beszélni arról, amiről általában nem beszélnek az olyan bejegyzésekben, mint a "mit képes felfedezni a robotunk": mit nem tudnak az elemzők, mi a valódi szerepük és helyük a szoftver szállítási folyamatában, és hogyan kell őket helyesen bevezetni.

Integrálja a statikus elemzést a folyamatába, ne csak hibák keresésére használja
Egy példát említenék (forrás: wikipedia).

) Amit a statikus elemzők sosem fognak tudni

Mi az, a gyakorlatban, az forráskód elemzés? Adunk bemenetként egyes forrásokat, és a kimenetként rövid időn belül (sokkal rövidebb idő alatt, mint a tesztek futtatása) információkat kapunk a rendszerünkről. Az alapvető és matematikailag elkerülhetetlen korlátozás az, hogy így csak egy meglehetősen szűk információkört tudunk megszerezni.

A legismertebb példa egy olyan problémára, amelyet nem lehet statikus elemzés segítségével megoldani — a megállási probléma: ez egy tétel, amely bebizonyítja, hogy lehetetlen egy általános algoritmust kidolgozni, amely a program forráskódja alapján meghatározza, hogy az végtelen ciklusba kerül-e vagy véges időn belül befejeződik. E tétel kiterjesztése a Rice-tételA definíciója annak, hogy egy tetszőleges program képes-e kiszámítani egy tetszőleges nem triviális tulajdonságú számítási funkciót, algoritmikusan nem megoldható feladat. Például lehetetlen írni egy elemzőt, amely meg tudja határozni bármilyen forrással, hogy a vizsgált program egy algoritmus implementációja, amely például egy egész szám négyzetre emelését végzi.

Ezért a statikus elemzők funkcionalitása megkérdőjelezhetetlen korlátokkal rendelkezik. A statikus elemző soha nem tudja minden esetben meghatározni az olyan dolgokat, mint például a "null pointer exception" fellépése azokon a nyelveken, amelyek megengedik a null értéket, vagy minden esetben az "attribute not found" előfordulását olyan dinamikusan típusos nyelveknél. Amit a legfejlettebb statikus elemző tud tenni, az az, hogy kiemeli az olyan speciális eseteket, amelyek száma a lehetséges problémákhoz képest az Ön forráskódjában, nem túlzó módon, egy csepp a tengerben.

A statikus elemzés nem hibakeresés.

Az eddigiekből a következő következtetés vonható le: a statikus elemzés nem eszköz a program hibáinak csökkentésére. Merem állítani: amikor először alkalmazzák a projektjében, érdekes helyeket talál a kódban, de valószínűleg nem fogja észlelni a programja működésére hatással lévő hibákat.

Az elemzők által automatikusan felfedezett hibák példái lenyűgözőek, de ne feledjük, hogy ezek a példák nagy kódbázisok széles körű átvizsgálása révén kerültek elő. Ugyanezen elv alapján a hackerek, akik képesek több egyszerű jelszót megpróbálni sok fióknál, végül megtalálják azokat a fiókokat, ahol egyszerű jelszó található.

Jelenti ez, hogy a statikus elemzést nem szabad alkalmazni? Természetesen nem! Ugyanazon okból, amiért minden új jelszót érdemes leellenőrizni a "kevésbé biztonságos" jelszavak fekete listáján.

A statikus elemzés több, mint hibakeresés.

Valójában a megoldható elemzési feladatok sokkal szélesebb körűek. Az általános statikus elemzés bármilyen forráskód-ellenőrzés, amelyet a futtatás előtt végeznek. Íme néhány dolog, amit tenni lehet:

  • A kódolás stílusának ellenőrzése tág értelemben. Ide tartozik a formázás ellenőrzése, valamint a felesleges zárójelek használatának keresése, küszöbértékek beállítása olyan metrikákra, mint a sorok száma vagy a módszer ciklomatikus bonyolultsága, és így tovább — minden, ami potenciálisan megnehezíti a kód olvashatóságát és karbantartását. Java esetében ilyen eszköz a Checkstyle, Pythonban pedig a flake8. Az ilyen típusú programokat általában "lintereknek" hívják.
  • Nemcsak a végrehajtható kód analízisnek van alávetve. Az erőforrásfájlok, mint a JSON, YAML, XML, .properties automatikusan validálhatók (és kell is!), hiszen jobb tudni, hogy egy páratlan idézőjel miatt megsérült a JSON struktúrája, mint amikor a tesztek futása vagy a futásidő során derül ki. Vannak megfelelő eszközök: például YAMLlint, JSONLint.
  • A fordítás (vagy a dinamikus programozási nyelvek esetén parse-olás) szintén a statikus analízis egy formája. Általában a fordítók figyelmeztetéseket adhatnak, amelyek a forráskód minőségi problémáira utalnak, és ezeket nem szabad figyelmen kívül hagyni.
  • Néha a fordítás nem csupán a végrehajtható kód fordítása. Például, ha a dokumentációs formátumod AsciiDoctor, akkor amikor azt HTML/PDF formátumba alakítod, az AsciiDoctor feldolgozó (Maven plugin) figyelmeztetéseket adhat, például megsértett belső linkekről. Ez jelentős ok arra, hogy ne fogadjuk el a dokumentációs módosításokat tartalmazó Pull Requestet.
  • A helyesírás-ellenőrzés is a statikus analízis egy formája. Az eszköz aspell képes ellenőrizni a helyesírást nemcsak a dokumentációban, hanem a programok forráskódjaiban (kommentek és literálok) is, különböző programozási nyelveken, így például C/C++, Java és Python nyelveken. Egy felhasználói felületen vagy a dokumentációban elkövetett helyesírási hiba szintén hiba!
  • A konfigurációs tesztek (hogy mit jelentenek, lásd az és az jelentések), bár a pytest típusú moduláris tesztek futási környezetében hajtják végre őket, valójában a statikus analízis egy formájának számítanak, mivel nem hajtják végre a forráskódokat a futásuk során.

Ahogy látjuk, a hibák keresése a listában a legkevésbé fontos szerepet játszik, míg minden egyéb ingyenes open source eszközök használatával elérhető.

Milyen típusú statikus elemzéseket érdemes alkalmazni a projektjében? Természetesen mindet, minél több, annál jobb! A lényeg, hogy ezt helyesen vezessük be, erről lesz szó a következőkben.

A szállítási csővezeték mint többlépcsős szűrő és a statikus elemzés mint az első lépcső

A folyamatos integráció klasszikus metaforája a csővezeték, amelyen keresztül a változtatások áramlanak — a forráskód változtatásától a termelési környezetbe történő kiadásig. A csővezeték standard szakaszainak sorrendje a következőképpen néz ki:

  1. statisztikai elemzés
  2. fordítás
  3. modul tesztek
  4. integrációs tesztek
  5. UI tesztek
  6. kézi ellenőrzés

Az N-edik szakaszban elutasított változások nem továbbíthatók az N+1 szakaszra.

Miért így, és nem másképp? Azokban a szakaszokban, amelyek a teszteléshez kapcsolódnak, a tesztelők széles körben ismert tesztelési piramisról értesülnek.

Integrálja a statikus elemzést a folyamatába, ne csak hibák keresésére használja
Teszt piramis. Forrás: cikk Martin Fowler.

A piramis alsó részén találhatók azok a tesztek, amelyeket könnyebb írni, gyorsabban futnak, és nem hajlamosak a hamis riasztásokra. Ezért ezekből kell több, nagyobb kódot kell lefedniük, és először kell őket végrehajtani. A piramis felső részén éppen ellenkező a helyzet, ezért az integrációs és UI tesztek számát minimálisra kell csökkenteni. Az ember ebben a láncban a legdrágább, leglassabb és legmegbízhatatlanabb erőforrás, ezért a legvégére került, és csak akkor végez munkát, ha az előző szakaszok nem találtak hibát. Azonban ugyanezek az elvek érvényesek a teszteléssel közvetlenül nem kapcsolatos szakaszokban is!

Szeretnék egy analógiát javasolni, amely egy többlépcsős vízszűrő rendszert ábrázol. A bemeneten szennyezett víz (hibás változtatások) érkezik, a kimeneten pedig tiszta vizet kell kapnunk, amelyből minden nem kívánt szennyeződést kiszűrünk.

Integrálja a statikus elemzést a folyamatába, ne csak hibák keresésére használja
Többlépcsős szűrő. Forrás: Wikimedia Commons

Ahogy ismert, a tisztító szűrőket úgy tervezik, hogy minden következő fokozat képes legyen egyre finomabb szennyeződések kiszűrésére. A durvább tisztítással foglalkozó fokozatok nagyobb áteresztőképességgel és alacsonyabb költséggel rendelkeznek. Analógiánkban ez azt jelenti, hogy a belépési minőségi kapuk nagyobb teljesítményt mutatnak, kevesebb erőfeszítést igényelnek az indításhoz, és maguk is kevésbé igényesek a működés során — és pontosan ilyen sorrendben vannak felépítve. A statikus elemzés szerepe, amelyről most már tudjuk, hogy csak a legdurvább hibákat képes kiszűrni — ez az „iszapfogó” rács szerepe a szűrők sorozatának elején.

A statikus elemzés önmagában nem javítja a végtermék minőségét, akárcsak az „iszapfogó” nem teszi ivóvízzé a vizet. Mégis, a többi elemközli összefüggésben nyilvánvaló a jelentősége. Bár a többlépcsős szűrőben a kimeneti fokozatok potenciálisan képesek ugyanezek elfogására, egyértelmű, hogy milyen következményekkel jár, ha csupán a finom szűrés lépéseire támaszkodunk, a belépési fokozatok nélkül.

Az „iszapfogó” célja, hogy tehermentesítse a következő lépcsőket a nagyon durva hibák kiszűrésétől. Például legalább annyira, hogy a kódellenőrzést végző személynek nem szabadna az elformázatlan kóddal és a megállapított kódolási normák (mint a felesleges zárójelek vagy túl mély ágazások) megsértésével foglalkoznia. Az olyan hibáknak, mint az NPE, egységtesztekkel kellene kiszűrődnie, de ha a teszt előtt az elemző már figyelmeztet minket arra, hogy hiba fog bekövetkezni — ez jelentősen felgyorsítja a javítást.

Gondolom, most már világos, miért nem javítja a termék minőségét a statikus elemzés, ha alkalomszerűen alkalmazzák, és miért kellene folyamatosan használni a durva hibákkal kapcsolatos módosítások kiszűrésére. A kérdés, hogy a statikus elemző alkalmazása javítja-e a termék minőségét, körülbelül egyenlő azzal a kérdéssel, hogy „javulnak-e a víz ivási tulajdonságai, ha egy piszkos víztározóból áttereljük egy szűrőn?”

Bevezetés egy örökölt projekthez

Fontos gyakorlati kérdés: hogyan lehet a statikus elemzést beépíteni a folyamatos integráció folyamatába, mint "minőségi kaput"? Az automatikus teszteknél minden egyértelmű: van egy tesztsorozat, amelynél bármilyen hiba elegendő ahhoz, hogy azt mondjuk, a build nem teljesítette a minőségi kaput. Az ilyen módon a statikus elemzés eredményeivel kapcsolatos kapu beállítása azonban megbukik: a legacy kódon annyi figyelmeztetés van, hogy azt teljesen figyelmen kívül hagyni nem lehet, de a termék szállítását sem lehet csak azért leállítani, mert figyelmeztetések vannak az elemzőben.

Mivel először alkalmazva bármely projektben az elemző óriási mennyiségű figyelmeztetést ad ki, amelyek túlnyomó többsége nem kapcsolódik a termék megfelelő működéséhez. Az összes megjegyzést egyszerre javítani nem lehet, és sok esetben nem is szükséges. Végső soron tudjuk, hogy a termékünk összességében működik, és a statikus elemzés bevezetése előtt is!

Ennek eredményeként sokan az epizodikus statikus elemzés használatára korlátozódnak, vagy csak információs módban használják, amikor a build során egyszerűen kiadnak egy jelentést az elemzőtől. Ez egyenértékű a teljes elemzés hiányával, mert ha már annyi figyelmeztetésünk van, akkor egy új (bármennyire is komoly) figyelmeztetés a kód módosításakor észrevétlen marad.

Az alábbi módszerek ismertek a minőségi kapuk bevezetésére:

  • Az összes figyelmeztetés vagy a figyelmeztetések száma és a kód sorainak száma közötti arány limitjének beállítása. Ez rosszul működik, mivel az ilyen kapu szabadon engedi a változásokat új hibákkal, amíg a limitet nem lépik át.
  • A régi figyelmeztetések kódba történő megszorítása, a figyelmeztetések figyelmen kívül hagyása egy adott pillanatban, és az új figyelmeztetések megjelenésekor a build visszautasítása. Ezt a funkciót a PVS-studio és néhány online forrás, például a Codacy kínálja. Nem dolgoztam a PVS-studio-val, ami a Codacy-t illeti, a fő problémájuk abban rejlik, hogy nehezen megállapítható, mi tekinthető 'régi' és mi 'új' hibának — ez egy meglehetősen bonyolult és nem mindig jól működő algoritmus, különösen akkor, ha a fájlok jelentősen módosulnak vagy átnevezésre kerülnek. Az emlékemben a Codacy képes volt kihagyni az új figyelmeztetéseket a pull requestekben, ugyanakkor nem engedte át a pull requestet a kód ezen változásainak figyelmen kívül hagyása miatt.
  • Szerintem a leghatékonyabb megoldás az, ami a könyvben szerepel. A Continuous Delivery A 'hálózati' módszer ('ratcheting'). A fő ötlet az, hogy minden kiadás sajátossága a statikus elemzés által talált figyelmeztetések mennyisége, és csak olyan változtatások engedhetők meg, amelyek nem növelik a figyelmeztetések összegét.

Hálózati

Ez így működik:

  1. A kezdeti lépésben a kiadás metadatumaiban rögzítjük a kódban található figyelmeztetések számát, amelyeket az elemzők találnak. Így, amikor a fő branchet építjük, nem egyszerűen '7.0.2 kiadás', hanem '7.0.2 kiadás, amely 100500 Checkstyle figyelmeztetést tartalmaz' kerül feljegyzésre. Ha fejlett repo menedzsert használ, például az Artifactory-t, könnyen megőrzi a kiadásához tartozó ilyen metadatokat.
  2. Mostantól minden pull request buildelése összehasonlítja a keletkező figyelmeztetések számát a jelenlegi kiadásban található mennyiséggel. Ha a PR ezen szám növekedéséhez vezet, a kód nem megy át a statikus elemzési minőségi kapun. Ha a figyelmeztetések száma csökken vagy változatlan marad — akkor átmegy.
  3. A következő kiadásnál a számított figyelmeztetések száma ismét feljegyzésre kerül a kiadás metadatumaiban.

Így, fokozatosan, de folyamatosan (mint egy ratchet működésekor), a figyelmeztetések száma a nullához fog közelíteni. Persze a rendszert meg lehet trükközni, ha új figyelmeztetést adunk hozzá, de kijavítunk egy régit. Ez rendben van, mert hosszú távon eredményt ad: a figyelmeztetéseket általában nem egyesével, hanem egy adott típusú csoportban javítják, és az összes könnyen kijavítható figyelmeztetés viszonylag gyorsan eltűnik.

Ez a grafikon azt mutatja, hogy mennyi Checkstyle figyelmeztetés volt fél év alatt egy ilyen „ratchet” működésekor az egyik OpenSource projektünknél.A figyelmeztetések száma egy nagyságrenddel csökkent, és ez természetes módon történt, párhuzamosan a termék fejlesztésével!

Integrálja a statikus elemzést a folyamatába, ne csak hibák keresésére használja

Én egy módosított verziót alkalmazok ebből a módszerből, amely külön számolja a figyelmeztetéseket a projekt moduljainak és elemző eszközeinek bontásában, a kialakuló YAML fájl metainformációval a buildről körülbelül í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 „ratchet” bármely statikus elemző eszközre megvalósítható, anélkül, hogy pluginokra vagy külső eszközökre lenne szükség. Minden elemző a saját jelentését adja egyszerű szöveges vagy XML formátumban, ami könnyen elemezhető. Csak a szükséges logikát kell beírni a CI-scriptbe. Megnézheti, hogyan valósítottuk meg ezt az open source projektjeinkben a Jenkins és Artifactory alapjain. itt vagy itt. Mindkét példa a ratchetlib: metódus countWarnings() a szokásos módon számolja a Checkstyle és Spotbugs által generált xml-címkéket a fájlokban, míg a compareWarningMaps() valósítja meg azt a bizonyos ratchet-et, amely hibaüzenetet ad, ha a figyelmeztetések száma bármelyik kategóriában növekszik.

Érdekes megvalósítási lehetőség a „hamisítvány” elemzésére a kommentek, szöveges literalok és dokumentáció helyesírásának ellenőrzésére az aspell segítségével. Mint tudjuk, helyesírás-ellenőrzés során nem minden ismeretlen szó, ami a standard szótárban nem szerepel, hibás; ezek felvehetők a felhasználói szótárba. Ha a felhasználói szótár a projekt forráskódjának részévé válik, akkor a helyesírási minőségi kapu a következőképpen fogalmazható meg: az aspell végrehajtása a standard és a felhasználói szótárral. nem kellene nem találni helyesírási hibákat.

A verzióelemző rögzítésének fontossága

Összefoglalva meg kell jegyezni, hogy bármilyen módon is építi be az elemzést a szállítási folyamatába, a verzióelemzőt rögzítenie kell. Ha megengedjük a verzióelemző önálló frissítését, akkor a következő pull request összeállításakor új hibák „bukkanhatnak fel”, amelyek nem kapcsolódnak a kód változásaihoz, hanem ahhoz, hogy az új elemző egyszerűen képes több hibát találni — ez pedig megnehezíti a pull requestek átvételének folyamatát. Az elemző frissítése tudatos lépés kell, hogy legyen. Egyébként az összes építőelem verziójának szigorú rögzítése általában szükséges követelmény, és külön beszélgetés témája.

Következtetések

  • A statikus elemzés nem fog hibákat találni, és nem javítja a terméke minőségét egyetlen alkalmazás eredményeként. Csak a folyamatos alkalmazásának van pozitív hatása a minőségre a szállítási folyamat során.
  • A hibák keresése egyáltalán nem a legfőbb célja az elemzésnek; a hasznos funkciók többsége elérhető opensource eszközökben.
  • Alkalmazza a statikus analízis eredményein alapuló minőségi kapukat a szállítási folyamat legelső szakaszában, használja a „hamisítványt” a legacy kódra.

Hivatkozások

  1. A Continuous Delivery
  2. A. Kudrjavcev: Programelemzés: hogyan érthetjük meg, hogy jó programozók vagyunk előadás a különböző kód-elemzési módszerekről (nemcsak statikus!)

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster