Implementeer statische analyses in het proces, in plaats van deze te gebruiken om bugs te vinden

Ik werd ertoe aangezet dit artikel te schrijven door de grote hoeveelheid materiaal over statische analyse die steeds meer onder mijn aandacht komt. Ten eerste dit PVS-studio blog, dat zichzelf actief promoot op Habré met behulp van beoordelingen van fouten die door hun tool in open source-projecten zijn gevonden. Recentelijk is PVS-studio geïmplementeerd Java-ondersteuning, en natuurlijk de ontwikkelaars van IntelliJ IDEA, wiens ingebouwde analysator waarschijnlijk de meest geavanceerde is voor Java van vandaag, kon niet wegblijven.

Bij het lezen van dergelijke recensies krijg je het gevoel dat we het over een magisch elixer hebben: druk op de knop, en hier is het - een lijst met gebreken voor je ogen. Het lijkt erop dat naarmate de analysers verbeteren, er automatisch steeds meer bugs zullen worden gevonden, en dat de producten die door deze robots worden gescand steeds beter zullen worden, zonder enige inspanning van onze kant.

Maar er bestaan ​​geen magische elixers. Ik zou graag willen praten over waar meestal niet over gesproken wordt in berichten als “hier zijn de dingen die onze robot kan vinden”: wat analysatoren niet kunnen doen, wat is hun echte rol en plaats in het softwareleveringsproces, en hoe ze correct te implementeren .

Implementeer statische analyses in het proces, in plaats van deze te gebruiken om bugs te vinden
Ratel (bron: wikipedia).

Wat statische analysatoren nooit kunnen doen

Wat is broncodeanalyse vanuit praktisch oogpunt? We leveren een broncode als invoer en als uitvoer verkrijgen we in korte tijd (veel korter dan bij het uitvoeren van tests) enige informatie over ons systeem. De fundamentele en wiskundig onoverkomelijke beperking is dat we op deze manier slechts een vrij beperkte categorie informatie kunnen verkrijgen.

Het bekendste voorbeeld van een probleem dat niet kan worden opgelost met behulp van statische analyse is afsluiten probleem: Dit is een stelling die bewijst dat het onmogelijk is een algemeen algoritme te ontwikkelen dat op basis van de broncode van een programma kan bepalen of het binnen een eindige tijd in een lus of in een eindige tijd zal eindigen. Een uitbreiding van deze stelling is De stelling van Rice, waarin staat dat voor elke niet-triviale eigenschap van berekenbare functies het bepalen of een willekeurig programma een functie met een dergelijke eigenschap evalueert, een algoritmisch hardnekkig probleem is. Het is bijvoorbeeld onmogelijk om een ​​analysator te schrijven die uit welke broncode dan ook kan bepalen of het geanalyseerde programma een implementatie is van een algoritme dat bijvoorbeeld het kwadraat van een geheel getal berekent.

De functionaliteit van statische analysatoren heeft dus onoverkomelijke beperkingen. Een statische analysator zal nooit in alle gevallen zaken kunnen detecteren als bijvoorbeeld het voorkomen van een "null pointer exception" in talen die de waarde van null toestaan, of in alle gevallen het optreden van een "null pointer exception" kunnen vaststellen. attribuut niet gevonden" in dynamisch getypeerde talen. Het enige dat de meest geavanceerde statische analysator kan doen, is speciale gevallen benadrukken, waarvan het aantal, van alle mogelijke problemen met uw broncode, zonder overdrijving een druppel op een gloeiende plaat is.

Bij statische analyse gaat het niet om het vinden van bugs

Uit het bovenstaande volgt de conclusie: statische analyse is geen middel om het aantal fouten in een programma te verminderen. Ik zou durven zeggen: wanneer het voor de eerste keer op uw project wordt toegepast, zal het “interessante” plaatsen in de code vinden, maar hoogstwaarschijnlijk zal het geen gebreken vinden die de kwaliteit van uw programma beïnvloeden.

De voorbeelden van defecten die automatisch door analysers worden gevonden zijn indrukwekkend, maar we mogen niet vergeten dat deze voorbeelden zijn gevonden door het scannen van een groot aantal grote codebases. Volgens hetzelfde principe vinden hackers die de mogelijkheid hebben om verschillende eenvoudige wachtwoorden op een groot aantal accounts uit te proberen, uiteindelijk die accounts met een eenvoudig wachtwoord.

Betekent dit dat statische analyse niet mag worden gebruikt? Natuurlijk niet! En om precies dezelfde reden dat het de moeite waard is om bij elk nieuw wachtwoord te controleren of het op de stoplijst van ‘eenvoudige’ wachtwoorden staat.

Statische analyse is meer dan het vinden van bugs

In feite zijn de problemen die praktisch door analyse worden opgelost veel breder. In het algemeen is statische analyse immers elke verificatie van broncodes die wordt uitgevoerd voordat deze wordt gelanceerd. Hier zijn enkele dingen die u kunt doen:

  • Het controleren van codeerstijl in de breedste zin van het woord. Dit omvat zowel het controleren van de opmaak, het zoeken naar het gebruik van lege/extra haakjes, het instellen van drempels voor metrieken zoals het aantal regels/cyclomatische complexiteit van een methode, enz. - alles wat mogelijk de leesbaarheid en onderhoudbaarheid van de code belemmert. In Java is zo'n tool Checkstyle, in Python - flake8. Programma's van deze klasse worden gewoonlijk 'linters' genoemd.
  • Niet alleen uitvoerbare code kan worden geanalyseerd. Bronbestanden zoals JSON, YAML, XML, .properties kunnen (en moeten!) automatisch worden gecontroleerd op geldigheid. Het is tenslotte beter om in een vroeg stadium van de automatische Pull Request-verificatie te ontdekken dat de JSON-structuur kapot is vanwege enkele ongepaarde quotes dan tijdens de testuitvoering of runtime? Er zijn geschikte hulpmiddelen beschikbaar: b.v. YAMLlint, JSONLint.
  • Compilatie (of parseren voor dynamische programmeertalen) is ook een vorm van statische analyse. Over het algemeen zijn compilers in staat waarschuwingen te produceren die problemen met de kwaliteit van de broncode aangeven en die niet mogen worden genegeerd.
  • Soms is compilatie meer dan alleen het compileren van uitvoerbare code. Als u bijvoorbeeld documentatie heeft in het formaat AsciiDoctor, en op het moment dat het in HTML/PDF wordt omgezet, wordt de AsciiDoctor-handler (Maven-plug-in) kan bijvoorbeeld waarschuwingen geven over verbroken interne links. En dit is een goede reden om de Pull Request met documentatiewijzigingen niet te accepteren.
  • Spellingcontrole is ook een vorm van statische analyse. Nutsvoorziening een toverspreuk kan niet alleen de spelling controleren in documentatie, maar ook in programmabroncodes (commentaar en letterlijke waarden) in verschillende programmeertalen, waaronder C/C++, Java en Python. Ook een spelfout in de gebruikersinterface of documentatie is een defect!
  • Configuratietests (over wat ze zijn - zie. deze и deze rapporten), hoewel uitgevoerd in een unit-testruntime zoals pytest, zijn in feite ook een soort statische analyse, aangezien ze tijdens de uitvoering ervan geen broncodes uitvoeren.

Zoals je kunt zien, speelt het zoeken naar bugs in deze lijst de minst belangrijke rol, en al het andere is beschikbaar met behulp van gratis open source-tools.

Welke van deze soorten statische analyses moet u in uw project gebruiken? Hoe meer hoe beter natuurlijk! Het belangrijkste is om het correct te implementeren, wat verder zal worden besproken.

Leveringspijplijn als meertrapsfilter en statische analyse als eerste fase

De klassieke metafoor voor continue integratie is een pijplijn waar veranderingen doorheen stromen, van wijzigingen in de broncode tot levering en productie. De standaardvolgorde van fasen in deze pijplijn ziet er als volgt uit:

  1. statische analyse
  2. омпиляция
  3. unit testen
  4. integratie testen
  5. UI-tests
  6. handmatige controle

Wijzigingen die in de N-de fase van de pijplijn zijn afgewezen, worden niet overgebracht naar fase N+1.

Waarom precies zo en niet anders? In het testgedeelte van de pijplijn zullen testers de bekende testpiramide herkennen.

Implementeer statische analyses in het proces, in plaats van deze te gebruiken om bugs te vinden
Piramide testen. Bron: artikel Martin Fowler.

Onderaan deze piramide bevinden zich tests die gemakkelijker te schrijven zijn, sneller uit te voeren en niet de neiging hebben te mislukken. Daarom zouden er meer van moeten zijn, ze zouden meer code moeten omvatten en als eerste moeten worden uitgevoerd. Aan de top van de piramide is het tegenovergestelde waar, dus het aantal integratie- en UI-tests moet tot het noodzakelijke minimum worden teruggebracht. De persoon in deze keten is de duurste, langzaamste en meest onbetrouwbare hulpbron, dus hij staat helemaal aan het einde en voert het werk alleen uit als de voorgaande fasen geen gebreken hebben gevonden. Dezelfde principes worden echter gebruikt om een ​​pijpleiding aan te leggen in delen die niet direct verband houden met testen!

Ik zou graag een analogie willen aanbieden in de vorm van een meertraps waterfiltratiesysteem. Aan de ingang wordt vuil water (veranderingen met defecten) aangevoerd; aan de uitgang moeten we schoon water ontvangen, waarin alle ongewenste verontreinigingen zijn geëlimineerd.

Implementeer statische analyses in het proces, in plaats van deze te gebruiken om bugs te vinden
Meertrapsfilter. Bron: Wikimedia Commons

Zoals u weet zijn reinigingsfilters zo ontworpen dat elke volgende cascade een steeds fijnere fractie van de verontreinigingen kan filteren. Tegelijkertijd hebben cascades van grovere zuivering een hogere doorvoer en lagere kosten. In onze analogie betekent dit dat poorten voor invoerkwaliteit sneller zijn, minder inspanning vergen om te starten en zelf pretentielozer in gebruik zijn - en dit is de volgorde waarin ze worden gebouwd. De rol van de statische analyse, die, zoals we nu begrijpen, alleen de grofste gebreken kan wegnemen, is de rol van het ‘modder’-raster aan het allereerste begin van de filtercascade.

Statische analyse op zichzelf verbetert de kwaliteit van het eindproduct niet, net zoals een ‘modderfilter’ water niet drinkbaar maakt. En toch is het belang ervan, in combinatie met andere elementen van de pijplijn, duidelijk. Hoewel bij een meertrapsfilter de eindtrappen in potentie alles kunnen opvangen wat de invoertrappen doen, is het duidelijk welke consequenties zullen voortvloeien uit een poging om het alleen met fijnzuiveringstrappen te doen, zonder invoertrappen.

Het doel van de “modderval” is om te voorkomen dat volgende cascades zeer grove defecten oplopen. De persoon die de codebeoordeling uitvoert, mag bijvoorbeeld op zijn minst niet worden afgeleid door onjuist opgemaakte code en overtredingen van gevestigde codeerstandaarden (zoals extra haakjes of te diep geneste vertakkingen). Bugs zoals NPE's moeten worden opgespoord door unit-tests, maar als de analysator ons al vóór de test aangeeft dat er onvermijdelijk een bug zal optreden, zal dit de oplossing ervan aanzienlijk versnellen.

Ik geloof dat het nu duidelijk is waarom statische analyse de kwaliteit van het product niet verbetert als het af en toe wordt gebruikt, en voortdurend moet worden gebruikt om veranderingen met grove gebreken eruit te filteren. De vraag of het gebruik van een statische analysator de kwaliteit van uw product zal verbeteren, komt grofweg overeen met de vraag: "Zal water uit een vuile vijver de drinkkwaliteit verbeteren als het door een vergiet wordt gevoerd?"

Implementatie in een legacy-project

Een belangrijke praktische vraag: hoe implementeer je statische analyse in het continue integratieproces als een ‘kwaliteitspoort’? In het geval van automatische tests is alles duidelijk: er is een reeks tests, het mislukken van een van deze is voldoende reden om aan te nemen dat de assemblage de kwaliteitspoort niet heeft doorstaan. Een poging om op dezelfde manier een poort te installeren op basis van de resultaten van een statische analyse mislukt: er staan ​​te veel analysewaarschuwingen in de oude code, je wilt ze niet volledig negeren, maar het is ook onmogelijk om de verzending van een product stop te zetten alleen maar omdat het analyserwaarschuwingen bevat.

Wanneer de analysator voor het eerst wordt gebruikt, produceert hij bij elk project een groot aantal waarschuwingen, waarvan de overgrote meerderheid geen verband houdt met de goede werking van het product. Het is onmogelijk om al deze opmerkingen in één keer te corrigeren, en veel ervan zijn ook niet nodig. We weten immers dat ons product als geheel werkt, zelfs voordat we statische analyse introduceerden!

Als gevolg hiervan zijn velen beperkt tot incidenteel gebruik van statische analyse, of gebruiken ze deze alleen in de informatiemodus, wanneer tijdens de montage eenvoudigweg een analyserapport wordt uitgegeven. Dit komt overeen met het ontbreken van enige analyse, want als we al veel waarschuwingen hebben, blijft het optreden van een nieuwe (hoe ernstig ook) bij het wijzigen van de code onopgemerkt.

De volgende methoden voor het introduceren van kwaliteitspoorten zijn bekend:

  • Een limiet instellen voor het totale aantal waarschuwingen of het aantal waarschuwingen gedeeld door het aantal regels code. Dit werkt slecht, omdat zo'n poort veranderingen met nieuwe defecten vrijelijk doorlaat, zolang hun limiet niet wordt overschreden.
  • Op een gegeven moment repareren we alle oude waarschuwingen in de code als genegeerd, en weigeren we deze op te bouwen wanneer er nieuwe waarschuwingen optreden. Deze functionaliteit wordt geleverd door PVS-studio en enkele online bronnen, bijvoorbeeld Codacy. Ik heb niet de kans gehad om in PVS-studio te werken, omdat mijn ervaring met Codacy het grootste probleem is dat het bepalen van wat een “oude” en wat een “nieuwe” fout is, een nogal complex algoritme is dat niet altijd werkt correct, vooral als bestanden sterk zijn gewijzigd of hernoemd. In mijn ervaring kon Codacy nieuwe waarschuwingen in een pull-verzoek negeren, terwijl het tegelijkertijd een pull-verzoek niet kon doorgeven vanwege waarschuwingen die geen verband hielden met wijzigingen in de code van een bepaalde PR.
  • Naar mijn mening is de meest effectieve oplossing degene die in het boek wordt beschreven Continue levering “ratelmethode”. Het basisidee is dat het aantal statische analysewaarschuwingen een eigenschap is van elke release, en dat alleen wijzigingen zijn toegestaan ​​die het totale aantal waarschuwingen niet vergroten.

ratel

Het werkt op deze manier:

  1. In de beginfase wordt in de metadata vastgelegd hoeveel waarschuwingen er in de door de analysatoren aangetroffen waarschuwingen voorkomen. Dus als je upstream bouwt, schrijft je repositorymanager niet alleen “release 7.0.2”, maar “release 7.0.2 met 100500 checkstyle-waarschuwingen.” Als u een geavanceerde repositorymanager (zoals Artifactory) gebruikt, is het eenvoudig om dergelijke metadata over uw release op te slaan.
  2. Nu vergelijkt elke pull-aanvraag, wanneer deze is gebouwd, het aantal resulterende waarschuwingen met het aantal waarschuwingen dat beschikbaar is in de huidige release. Als PR tot een verhoging van dit aantal leidt, passeert de code de kwaliteitspoort voor statische analyse niet. Als het aantal waarschuwingen afneemt of niet verandert, gaat het voorbij.
  3. Bij de volgende release wordt het herberekende aantal waarschuwingen opnieuw vastgelegd in de releasemetadata.

Dus beetje bij beetje maar gestaag (zoals wanneer een ratel werkt), zal het aantal waarschuwingen naar nul neigen. Natuurlijk kan het systeem worden misleid door een nieuwe waarschuwing te introduceren, maar die van iemand anders te corrigeren. Dit is normaal, omdat het over een lange afstand resultaten oplevert: waarschuwingen worden in de regel niet individueel gecorrigeerd, maar in een groep van een bepaald type tegelijk, en alle gemakkelijk verwijderbare waarschuwingen worden vrij snel geëlimineerd.

Deze grafiek toont het totale aantal Checkstyle-waarschuwingen gedurende zes maanden gebruik van een dergelijke “ratel” aan een van onze OpenSource-projecten. Het aantal waarschuwingen is met een orde van grootte afgenomen, en dit gebeurde uiteraard parallel met de productontwikkeling!

Implementeer statische analyses in het proces, in plaats van deze te gebruiken om bugs te vinden

Ik gebruik een aangepaste versie van deze methode, waarbij waarschuwingen afzonderlijk worden geteld per projectmodule en analysetool, wat resulteert in een YAML-bestand met build-metagegevens dat er ongeveer zo uitziet:

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 elk geavanceerd CI-systeem kan ratel worden geïmplementeerd voor alle statische analysetools zonder afhankelijk te zijn van plug-ins en tools van derden. Elke analysator produceert zijn eigen rapport in een eenvoudig tekst- of XML-formaat dat gemakkelijk te analyseren is. Het enige dat overblijft is het schrijven van de nodige logica in het CI-script. Hoe dit wordt geïmplementeerd, ziet u in onze open source projecten op basis van Jenkins en Artifactory hier of hier. Beide voorbeelden zijn afhankelijk van de bibliotheek ratellib: methode countWarnings() telt xml-tags in bestanden gegenereerd door Checkstyle en Spotbugs op de gebruikelijke manier, en compareWarningMaps() implementeert dezelfde ratel en geeft een foutmelding wanneer het aantal waarschuwingen in een van de categorieën toeneemt.

Een interessante implementatie van de "ratel" is mogelijk voor het analyseren van de spelling van opmerkingen, letterlijke tekst en documentatie met behulp van aspell. Zoals u weet, zijn bij het controleren van de spelling niet alle woorden die onbekend zijn in het standaardwoordenboek onjuist; ze kunnen aan het gebruikerswoordenboek worden toegevoegd. Als u een aangepast woordenboek onderdeel maakt van de broncode van het project, kan de spellingkwaliteitspoort op deze manier worden geformuleerd: aspell uitvoeren met een standaard en aangepast woordenboek zou niet moeten vind geen spelfouten.

Over het belang van het repareren van de analyserversie

Concluderend is het punt om op te merken dat, ongeacht hoe u analyses in uw leveringspijplijn implementeert, de versie van de analyser moet worden hersteld. Als u toestaat dat de analysator spontaan wordt bijgewerkt, kunnen er bij het samenstellen van het volgende pull-verzoek nieuwe defecten "opduiken" die geen verband houden met codewijzigingen, maar verband houden met het feit dat de nieuwe analysator eenvoudigweg meer defecten kan vinden - en dit zal uw proces van het accepteren van pull-aanvragen doorbreken. Het upgraden van een analysator moet een bewuste actie zijn. Een rigide bevestiging van de versie van elk montageonderdeel is echter over het algemeen een noodzakelijke vereiste en een onderwerp voor een aparte discussie.

Bevindingen

  • Statische analyse zal geen bugs voor u vinden en zal de kwaliteit van uw product niet verbeteren als gevolg van een enkele toepassing. Een positief effect op de kwaliteit kan alleen worden bereikt door het constante gebruik ervan tijdens het leveringsproces.
  • Het vinden van bugs is helemaal niet de hoofdtaak van de analyse; de ​​overgrote meerderheid van de nuttige functies is beschikbaar in opensource-tools.
  • Implementeer kwaliteitspoorten op basis van de resultaten van statische analyses in de allereerste fase van de leveringspijplijn, met behulp van een “ratel” voor verouderde code.

referenties

  1. Continue levering
  2. A. Kudryavtsev: Programma-analyse: hoe te begrijpen dat u een goede programmeur bent rapporteer over verschillende methoden voor codeanalyse (niet alleen statisch!)

Bron: www.habr.com

Voeg een reactie