Implementera statisk analys i processen istället för att använda den för att hitta buggar

Jag blev uppmanad att skriva den här artikeln av den stora mängd material om statisk analys som alltmer kommer till min uppmärksamhet. För det första, detta PVS-studioblogg, som aktivt marknadsför sig på Habré med hjälp av recensioner av fel som hittats av deras verktyg i projekt med öppen källkod. Nyligen implementerad PVS-studio Java-stöd, och, naturligtvis, utvecklarna av IntelliJ IDEA, vars inbyggda analysator förmodligen är den mest avancerade för Java idag, kunde inte hålla sig borta.

När du läser sådana recensioner får du känslan av att vi pratar om ett magiskt elixir: tryck på knappen, och här är den - en lista över defekter framför dina ögon. Det verkar som att allt eftersom analysatorerna förbättras kommer fler och fler buggar att hittas automatiskt, och produkterna som skannas av dessa robotar kommer att bli bättre och bättre, utan någon ansträngning från vår sida.

Men det finns inga magiska elixirer. Jag skulle vilja prata om det som vanligtvis inte pratas om i inlägg som "här är de saker som vår robot kan hitta": vad analysatorer inte kan göra, vad är deras verkliga roll och plats i mjukvaruleveransprocessen och hur man implementerar dem korrekt .

Implementera statisk analys i processen istället för att använda den för att hitta buggar
Ratchet (källa: википедия).

Vad statiska analysatorer aldrig kan göra

Vad är källkodsanalys ur en praktisk synvinkel? Vi tillhandahåller en del källkod som indata, och som utdata får vi på kort tid (mycket kortare än pågående tester) lite information om vårt system. Den grundläggande och matematiskt oöverstigliga begränsningen är att vi bara kan få en ganska snäv klass av information på detta sätt.

Det mest kända exemplet på ett problem som inte kan lösas med statisk analys är avstängningsproblem: Detta är ett teorem som bevisar att det är omöjligt att utveckla en generell algoritm som kan avgöra från källkoden för ett program om det kommer att loopa eller avslutas inom en begränsad tid. En förlängning av detta teorem är Rice's teorem, som anger att för alla icke-triviala egenskaper hos beräkningsbara funktioner, är det ett algoritmiskt svårlöst problem att avgöra om ett godtyckligt program utvärderar en funktion med en sådan egenskap. Det är till exempel omöjligt att skriva en analysator som kan avgöra från vilken källkod som helst om programmet som analyseras är en implementering av en algoritm som beräknar, till exempel, kvadreringen av ett heltal.

Funktionaliteten hos statiska analysatorer har således oöverstigliga begränsningar. En statisk analysator kommer aldrig i alla fall att kunna upptäcka sådant som till exempel förekomsten av ett "nullpekareundantag" på språk som tillåter värdet av null, eller i alla fall att fastställa förekomsten av ett " attribut inte hittat" på dynamiskt skrivna språk. Allt som den mest avancerade statiska analysatorn kan göra är att markera speciella fall, vars antal, bland alla möjliga problem med din källkod, utan att överdriva är en droppe i hinken.

Statisk analys handlar inte om att hitta buggar

Av ovanstående följer slutsatsen: statisk analys är inte ett sätt att minska antalet defekter i ett program. Jag skulle våga säga: när det appliceras på ditt projekt för första gången, kommer det att hitta "intressanta" platser i koden, men troligtvis kommer det inte att hitta några defekter som påverkar kvaliteten på ditt program.

Exemplen på defekter som automatiskt hittas av analysatorer är imponerande, men vi bör inte glömma att dessa exempel hittades genom att skanna en stor uppsättning stora kodbaser. Enligt samma princip hittar hackare som har möjlighet att prova flera enkla lösenord på ett stort antal konton så småningom de konton som har ett enkelt lösenord.

Betyder detta att statisk analys inte bör användas? Självklart inte! Och av exakt samma anledning som det är värt att kontrollera varje nytt lösenord för att se till att det finns med i stopplistan över "enkla" lösenord.

Statisk analys är mer än att hitta buggar

Faktum är att de problem som praktiskt löses genom analys är mycket bredare. När allt kommer omkring, i allmänhet är statisk analys varje verifiering av källkoder som utförs innan de lanseras. Här är några saker du kan göra:

  • Kontrollera kodningsstil i ordets vidaste bemärkelse. Detta inkluderar både att kontrollera formatering, leta efter användningen av tomma/extra parenteser, ställa in tröskelvärden för mått som antal rader/cyklomatisk komplexitet för en metod, etc. - allt som potentiellt hindrar läsbarheten och underhållbarheten av koden. I Java är ett sådant verktyg Checkstyle, i Python - flake8. Program i den här klassen kallas vanligtvis "linters".
  • Inte bara körbar kod kan analyseras. Resursfiler som JSON, YAML, XML, .properties kan (och bör!) automatiskt kontrolleras för giltighet. När allt kommer omkring är det bättre att ta reda på att JSON-strukturen är trasig på grund av några oparade citat i ett tidigt skede av automatisk Pull Request-verifiering än under testkörning eller körtid? Lämpliga verktyg finns: t.ex. YAMLlint, JSONLint.
  • Kompilering (eller analys för dynamiska programmeringsspråk) är också en typ av statisk analys. I allmänhet kan kompilatorer producera varningar som indikerar problem med källkodens kvalitet och bör inte ignoreras.
  • Ibland är kompilering mer än att bara kompilera körbar kod. Till exempel om du har dokumentation i formatet AsciiDoctor, i det ögonblick då den omvandlas till HTML/PDF, hanterar AsciiDoctor (Maven-plugin) kan utfärda varningar, till exempel om trasiga interna länkar. Och detta är en bra anledning att inte acceptera Pull-förfrågan med dokumentationsändringar.
  • Stavningskontroll är också en typ av statisk analys. Verktyg en förtrollning kan kontrollera stavning inte bara i dokumentationen, utan även i programkällkoder (kommentarer och bokstavliga ord) i olika programmeringsspråk, inklusive C/C++, Java och Python. Ett stavfel i användargränssnittet eller dokumentationen är också ett fel!
  • Konfigurationstester (om vad de är - se. detta и detta rapporter), även om de körs i en enhetstestkörning såsom pytest, är de i själva verket också en typ av statisk analys, eftersom de inte exekverar källkoder under exekveringen.

Som du kan se spelar sökning efter buggar i den här listan den minst viktiga rollen, och allt annat är tillgängligt genom att använda gratis verktyg med öppen källkod.

Vilken av dessa typer av statisk analys ska du använda i ditt projekt? Självklart, ju fler desto bättre! Det viktigaste är att implementera det korrekt, vilket kommer att diskuteras vidare.

Leveranspipeline som ett flerstegsfilter och statisk analys som första steg

Den klassiska metaforen för kontinuerlig integration är en pipeline genom vilken förändringar flödet, från källkodsändringar till leverans till produktion. Standardsekvensen av steg i denna pipeline ser ut så här:

  1. statisk analys
  2. kompilering
  3. enhetstester
  4. integrationstester
  5. UI-tester
  6. manuell kontroll

Ändringar som avvisats i det N:e steget av pipelinen överförs inte till steg N+1.

Varför just på detta sätt och inte på annat sätt? I testdelen av pipelinen kommer testare att känna igen den välkända testpyramiden.

Implementera statisk analys i processen istället för att använda den för att hitta buggar
Testa pyramid. Källa: artikel Martin Fowler.

Längst ner i denna pyramid finns test som är lättare att skriva, snabbare att utföra och som inte har en tendens att misslyckas. Därför borde det finnas fler av dem, de borde täcka mer kod och exekveras först. I toppen av pyramiden är det tvärtom, så antalet integrations- och UI-tester bör reduceras till det nödvändiga minimum. Personen i denna kedja är den dyraste, långsamma och otillförlitliga resursen, så han är i slutet och utför bara arbetet om de tidigare stegen inte hittade några defekter. Samma principer används dock för att bygga en pipeline i delar som inte är direkt relaterade till testning!

Jag skulle vilja erbjuda en analogi i form av ett flerstegs vattenfiltreringssystem. Smutsigt vatten (förändringar med defekter) tillförs ingången, vid utgången måste vi få rent vatten, i vilket alla oönskade föroreningar har eliminerats.

Implementera statisk analys i processen istället för att använda den för att hitta buggar
Flerstegsfilter. Källa: Wikimedia Commons

Som ni vet är rengöringsfilter utformade så att varje efterföljande kaskad kan filtrera bort en allt finare del av föroreningar. Samtidigt har grövre reningskaskader högre genomströmning och lägre kostnad. I vår analogi betyder detta att grindar av ingångskvalitet är snabbare, kräver mindre ansträngning för att starta och är i sig mer opretentiösa i drift - och det är den sekvens i vilken de är byggda. Rollen för statisk analys, som, som vi nu förstår, kan rensa bort endast de grövre defekterna, är rollen som "lera"-nätet i början av filterkaskaden.

Statisk analys i sig förbättrar inte kvaliteten på slutprodukten, precis som ett "lerfilter" inte gör vatten drickbart. Och ändå, i samband med andra delar av pipelinen, är dess betydelse uppenbar. Även om utgångsstegen i ett flerstegsfilter potentiellt är kapabla att fånga allt som ingångsstegen gör, är det tydligt vilka konsekvenser som kommer att bli av ett försök att nöja sig med enbart finreningssteg, utan ingångssteg.

Syftet med "lerfällan" är att befria efterföljande kaskader från att fånga upp mycket grova defekter. Till exempel bör åtminstone personen som gör kodgranskningen inte distraheras av felaktigt formaterad kod och brott mot etablerade kodningsstandarder (som extra parenteser eller för djupt kapslade grenar). Buggar som NPE bör fångas upp av enhetstester, men om analysatorn redan före testet indikerar för oss att en bugg kommer att hända, kommer detta att avsevärt påskynda åtgärden.

Jag tror att det nu är klart varför statisk analys inte förbättrar kvaliteten på produkten om den används ibland, och bör användas ständigt för att filtrera bort förändringar med grova defekter. Frågan om huruvida användning av en statisk analysator kommer att förbättra kvaliteten på din produkt motsvarar ungefär att fråga: "Kommer vatten som tas från en smutsig damm att förbättras i drickskvalitet om det passerar genom ett durkslag?"

Implementering i ett äldre projekt

En viktig praktisk fråga: hur implementerar man statisk analys i den kontinuerliga integrationsprocessen som en "kvalitetsport"? När det gäller automatiska tester är allt uppenbart: det finns en uppsättning tester, ett misslyckande av någon av dem är tillräcklig anledning att tro att monteringen inte klarade kvalitetsporten. Ett försök att installera en grind på samma sätt baserat på resultaten av en statisk analys misslyckas: det finns för många analysvarningar i den äldre koden, du vill inte ignorera dem helt, men det är också omöjligt att sluta skicka en produkt bara för att den innehåller analysatorvarningar.

När den används för första gången producerar analysatorn ett stort antal varningar på alla projekt, varav de allra flesta inte är relaterade till produktens korrekta funktion. Det är omöjligt att korrigera alla dessa kommentarer på en gång, och många är inte nödvändiga. När allt kommer omkring vet vi att vår produkt som helhet fungerar, även innan vi införde statisk analys!

Som ett resultat är många begränsade till tillfällig användning av statisk analys, eller använder den bara i informationsläge, när en analysatorrapport helt enkelt utfärdas under montering. Detta motsvarar frånvaron av någon analys, för om vi redan har många varningar, så går förekomsten av en annan (oavsett hur allvarlig) när du ändrar koden obemärkt.

Följande metoder för att introducera kvalitetsgrindar är kända:

  • Ställa in en gräns för det totala antalet varningar eller antalet varningar dividerat med antalet kodrader. Detta fungerar dåligt, eftersom en sådan grind fritt tillåter förändringar med nya defekter att passera, så länge deras gräns inte överskrids.
  • Fixar, vid ett visst tillfälle, alla gamla varningar i koden som ignorerade, och vägrar bygga när nya varningar uppstår. Denna funktion tillhandahålls av PVS-studio och vissa onlineresurser, till exempel Codacy. Jag hade inte möjligheten att arbeta i PVS-studio, för min erfarenhet av Codacy är deras huvudproblem att bestämma vad som är ett "gammalt" och vad som är ett "nytt" fel är en ganska komplex algoritm som inte alltid fungerar korrekt, särskilt om filer är kraftigt modifierade eller bytt namn. Enligt min erfarenhet kunde Codacy ignorera nya varningar i en pull-förfrågan, samtidigt som den inte godkände en pull-förfrågan på grund av varningar som inte var relaterade till ändringar i koden för en given PR.
  • Enligt min mening är den mest effektiva lösningen den som beskrivs i boken Kontinuerlig leverans "spärrmetoden". Grundtanken är att antalet statiska analysvarningar är en egenskap för varje version, och endast ändringar är tillåtna som inte ökar det totala antalet varningar.

Ratchet

Det fungerar så här:

  1. I det inledande skedet görs en registrering i metadata om frisläppandet av antalet varningar i koden som analyserarna hittat. Så när du bygger uppströms skriver din förvarshanterare inte bara "release 7.0.2", utan "release 7.0.2 som innehåller 100500 XNUMX checkstyle-varningar." Om du använder en avancerad repository manager (som Artifactory) är det enkelt att lagra sådan metadata om din release.
  2. Nu jämför varje pull-begäran, när den är byggd, antalet resulterande varningar med antalet tillgängliga varningar i den aktuella versionen. Om PR leder till en ökning av detta antal, passerar koden inte kvalitetsgrinden för statisk analys. Om antalet varningar minskar eller inte ändras går det över.
  3. Vid nästa release kommer det omräknade antalet varningar att registreras igen i releasemetadata.

Så lite i taget men stadigt (som när en spärrhake fungerar) kommer antalet varningar att tendera till noll. Självklart kan systemet luras genom att införa en ny varning, men korrigera någon annans. Detta är normalt, eftersom det över en lång sträcka ger resultat: varningar korrigeras, som regel, inte individuellt, utan i en grupp av en viss typ på en gång, och alla lätt borttagbara varningar elimineras ganska snabbt.

Denna graf visar det totala antalet Checkstyle-varningar för sex månaders drift av en sådan "spärr" på ett av våra OpenSource-projekt. Antalet varningar har minskat med en storleksordning, och detta skedde naturligt, parallellt med produktutvecklingen!

Implementera statisk analys i processen istället för att använda den för att hitta buggar

Jag använder en modifierad version av den här metoden, som separat räknar varningar av projektmodul och analysverktyg, vilket resulterar i en YAML-fil med byggmetadata som ser ut ungefär så här:

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

I alla avancerade CI-system kan ratchet implementeras för alla statiska analysverktyg utan att förlita sig på plugins och tredjepartsverktyg. Varje analysator producerar sin egen rapport i ett enkelt text- eller XML-format som är lätt att analysera. Allt som återstår är att skriva den nödvändiga logiken i CI-skriptet. Du kan se hur detta implementeras i våra open source-projekt baserade på Jenkins och Artifactory här eller här. Båda exemplen beror på biblioteket ratchetlib: metod countWarnings() räknar xml-taggar i filer som genereras av Checkstyle och Spotbugs på vanligt sätt, och compareWarningMaps() implementerar samma spärrhake, vilket ger ett fel när antalet varningar i någon av kategorierna ökar.

En intressant implementering av "spärren" är möjlig för att analysera stavningen av kommentarer, bokstavliga texter och dokumentation med aspell. Som du vet är inte alla ord som är okända i standardlexikonet felaktiga när du kontrollerar stavningen, de kan läggas till i användarlexikonet. Om du gör en anpassad ordbok till en del av projektets källkod, så kan stavningskvalitetsporten formuleras så här: köra aspell med en standard och anpassad ordbok borde inte hittar inga stavfel.

Om vikten av att fixa analysatorversionen

Sammanfattningsvis är poängen att notera att oavsett hur du implementerar analys i din leveranspipeline, måste versionen av analysatorn fixas. Om du tillåter att analysatorn uppdateras spontant, kan nya defekter "dyka upp" vid montering av nästa pull-begäran som inte är relaterade till kodändringar, utan är relaterade till det faktum att den nya analysatorn helt enkelt kan hitta fler defekter - och detta kommer att bryta din process för att acceptera pull-förfrågningar. Att uppgradera en analysator bör vara en medveten åtgärd. Fast fixering av versionen av varje monteringskomponent är dock i allmänhet ett nödvändigt krav och ett ämne för en separat diskussion.

Resultat

  • Statisk analys kommer inte att hitta buggar för dig och kommer inte att förbättra kvaliteten på din produkt som ett resultat av en enda applikation. En positiv effekt på kvaliteten kan endast uppnås genom att den ständigt används under leveransprocessen.
  • Att hitta buggar är inte alls huvuduppgiften för analys, de allra flesta användbara funktioner är tillgängliga i opensource-verktyg.
  • Implementera kvalitetsgrindar baserat på resultaten av statisk analys i det allra första steget av leveranspipelinen, med hjälp av en "spärr" för äldre kod.

referenser

  1. Kontinuerlig leverans
  2. A. Kudryavtsev: Programanalys: hur man förstår att du är en bra programmerare rapportera om olika metoder för kodanalys (inte bara statisk!)

Källa: will.com

Lägg en kommentar