Implementeu l'anàlisi estàtica al procés, en lloc d'utilitzar-la per trobar errors

Em va impulsar a escriure aquest article la gran quantitat de materials sobre anàlisi estàtica que cada cop em criden més l'atenció. En primer lloc, això Bloc PVS-studio, que es promociona activament a Habré amb l'ajuda de revisions d'errors trobats per la seva eina en projectes de codi obert. Recentment s'ha implementat PVS-studio Suport Java, i, per descomptat, els desenvolupadors d'IntelliJ IDEA, l'analitzador integrat dels quals és probablement el més avançat per a Java actualment, no podia allunyar-se.

Quan llegiu aquestes ressenyes, teniu la sensació que estem parlant d'un elixir màgic: premeu el botó i aquí teniu una llista de defectes davant els vostres ulls. Sembla que a mesura que millorin els analitzadors, automàticament es trobaran més errors, i els productes escanejats per aquests robots seran cada cop millors, sense cap esforç per part nostra.

Però no hi ha elixirs màgics. M'agradaria parlar del que normalment no es parla en publicacions com "aquí són les coses que pot trobar el nostre robot": què no poden fer els analitzadors, quin és el seu paper real i el seu lloc en el procés de lliurament del programari i com implementar-los correctament. .

Implementeu l'anàlisi estàtica al procés, en lloc d'utilitzar-la per trobar errors
Ratchet (font: Wikipedia).

El que mai poden fer els analitzadors estàtics

Què és l'anàlisi del codi font, des d'un punt de vista pràctic? Proporcionem algun codi font com a entrada, i com a sortida, en poc temps (molt més curt que executar proves) obtenim informació sobre el nostre sistema. La limitació fonamental i matemàticament insuperable és que només podem obtenir una classe d'informació força limitada d'aquesta manera.

L'exemple més famós d'un problema que no es pot resoldre mitjançant l'anàlisi estàtica és problema de tancament: Aquest és un teorema que demostra que és impossible desenvolupar un algorisme general que pugui determinar a partir del codi font d'un programa si es farà un bucle o finalitzarà en un temps finit. Una extensió d'aquest teorema és Teorema de Rice, que estableix que per a qualsevol propietat no trivial de funcions computables, determinar si un programa arbitrari avalua una funció amb aquesta propietat és un problema algorítmicament intractable. Per exemple, és impossible escriure un analitzador que pugui determinar a partir de qualsevol codi font si el programa que s'està analitzant és una implementació d'un algorisme que calcula, per exemple, el quadrat d'un nombre enter.

Així, la funcionalitat dels analitzadors estàtics té limitacions insuperables. Un analitzador estàtic mai no podrà detectar en tots els casos coses com, per exemple, l'ocurrència d'una "excepció de punter nul" en idiomes que permeten el valor de nul, o en tots els casos determinar l'ocurrència d'una " no s'ha trobat l'atribut" en idiomes escrits dinàmicament. L'únic que pot fer l'analitzador estàtic més avançat és destacar casos especials, el nombre dels quals, entre tots els possibles problemes amb el vostre codi font, és, sense exagerar, una gota a l'oceà.

L'anàlisi estàtica no es tracta de trobar errors

De l'anterior, se'n desprèn la conclusió: l'anàlisi estàtica no és un mitjà per reduir el nombre de defectes d'un programa. M'atreviria a dir: quan s'apliqui al vostre projecte per primera vegada, trobarà llocs "interessants" al codi, però, molt probablement, no trobarà cap defecte que afecti la qualitat del vostre programa.

Els exemples de defectes trobats automàticament pels analitzadors són impressionants, però no hem d'oblidar que aquests exemples es van trobar escanejant un gran conjunt de bases de codi grans. Pel mateix principi, els pirates informàtics que tenen l'oportunitat de provar diverses contrasenyes simples en un gran nombre de comptes finalment troben aquells comptes que tenen una contrasenya simple.

Vol dir això que no s'ha d'utilitzar l'anàlisi estàtica? És clar que no! I exactament per la mateixa raó que val la pena comprovar cada nova contrasenya per assegurar-se que s'inclou a la llista de parades de contrasenyes "simples".

L'anàlisi estàtica és més que trobar errors

De fet, els problemes pràcticament resolts per l'anàlisi són molt més amplis. Al cap i a la fi, en general, l'anàlisi estàtica és qualsevol verificació dels codis font realitzada abans de llançar-los. Aquí teniu algunes coses que podeu fer:

  • Comprovació de l'estil de codificació en el sentit més ampli de la paraula. Això inclou tant la comprovació del format, la recerca de l'ús de parèntesis buits/extra, l'establiment de llindars en mètriques com el nombre de línies/la complexitat ciclomàtica d'un mètode, etc. - qualsevol cosa que pugui impedir la llegibilitat i el manteniment del codi. A Java, aquesta eina és Checkstyle, en Python - flake8. Els programes d'aquesta classe solen anomenar-se "linters".
  • No només es pot analitzar el codi executable. Els fitxers de recursos com ara JSON, YAML, XML, .properties es poden (i haurien!) ser verificats automàticament per a la validesa. Al cap i a la fi, és millor esbrinar que l'estructura JSON està trencada a causa d'algunes cometes no aparellades en una fase inicial de la verificació automàtica de la sol·licitud d'extracció que durant l'execució de la prova o el temps d'execució? Les eines adequades estan disponibles: p. YAMLint, JSONLint.
  • La compilació (o anàlisi per a llenguatges de programació dinàmics) també és un tipus d'anàlisi estàtica. En general, els compiladors són capaços de produir avisos que indiquen problemes amb la qualitat del codi font i no s'han d'ignorar.
  • De vegades, la compilació és més que compilar codi executable. Per exemple, si teniu documentació en el format AsciiDoctor, llavors en el moment de convertir-lo en HTML/PDF el controlador AsciiDoctor (Complement Maven) pot emetre advertències, per exemple, sobre enllaços interns trencats. I aquesta és una bona raó per no acceptar la Pull Request amb canvis de documentació.
  • La correcció ortogràfica també és un tipus d'anàlisi estàtica. Utilitat aspell és capaç de comprovar l'ortografia no només a la documentació, sinó també als codis font del programa (comentaris i literals) en diversos llenguatges de programació, inclosos C/C++, Java i Python. Un error ortogràfic a la interfície d'usuari o a la documentació també és un defecte!
  • Proves de configuració (sobre què són - vegeu. aquest и aquest informes), tot i que s'executen en un temps d'execució de prova unitària com pytest, de fet també són un tipus d'anàlisi estàtica, ja que no executen codis font durant la seva execució.

Com podeu veure, la cerca d'errors en aquesta llista té el paper menys important, i tota la resta està disponible mitjançant eines gratuïtes de codi obert.

Quin d'aquests tipus d'anàlisi estàtica hauríeu d'utilitzar al vostre projecte? Per descomptat, com més millor! El més important és implementar-lo correctament, cosa que es comentarà més endavant.

Conducte de lliurament com a filtre multietapa i anàlisi estàtica com a primera etapa

La metàfora clàssica de la integració contínua és una canalització a través del qual flueixen els canvis, des dels canvis al codi font fins al lliurament a la producció. La seqüència estàndard d'etapes d'aquesta canalització és la següent:

  1. anàlisi estàtica
  2. compilació
  3. proves unitàries
  4. proves d'integració
  5. Proves d'IU
  6. verificació manual

Els canvis rebutjats a l'etapa N del gasoducte no es traslladen a l'etapa N+1.

Per què exactament d'aquesta manera i no d'una altra manera? A la part de proves del gasoducte, els provadors reconeixeran la coneguda piràmide de proves.

Implementeu l'anàlisi estàtica al procés, en lloc d'utilitzar-la per trobar errors
Piràmide de prova. Font: article Martin Fowler.

A la part inferior d'aquesta piràmide hi ha proves que són més fàcils d'escriure, més ràpides d'executar i que no tenen tendència a fallar. Per tant, n'hi hauria d'haver-ne més, haurien de cobrir més codi i ser executats primer. A la part superior de la piràmide, passa el contrari, de manera que el nombre de proves d'integració i d'IU s'hauria de reduir al mínim necessari. La persona d'aquesta cadena és el recurs més car, lent i poc fiable, de manera que està al final i només realitza el treball si les etapes anteriors no van trobar cap defecte. Tanmateix, s'utilitzen els mateixos principis per construir una canonada en parts que no estan directament relacionades amb les proves!

M'agradaria oferir una analogia en forma d'un sistema de filtració d'aigua de diverses etapes. L'aigua bruta (canvia amb defectes) es subministra a l'entrada; a la sortida hem de rebre aigua neta, en la qual s'han eliminat tots els contaminants no desitjats.

Implementeu l'anàlisi estàtica al procés, en lloc d'utilitzar-la per trobar errors
Filtre multietapa. Font: Wikimedia Commons

Com sabeu, els filtres de neteja estan dissenyats perquè cada cascada posterior pugui filtrar una fracció cada cop més fina de contaminants. Al mateix temps, les cascades de purificació més gruixuda tenen un rendiment més elevat i un cost més baix. En la nostra analogia, això vol dir que les portes de qualitat d'entrada són més ràpides, requereixen menys esforç per començar i que funcionen amb menys pretensions, i aquesta és la seqüència en què es construeixen. El paper de l'anàlisi estàtica, que, com ara entenem, és capaç d'eliminar només els defectes més grossos, és el paper de la graella de "fang" al principi de la cascada del filtre.

L'anàlisi estàtica per si sola no millora la qualitat del producte final, de la mateixa manera que un "filtre de fang" no fa que l'aigua sigui potable. I tanmateix, juntament amb altres elements del gasoducte, la seva importància és òbvia. Tot i que en un filtre multietapa les etapes de sortida són potencialment capaços de capturar tot el que fan les etapes d'entrada, està clar quines conseqüències es produiran d'un intent de conformar-se només amb etapes de purificació fina, sense etapes d'entrada.

El propòsit de la "trampa de fang" és alleujar les cascades posteriors de la captura de defectes molt greus. Per exemple, com a mínim, la persona que fa la revisió del codi no s'ha de distreure amb un codi amb un format incorrecte i les infraccions dels estàndards de codificació establerts (com ara parèntesis addicionals o branques massa imbricades). Els errors com els NPE haurien de ser detectats per les proves d'unitat, però si fins i tot abans de la prova l'analitzador ens indica que es produirà un error, això accelerarà significativament la seva correcció.

Crec que ara està clar per què l'anàlisi estàtica no millora la qualitat del producte si s'utilitza ocasionalment, i s'ha d'utilitzar constantment per filtrar els canvis amb defectes greus. La qüestió de si l'ús d'un analitzador estàtic millorarà la qualitat del vostre producte és aproximadament equivalent a preguntar-se: "L'aigua presa d'un estany brut millorarà la qualitat de la beguda si es passa per un colador?"

Implementació en un projecte heretat

Una qüestió pràctica important: com implementar l'anàlisi estàtica en el procés d'integració contínua com a "porta de qualitat"? En el cas de les proves automàtiques, tot és evident: hi ha un conjunt de proves, el fracàs d'alguna d'elles és motiu suficient per creure que el muntatge no va passar la porta de qualitat. Un intent d'instal·lar una porta de la mateixa manera a partir dels resultats d'una anàlisi estàtica falla: hi ha massa avisos d'anàlisi al codi heretat, no voleu ignorar-los completament, però també és impossible deixar d'enviar un producte. només perquè conté advertències de l'analitzador.

Quan s'utilitza per primera vegada, l'analitzador produeix un gran nombre d'avisos en qualsevol projecte, la gran majoria dels quals no estan relacionats amb el bon funcionament del producte. És impossible corregir tots aquests comentaris alhora, i molts no són necessaris. Al cap i a la fi, sabem que el nostre producte en conjunt funciona, fins i tot abans d'introduir l'anàlisi estàtica!

Com a resultat, molts es limiten a l'ús ocasional de l'anàlisi estàtica, o només l'utilitzen en mode d'informació, quan simplement s'emet un informe de l'analitzador durant el muntatge. Això equival a l'absència de cap anàlisi, perquè si ja tenim molts avisos, passa desapercebut l'aparició d'un altre (per greu que sigui) en canviar el codi.

Es coneixen els mètodes següents per introduir portes de qualitat:

  • Establir un límit al nombre total d'avisos o al nombre d'avisos dividit pel nombre de línies de codi. Això funciona malament, perquè aquesta porta permet que passin lliurement canvis amb nous defectes, sempre que no se superi el seu límit.
  • Arreglar, en un moment determinat, totes les advertències antigues del codi ignorades i negar-se a crear quan es produeixin avisos nous. Aquesta funcionalitat la proporciona PVS-studio i alguns recursos en línia, per exemple, Codacy. No vaig tenir l'oportunitat de treballar a PVS-studio, pel que fa a la meva experiència amb Codacy, el seu principal problema és que determinar què és un error "vell" i què és un "nou" és un algorisme força complex que no sempre funciona. correctament, especialment si els fitxers estan molt modificats o canviats de nom. Segons la meva experiència, Codacy podria ignorar els nous avisos en una sol·licitud d'extracció, alhora que no passava una sol·licitud d'extracció a causa d'avisos que no estaven relacionats amb canvis en el codi d'un PR determinat.
  • Al meu entendre, la solució més eficaç és la que es descriu al llibre lliurament continu "Mètode de trinquet". La idea bàsica és que el nombre d'avises d'anàlisi estàtica és una propietat de cada llançament, i només es permeten canvis que no augmentin el nombre total d'avises.

Trinquet

Funciona d'aquesta manera:

  1. En l'etapa inicial, es fa un registre a les metadades sobre l'alliberament del nombre d'avises en el codi trobat pels analitzadors. Per tant, quan creeu aigües amunt, el vostre gestor de dipòsits escriu no només la "versió 7.0.2", sinó "la versió 7.0.2 que conté 100500 avisos d'estil de verificació". Si utilitzeu un gestor de dipòsits avançat (com Artifactory), emmagatzemar aquestes metadades sobre la vostra versió és fàcil.
  2. Ara, cada sol·licitud d'extracció, quan es crea, compara el nombre d'avisos resultants amb el nombre d'avisos disponibles a la versió actual. Si PR condueix a un augment d'aquest nombre, aleshores el codi no passa la porta de qualitat per a l'anàlisi estàtica. Si el nombre d'avisos disminueix o no canvia, passa.
  3. A la propera versió, el nombre d'advertiments recalculat es tornarà a registrar a les metadades de la versió.

Així, a poc a poc però de manera constant (com quan funciona un trinquet), el nombre d'avisos tendirà a zero. Per descomptat, el sistema es pot enganyar introduint un nou avís, però corregint el d'una altra persona. Això és normal, perquè a llarga distància dóna resultats: els avisos es corregeixen, per regla general, no individualment, sinó en un grup d'un determinat tipus alhora, i tots els avisos fàcilment eliminables s'eliminen amb força rapidesa.

Aquest gràfic mostra el nombre total d'avisos d'estil de verificació durant sis mesos de funcionament d'aquest "trinquet". un dels nostres projectes OpenSource. El nombre d'avisos ha disminuït en un ordre de magnitud, i això va passar de manera natural, paral·lelament al desenvolupament del producte!

Implementeu l'anàlisi estàtica al procés, en lloc d'utilitzar-la per trobar errors

Utilitzo una versió modificada d'aquest mètode, comptant per separat els avisos per mòdul de projecte i eina d'anàlisi, donant com a resultat un fitxer YAML amb metadades de compilació que s'assembla a això:

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

En qualsevol sistema CI avançat, el trinquet es pot implementar per a qualsevol eina d'anàlisi estàtica sense dependre de complements i eines de tercers. Cada analitzador produeix el seu propi informe en un format de text o XML senzill que és fàcil d'analitzar. Només queda escriure la lògica necessària a l'script CI. Podeu veure com s'implementa això als nostres projectes de codi obert basats en Jenkins i Artifactory aquí o aquí. Tots dos exemples depenen de la biblioteca ratchetlib: mètode countWarnings() compta les etiquetes xml als fitxers generats per Checkstyle i Spotbugs de la manera habitual, i compareWarningMaps() implementa el mateix trinquet, llançant un error quan augmenta el nombre d'avisos en qualsevol de les categories.

És possible una implementació interessant del "trinquet" per analitzar l'ortografia de comentaris, literals de text i documentació mitjançant aspell. Com sabeu, en comprovar l'ortografia, no totes les paraules desconegudes al diccionari estàndard són incorrectes; es poden afegir al diccionari de l'usuari. Si feu que un diccionari personalitzat forme part del codi font del projecte, la porta de qualitat ortogràfica es pot formular d'aquesta manera: executant aspell amb un diccionari estàndard i personalitzat. no hauria de fer-ho no trobar errors ortogràfics.

Sobre la importància d'arreglar la versió de l'analitzador

En conclusió, el punt a tenir en compte és que, independentment de com implementeu l'anàlisi al vostre pipeline de lliurament, la versió de l'analitzador s'ha de solucionar. Si permeteu que l'analitzador s'actualitzi espontàniament, en muntar la següent sol·licitud d'extracció, poden "aparèixer" nous defectes que no estiguin relacionats amb els canvis de codi, sinó que estiguin relacionats amb el fet que el nou analitzador simplement pot trobar més defectes: i això trencarà el vostre procés d'acceptació de sol·licituds d'extracció. L'actualització d'un analitzador hauria de ser una acció conscient. Tanmateix, la fixació rígida de la versió de cada component de muntatge és generalment un requisit necessari i un tema per a una discussió separada.

Troballes

  • L'anàlisi estàtica no us trobarà errors i no millorarà la qualitat del vostre producte com a resultat d'una sola aplicació. Un efecte positiu sobre la qualitat només es pot aconseguir mitjançant el seu ús constant durant el procés de lliurament.
  • Trobar errors no és en absolut la tasca principal d'anàlisi; la gran majoria de funcions útils estan disponibles a les eines de codi obert.
  • Implementeu portes de qualitat basades en els resultats de l'anàlisi estàtica en la primera etapa del pipeline de lliurament, utilitzant un "trinquet" per al codi heretat.

Referències

  1. lliurament continu
  2. A. Kudryavtsev: Anàlisi de programes: com entendre que sou un bon programador informe sobre diferents mètodes d'anàlisi de codi (no només estàtic!)

Font: www.habr.com

Afegeix comentari