Implementer statisk analyse i processen i stedet for at bruge den til at finde fejl

Jeg blev tilskyndet til at skrive denne artikel af den store mængde materialer om statisk analyse, der i stigende grad kommer til min opmærksomhed. For det første dette PVS-studie blog, som aktivt promoverer sig selv på Habré ved hjælp af anmeldelser af fejl fundet af deres værktøj i open source-projekter. For nylig implementeret PVS-studie Java support, og selvfølgelig udviklerne af IntelliJ IDEA, hvis indbyggede analysator nok er den mest avancerede til Java i dag, kunne ikke holde sig væk.

Når du læser sådanne anmeldelser, får du følelsen af, at vi taler om en magisk eliksir: tryk på knappen, og her er den - en liste over defekter foran dine øjne. Det ser ud til, at efterhånden som analysatorerne forbedres, vil flere og flere fejl automatisk blive fundet, og de produkter, der scannes af disse robotter, vil blive bedre og bedre uden nogen indsats fra vores side.

Men der er ingen magiske eliksirer. Jeg vil gerne tale om, hvad der normalt ikke taler om i indlæg som "her er de ting, vores robot kan finde": hvad analysatorer ikke kan gøre, hvad er deres reelle rolle og plads i softwareleveringsprocessen, og hvordan man implementerer dem korrekt .

Implementer statisk analyse i processen i stedet for at bruge den til at finde fejl
Ratchet (kilde: wikipedia).

Hvad statiske analysatorer aldrig kan

Hvad er kildekodeanalyse ud fra et praktisk synspunkt? Vi leverer noget kildekode som input, og som output får vi på kort tid (meget kortere end at køre tests) nogle oplysninger om vores system. Den grundlæggende og matematisk uoverstigelige begrænsning er, at vi kun kan opnå en ret snæver klasse af information på denne måde.

Det mest berømte eksempel på et problem, der ikke kan løses ved hjælp af statisk analyse er nedlukningsproblem: Dette er et teorem, der beviser, at det er umuligt at udvikle en generel algoritme, der ud fra kildekoden til et program kan bestemme, om det vil sløjfe eller afslutte i en begrænset tid. En forlængelse af denne sætning er Rice's teorem, som angiver, at for enhver ikke-triviel egenskab ved beregnelige funktioner er det et algoritmisk vanskeligt problem at bestemme, om et vilkårligt program evaluerer en funktion med en sådan egenskab. For eksempel er det umuligt at skrive en analysator, der ud fra en hvilken som helst kildekode kan afgøre, om det program, der analyseres, er en implementering af en algoritme, der beregner f.eks. kvadreringen af ​​et heltal.

Funktionaliteten af ​​statiske analysatorer har således uoverstigelige begrænsninger. En statisk analysator vil aldrig i alle tilfælde være i stand til at detektere ting som for eksempel forekomsten af ​​en "null pointer undtagelse" på sprog, der tillader værdien af ​​null, eller i alle tilfælde at bestemme forekomsten af ​​en " attribut ikke fundet" på dynamisk indtastede sprog. Alt, hvad den mest avancerede statiske analysator kan gøre, er at fremhæve særlige tilfælde, hvor antallet, blandt alle mulige problemer med din kildekode, uden overdrivelse er en dråbe i bøtten.

Statisk analyse handler ikke om at finde fejl

Ud fra ovenstående følger konklusionen: statisk analyse er ikke et middel til at reducere antallet af defekter i et program. Jeg vil vove at sige: Når det anvendes på dit projekt for første gang, vil det finde "interessante" steder i koden, men højst sandsynligt vil det ikke finde nogen defekter, der påvirker kvaliteten af ​​dit program.

Eksemplerne på defekter, der automatisk findes af analysatorer, er imponerende, men vi bør ikke glemme, at disse eksempler blev fundet ved at scanne et stort sæt store kodebaser. Efter samme princip finder hackere, der har mulighed for at prøve flere simple adgangskoder på et stort antal konti, til sidst de konti, der har en simpel adgangskode.

Betyder det, at statisk analyse ikke bør anvendes? Selvfølgelig ikke! Og af nøjagtig samme grund, at det er værd at tjekke hver ny adgangskode for at sikre, at den er inkluderet på stoplisten over "enkle" adgangskoder.

Statisk analyse er mere end at finde fejl

Faktisk er de problemer, der praktisk talt løses ved analyse, meget bredere. Når alt kommer til alt, generelt er statisk analyse enhver verifikation af kildekoder, der udføres før de lanceres. Her er nogle ting, du kan gøre:

  • Kontrol af kodningsstil i ordets bredeste forstand. Dette inkluderer både kontrol af formatering, søgning efter brug af tomme/ekstra parenteser, indstilling af tærskler på metrikker som antal linjer/cyklomatisk kompleksitet af en metode osv. - alt, hvad der potentielt hæmmer kodens læsbarhed og vedligeholdelighed. I Java er et sådant værktøj Checkstyle, i Python - flake8. Programmer i denne klasse kaldes normalt "linters".
  • Ikke kun eksekverbar kode kan analyseres. Ressourcefiler såsom JSON, YAML, XML, .properties kan (og bør!) automatisk kontrolleres for gyldighed. Det er trods alt bedre at finde ud af, at JSON-strukturen er brudt på grund af nogle uparrede citater på et tidligt stadium af automatisk Pull Request-verifikation end under testudførelse eller kørselstid? Der er passende værktøjer til rådighed: f.eks. YAMLlint, JSONLint.
  • Kompilering (eller parsing for dynamiske programmeringssprog) er også en form for statisk analyse. Generelt er compilere i stand til at producere advarsler, der indikerer problemer med kildekodens kvalitet og bør ikke ignoreres.
  • Nogle gange er kompilering mere end blot at kompilere eksekverbar kode. For eksempel hvis du har dokumentation i formatet AsciiDoctor, så i det øjeblik, hvor den bliver omdannet til HTML/PDF, er AsciiDoctor-handleren (Maven-plugin) kan udsende advarsler, for eksempel om ødelagte interne links. Og dette er en god grund til ikke at acceptere Pull-anmodningen med dokumentationsændringer.
  • Stavekontrol er også en form for statisk analyse. Utility En stavning er i stand til at kontrollere stavning ikke kun i dokumentationen, men også i programmets kildekoder (kommentarer og bogstaver) på forskellige programmeringssprog, herunder C/C++, Java og Python. En stavefejl i brugergrænsefladen eller dokumentationen er også en defekt!
  • Konfigurationstests (om hvad de er - se. dette и dette rapporter), selvom de udføres i en enhedstest-runtime såsom pytest, er de faktisk også en type statisk analyse, da de ikke udfører kildekoder under deres udførelse.

Som du kan se, spiller søgning efter fejl på denne liste den mindst vigtige rolle, og alt andet er tilgængeligt ved at bruge gratis open source-værktøjer.

Hvilken af ​​disse typer statisk analyse skal du bruge i dit projekt? Selvfølgelig, jo flere jo bedre! Det vigtigste er at implementere det korrekt, hvilket vil blive diskuteret yderligere.

Leveringspipeline som et flertrinsfilter og statisk analyse som første trin

Den klassiske metafor for kontinuerlig integration er en pipeline, hvorigennem ændringer flow, fra kildekodeændringer til levering til produktion. Standardsekvensen af ​​stadier i denne pipeline ser sådan ud:

  1. statisk analyse
  2. kompilering
  3. enhedstest
  4. integrationstest
  5. UI test
  6. manuel kontrol

Ændringer, der afvises på den N. fase af rørledningen, overføres ikke til fase N+1.

Hvorfor præcis på denne måde og ikke på anden måde? I testdelen af ​​pipelinen vil testere genkende den velkendte testpyramide.

Implementer statisk analyse i processen i stedet for at bruge den til at finde fejl
Test pyramide. Kilde: artiklen Martin Fowler.

I bunden af ​​denne pyramide er tests, der er nemmere at skrive, hurtigere at udføre og ikke har en tendens til at fejle. Derfor skulle der være flere af dem, de skulle dække mere kode og blive eksekveret først. I toppen af ​​pyramiden er det modsatte tilfældet, så antallet af integrations- og UI-tests bør reduceres til det nødvendige minimum. Personen i denne kæde er den dyreste, langsomme og upålidelige ressource, så han er til sidst og udfører kun arbejdet, hvis de foregående faser ikke fandt nogen defekter. De samme principper bruges dog til at bygge en rørledning i dele, der ikke er direkte relateret til test!

Jeg vil gerne tilbyde en analogi i form af et flertrins vandfiltreringssystem. Snavset vand (skifter med defekter) tilføres indgangen, ved udgangen skal vi modtage rent vand, hvori alle uønskede forureninger er elimineret.

Implementer statisk analyse i processen i stedet for at bruge den til at finde fejl
Flertrins filter. Kilde: Wikimedia Commons

Rengøringsfiltre er som bekendt designet således, at hver efterfølgende kaskade kan filtrere en stadig finere del af forurenende stoffer fra. Samtidig har grovere rensningskaskader højere gennemløb og lavere omkostninger. I vores analogi betyder det, at inputkvalitetsporte er hurtigere, kræver mindre indsats for at starte og i sig selv er mere uhøjtidelige i drift - og det er den sekvens, de er bygget i. Den statiske analyses rolle, som, som vi nu forstår, kun er i stand til at luge ud i de groveste defekter, er rollen som "mudder"-gitteret helt i begyndelsen af ​​filterkaskaden.

Statisk analyse i sig selv forbedrer ikke kvaliteten af ​​slutproduktet, ligesom et "mudderfilter" ikke gør vand drikkeligt. Og alligevel, sammen med andre elementer i pipelinen, er dens betydning indlysende. Selvom udgangstrinene i et flertrinsfilter potentielt er i stand til at fange alt, hvad inputtrinene gør, er det klart, hvilke konsekvenser der vil få af et forsøg på at nøjes med finrensningstrin alene, uden inputtrin.

Formålet med "mudderfælden" er at aflaste efterfølgende kaskader fra at fange meget grove skavanker. For eksempel bør den person, der laver kodegennemgangen, som minimum ikke blive distraheret af forkert formateret kode og overtrædelser af etablerede kodningsstandarder (som ekstra parenteser eller for dybt indlejrede grene). Fejl som NPE'er bør fanges af enhedstests, men hvis analysatoren allerede før testen indikerer for os, at en fejl er bundet til at ske, vil dette fremskynde reparationen betydeligt.

Jeg mener, at det nu er klart, hvorfor statisk analyse ikke forbedrer kvaliteten af ​​produktet, hvis det bruges lejlighedsvis, og bør bruges konstant til at bortfiltrere ændringer med grove defekter. Spørgsmålet om, hvorvidt brugen af ​​en statisk analysator vil forbedre kvaliteten af ​​dit produkt, svarer nogenlunde til at spørge: "Vil vand taget fra en snavset dam blive forbedret i drikkekvaliteten, hvis det føres gennem et dørslag?"

Implementering i et legacy projekt

Et vigtigt praktisk spørgsmål: hvordan implementerer man statisk analyse i den kontinuerlige integrationsproces som en "kvalitetsport"? I tilfælde af automatiske test er alt indlysende: der er et sæt tests, svigt af nogen af ​​dem er tilstrækkelig grund til at tro, at forsamlingen ikke bestod kvalitetsporten. Et forsøg på at installere en gate på samme måde baseret på resultaterne af en statisk analyse mislykkes: der er for mange analyseadvarsler i den gamle kode, du vil ikke helt ignorere dem, men det er også umuligt at stoppe forsendelsen af ​​et produkt bare fordi den indeholder analysatoradvarsler.

Når den bruges for første gang, producerer analysatoren et stort antal advarsler på ethvert projekt, hvoraf langt de fleste ikke er relateret til produktets korrekte funktion. Det er umuligt at rette alle disse kommentarer på én gang, og mange er ikke nødvendige. Når alt kommer til alt, ved vi, at vores produkt som helhed fungerer, selv før vi introducerer statisk analyse!

Som et resultat er mange begrænset til lejlighedsvis brug af statisk analyse, eller bruger den kun i informationstilstand, når en analysatorrapport blot udsendes under montering. Dette svarer til fraværet af enhver analyse, for hvis vi allerede har mange advarsler, så går forekomsten af ​​en anden (uanset hvor alvorlig), når du ændrer koden, ubemærket.

Følgende metoder til at introducere kvalitetsporte er kendte:

  • Indstilling af en grænse for det samlede antal advarsler eller antallet af advarsler divideret med antallet af kodelinjer. Dette fungerer dårligt, fordi en sådan port frit tillader ændringer med nye defekter at passere igennem, så længe deres grænse ikke overskrides.
  • Retter, på et bestemt tidspunkt, alle gamle advarsler i koden som ignoreret, og nægter at bygge, når nye advarsler opstår. Denne funktionalitet leveres af PVS-studio og nogle online ressourcer, for eksempel Codacy. Jeg havde ikke mulighed for at arbejde i PVS-studio, for min erfaring med Codacy er deres hovedproblem, at det er en ret kompleks algoritme at bestemme, hvad der er en "gammel" og hvad der er en "ny" fejl. korrekt, især hvis filer er kraftigt ændret eller omdøbt. Efter min erfaring kunne Codacy ignorere nye advarsler i en pull-anmodning, samtidig med at en pull-anmodning ikke blev godkendt på grund af advarsler, der ikke var relateret til ændringer i koden for en given PR.
  • Efter min mening er den mest effektive løsning den, der er beskrevet i bogen Kontinuerlig tilførsel "skraldemetode". Den grundlæggende idé er, at antallet af statiske analyseadvarsler er en egenskab for hver udgivelse, og kun ændringer er tilladt, som ikke øger det samlede antal advarsler.

Skralde

Det fungerer på denne måde:

  1. I den indledende fase bliver der lavet en registrering i metadataene om frigivelsen af ​​antallet af advarsler i koden fundet af analysatorerne. Så når du bygger opstrøms, skriver din repository manager ikke bare "release 7.0.2", men "release 7.0.2 indeholdende 100500 checkstyle-advarsler." Hvis du bruger en avanceret repository manager (såsom Artifactory), er det nemt at gemme sådanne metadata om din udgivelse.
  2. Nu sammenligner hver pull-anmodning, når den er bygget, antallet af resulterende advarsler med antallet af tilgængelige advarsler i den aktuelle udgivelse. Hvis PR fører til en stigning i dette antal, så passerer koden ikke kvalitetsporten til statisk analyse. Hvis antallet af advarsler falder eller ikke ændres, så går det over.
  3. Ved næste udgivelse vil det genberegnet antal advarsler blive registreret igen i udgivelsens metadata.

Så lidt efter lidt, men støt (som når en skralde virker), vil antallet af advarsler have en tendens til nul. Systemet kan selvfølgelig snydes ved at indføre en ny advarsel, men korrigere en andens. Dette er normalt, fordi det over en lang afstand giver resultater: advarsler korrigeres som regel ikke individuelt, men i en gruppe af en bestemt type på én gang, og alle let aftagelige advarsler elimineres ret hurtigt.

Denne graf viser det samlede antal Checkstyle-advarsler for seks måneders drift af en sådan "skralde" på et af vores OpenSource-projekter. Antallet af advarsler er faldet med en størrelsesorden, og det skete naturligt sideløbende med produktudviklingen!

Implementer statisk analyse i processen i stedet for at bruge den til at finde fejl

Jeg bruger en modificeret version af denne metode, der separat tæller advarsler efter projektmodul og analyseværktøj, hvilket resulterer i en YAML-fil med build-metadata, der ser sådan ud:

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 ethvert avanceret CI-system kan ratchet implementeres til alle statiske analyseværktøjer uden at være afhængige af plugins og tredjepartsværktøjer. Hver analysator producerer sin egen rapport i et simpelt tekst- eller XML-format, der er let at analysere. Tilbage er blot at skrive den nødvendige logik i CI-scriptet. Du kan se, hvordan dette implementeres i vores open source-projekter baseret på Jenkins og Artifactory her eller her. Begge eksempler afhænger af biblioteket ratchetlib: metode countWarnings() tæller xml-tags i filer genereret af Checkstyle og Spotbugs på sædvanlig måde, og compareWarningMaps() implementerer den samme skralde og giver en fejl, når antallet af advarsler i en af ​​kategorierne stiger.

En interessant implementering af "ratchet" er mulig til at analysere stavemåden af ​​kommentarer, bogstavelige tekster og dokumentation ved hjælp af aspell. Som du ved, er det ikke alle ord, der er ukendte for standardordbogen, der er forkerte, når de stavekontrol, de kan tilføjes til brugerordbogen. Hvis du laver en brugerdefineret ordbog til en del af projektets kildekode, så kan stavekvalitetsporten formuleres på denne måde: at køre aspell med en standard og brugerdefineret ordbog bør ikke finder ingen stavefejl.

Om vigtigheden af ​​at fikse analysatorversionen

Afslutningsvis er pointen at bemærke, at uanset hvordan du implementerer analyse i din leveringspipeline, skal versionen af ​​analysatoren rettes. Hvis du tillader analysatoren at opdatere spontant, så når du samler den næste pull-anmodning, kan nye defekter "dukke op", som ikke er relateret til kodeændringer, men er relateret til det faktum, at den nye analysator simpelthen er i stand til at finde flere defekter - og dette vil bryde din proces med at acceptere pull-anmodninger. Opgradering af en analysator bør være en bevidst handling. Imidlertid er stiv fiksering af versionen af ​​hver samlingskomponent generelt et nødvendigt krav og et emne for en separat diskussion.

Fund

  • Statisk analyse vil ikke finde fejl for dig og vil ikke forbedre kvaliteten af ​​dit produkt som et resultat af en enkelt applikation. En positiv effekt på kvaliteten kan kun opnås ved konstant brug under leveringsprocessen.
  • At finde fejl er slet ikke analysens hovedopgave; langt de fleste nyttige funktioner er tilgængelige i opensource-værktøjer.
  • Implementer kvalitetsporte baseret på resultaterne af statisk analyse i det allerførste trin af leveringspipelinen, ved hjælp af en "skralde" for ældre kode.

RЎSЃS <P "RєRё

  1. Kontinuerlig tilførsel
  2. A. Kudryavtsev: Programanalyse: hvordan man forstår, at du er en god programmør rapport om forskellige metoder til kodeanalyse (ikke kun statisk!)

Kilde: www.habr.com

Tilføj en kommentar