Implementa análise estática no proceso, en lugar de usala para atopar erros

Moitoume a escribir este artigo a gran cantidade de materiais sobre análise estática que cada vez me chaman máis a atención. En primeiro lugar, isto Blog de PVS-studio, que se promove activamente en Habré coa axuda de revisións de erros atopados pola súa ferramenta en proxectos de código aberto. PVS-studio implementado recentemente Soporte de Java, e, por suposto, os desenvolvedores de IntelliJ IDEA, cuxo analizador integrado probablemente sexa o máis avanzado para Java na actualidade, non podía estar lonxe.

Ao ler tales comentarios, tes a sensación de que estamos a falar dun elixir máxico: preme o botón e aquí está: unha lista de defectos ante os teus ollos. Parece que a medida que melloren os analizadores, atoparanse automaticamente cada vez máis erros, e os produtos escaneados por estes robots irán mellorando, sen ningún esforzo da nosa parte.

Pero non hai elixires máxicos. Gustaríame falar do que normalmente non se fala en publicacións como "aquí están as cousas que o noso robot pode atopar": o que os analizadores non poden facer, cal é o seu papel real e o seu lugar no proceso de entrega de software e como implementalos correctamente. .

Implementa análise estática no proceso, en lugar de usala para atopar erros
Ratchet (fonte: Wikipedia).

O que os analizadores estáticos nunca poden facer

Que é a análise do código fonte, desde o punto de vista práctico? Proporcionamos algún código fonte como entrada e, como saída, en pouco tempo (moito máis curto que realizar probas) obtemos información sobre o noso sistema. A limitación fundamental e matematicamente insuperable é que só podemos obter unha clase de información bastante estreita deste xeito.

O exemplo máis famoso dun problema que non se pode resolver mediante a análise estática é problema de apagado: Este é un teorema que demostra que é imposible desenvolver un algoritmo xeral que poida determinar a partir do código fonte dun programa se se realizará un bucle ou rematará nun tempo finito. Unha extensión deste teorema é Teorema de Rice, que indica que para calquera propiedade non trivial das funcións computables, determinar se un programa arbitrario avalía unha función con tal propiedade é un problema algorítmicamente intratable. Por exemplo, é imposible escribir un analizador que poida determinar a partir de calquera código fonte se o programa que se está a analizar é unha implementación dun algoritmo que calcula, por exemplo, a cuadratura dun número enteiro.

Así, a funcionalidade dos analizadores estáticos ten limitacións insuperables. Un analizador estático nunca poderá detectar en todos os casos cousas como, por exemplo, a aparición dunha "excepción de punteiro nulo" en linguas que permiten o valor de nulo, ou en todos os casos determinar a aparición dun " atributo non encontrado" en linguaxes de tipo dinámico. O único que pode facer o analizador estático máis avanzado é destacar casos especiais, cuxo número, entre todos os posibles problemas co seu código fonte, é, sen esaxeración, unha gota no cubo.

A análise estática non consiste en atopar erros

Do anterior, despréndese a conclusión: a análise estática non é un medio para reducir o número de defectos nun programa. Atreveríame a dicir: cando se aplica ao teu proxecto por primeira vez, atopará lugares "interesantes" no código, pero, moi probablemente, non atopará ningún defecto que afecte á calidade do teu programa.

Os exemplos de defectos atopados automaticamente polos analizadores son impresionantes, pero non debemos esquecer que estes exemplos foron atopados escaneando un gran conxunto de grandes bases de código. Polo mesmo principio, os piratas informáticos que teñen a oportunidade de probar varios contrasinais simples nun gran número de contas acaban por atopar aquelas contas que teñen un contrasinal simple.

Significa isto que non se debe utilizar a análise estática? Por suposto que non! E polo mesmo motivo que paga a pena comprobar cada novo contrasinal para asegurarse de que está incluído na lista de paradas de contrasinais "simples".

A análise estática é máis que atopar erros

De feito, os problemas practicamente resoltos pola análise son moito máis amplos. Despois de todo, en xeral, a análise estática é calquera verificación dos códigos fonte realizada antes de que se poñan en marcha. Aquí tes algunhas cousas que podes facer:

  • Comprobando o estilo de codificación no sentido máis amplo da palabra. Isto inclúe tanto a comprobación do formato, a busca do uso de parénteses baleiros/extra, a definición de limiares en métricas como o número de liñas/a complexidade ciclomática dun método, etc. - calquera cousa que potencialmente impida a lexibilidade e mantemento do código. En Java, tal ferramenta é Checkstyle, en Python - flake8. Os programas desta clase adoitan chamarse "linters".
  • Non só se pode analizar o código executable. Os ficheiros de recursos como JSON, YAML, XML, .properties pódense (e deberían!) comprobarse automaticamente a validez. Despois de todo, é mellor descubrir que a estrutura JSON está rota debido a algunhas comiñas non emparelladas nunha fase inicial da verificación automática de Pull Request que durante a execución da proba ou o tempo de execución? Hai ferramentas axeitadas dispoñibles: p.ex. YAMLint, JSONLint.
  • A compilación (ou análise para linguaxes de programación dinámica) tamén é un tipo de análise estática. En xeral, os compiladores son capaces de producir avisos que indican problemas coa calidade do código fonte e non deben ser ignorados.
  • Ás veces, a compilación é algo máis que compilar código executable. Por exemplo, se tes documentación no formato AsciiDoutor, entón no momento de convertelo en HTML/PDF o manejador AsciiDoctor (Complemento Maven) pode emitir avisos, por exemplo, sobre ligazóns internas rotas. E esta é unha boa razón para non aceptar o Pull Request con cambios na documentación.
  • A corrección ortográfica tamén é un tipo de análise estática. Utilidade aspell é capaz de comprobar a ortografía non só na documentación, senón tamén nos códigos fonte do programa (comentarios e literais) en varias linguaxes de programación, incluíndo C/C++, Java e Python. Un erro ortográfico na interface de usuario ou na documentación tamén é un defecto.
  • Probas de configuración (sobre o que son - ver. isto и isto informes), aínda que se executan nun tempo de execución de proba unitaria como pytest, son de feito tamén un tipo de análise estática, xa que non executan códigos fonte durante a súa execución.

Como podes ver, buscar erros nesta lista xoga o papel menos importante, e todo o demais está dispoñible mediante ferramentas gratuítas de código aberto.

Cal destes tipos de análise estática deberías usar no teu proxecto? Por suposto, canto máis mellor! O principal é implementalo correctamente, o que se comentará máis adiante.

Canalización de entrega como filtro de varias etapas e análise estática como primeira etapa

A metáfora clásica da integración continua é unha canalización a través do cal os cambios flúen, desde os cambios de código fonte ata a entrega ata a produción. A secuencia estándar de etapas nesta canalización ten o seguinte aspecto:

  1. análise estática
  2. recompilación
  3. probas unitarias
  4. probas de integración
  5. Probas de IU
  6. comprobación manual

Os cambios rexeitados na etapa N do gasoduto non se trasladan á etapa N+1.

Por que exactamente deste xeito e non doutro xeito? Na parte de probas do gasoduto, os probadores recoñecerán a coñecida pirámide de probas.

Implementa análise estática no proceso, en lugar de usala para atopar erros
Pirámide de proba. Fonte: artigo Martin Fowler.

Na parte inferior desta pirámide hai probas que son máis fáciles de escribir, máis rápidas de executar e que non teñen tendencia a fallar. Polo tanto, debería haber máis deles, deberían cubrir máis código e executarse primeiro. Na parte superior da pirámide, ocorre o contrario, polo que o número de probas de integración e IU debería reducirse ao mínimo necesario. A persoa desta cadea é o recurso máis caro, lento e pouco fiable, polo que está ao final e só realiza o traballo se as etapas anteriores non atoparon defectos. Non obstante, os mesmos principios utilízanse para construír un oleoduto en partes non directamente relacionadas coas probas.

Gustaríame ofrecer unha analoxía en forma de sistema de filtración de auga en varias etapas. A auga sucia (cambios con defectos) é subministrada á entrada; na saída debemos recibir auga limpa, na que se eliminaron todos os contaminantes non desexados.

Implementa análise estática no proceso, en lugar de usala para atopar erros
Filtro multietapa. Fonte: Wikimedia Commons

Como sabes, os filtros de limpeza están deseñados para que cada cascada posterior poida filtrar unha fracción cada vez máis fina de contaminantes. Ao mesmo tempo, as cascadas de purificación máis grosa teñen un maior rendemento e un menor custo. Na nosa analoxía, isto significa que as portas de calidade de entrada son máis rápidas, requiren menos esforzo para comezar e son por si mesmas máis sen pretensións no seu funcionamento, e esta é a secuencia na que se constrúen. O papel da análise estática, que, como entendemos agora, é capaz de eliminar só os defectos máis groseiros, é o papel da reixa de "barro" ao comezo da cascada do filtro.

A análise estática por si só non mellora a calidade do produto final, do mesmo xeito que un "filtro de barro" non fai potable a auga. E aínda así, en conxunto con outros elementos do gasoduto, a súa importancia é obvia. Aínda que nun filtro de varias etapas as etapas de saída son potencialmente capaces de capturar todo o que fan as etapas de entrada, está claro que consecuencias resultarán dun intento de conformarse só con etapas de purificación fina, sen etapas de entrada.

O propósito da "trampa de barro" é aliviar as fervenzas posteriores de detectar defectos moi graves. Por exemplo, como mínimo, a persoa que realiza a revisión do código non debe distraerse con códigos con formato incorrecto e violacións dos estándares de codificación establecidos (como parénteses adicionais ou ramas aniñadas demasiado profundamente). Erros como os NPE deberían detectarse mediante probas unitarias, pero se mesmo antes da proba o analizador nos indica que un erro está obrigado a ocorrer, isto acelerará significativamente a súa corrección.

Creo que agora está claro por que a análise estática non mellora a calidade do produto se se usa ocasionalmente e debe usarse constantemente para filtrar os cambios con defectos graves. A pregunta de se o uso dun analizador estático mellorará a calidade do teu produto equivale aproximadamente a preguntar: "Mellorarase a calidade de beber a auga extraída dun estanque sucio se se pasa por un colador?"

Implementación nun proxecto legado

Unha cuestión práctica importante: como implementar a análise estática no proceso de integración continua como unha "porta de calidade"? No caso das probas automáticas, todo é obvio: hai un conxunto de probas, a falla de calquera delas é motivo suficiente para crer que a montaxe non pasou a porta de calidade. Un intento de instalar unha porta do mesmo xeito en función dos resultados dunha análise estática falla: hai demasiados avisos de análise no código herdado, non queres ignoralos por completo, pero tamén é imposible deixar de enviar un produto só porque contén advertencias do analizador.

Cando se usa por primeira vez, o analizador produce un gran número de avisos en calquera proxecto, a gran maioría dos cales non están relacionados co bo funcionamento do produto. É imposible corrixir todos estes comentarios á vez, e moitos non son necesarios. Despois de todo, sabemos que o noso produto no seu conxunto funciona, mesmo antes de introducir a análise estática.

Como resultado, moitos limítanse ao uso ocasional da análise estática, ou utilízana só en modo de información, cando simplemente se emite un informe do analizador durante a montaxe. Isto equivale á ausencia de calquera análise, porque se xa temos moitos avisos, a aparición doutro (por moi grave que sexa) ao cambiar o código pasa desapercibido.

Coñécense os seguintes métodos para introducir portas de calidade:

  • Establecer un límite no número total de avisos ou o número de avisos dividido polo número de liñas de código. Isto funciona mal, porque tal porta permite libremente o paso de cambios con novos defectos, sempre que non se supere o seu límite.
  • Corrixindo, nun momento determinado, todas as advertencias antigas do código como ignoradas e negándose a construír cando se producen novas advertencias. Esta funcionalidade é proporcionada por PVS-studio e algúns recursos en liña, por exemplo, Codacy. Non tiven a oportunidade de traballar en PVS-studio, xa que pola miña experiencia con Codacy, o seu principal problema é que determinar o que é un erro "vello" e que é un erro "novo" é un algoritmo bastante complexo que non sempre funciona. correctamente, especialmente se os ficheiros están moi modificados ou renomeados. Segundo a miña experiencia, Codacy podía ignorar as novas advertencias nunha solicitude de extracción, mentres que ao mesmo tempo non pasaba unha solicitude de extracción debido a avisos que non estaban relacionados con cambios no código dun determinado PR.
  • Na miña opinión, a solución máis eficaz é a descrita no libro Entrega continua "método de trinquete". A idea básica é que o número de avisos de análise estática é unha propiedade de cada versión, e só se permiten cambios que non aumenten o número total de avisos.

Trinquete

Funciona deste xeito:

  1. Na fase inicial, realízase un rexistro nos metadatos sobre a publicación do número de avisos no código atopado polos analizadores. Entón, cando creas ascendente, o teu xestor de repositorio escribe non só "versión 7.0.2", senón "versión 7.0.2 que contén 100500 avisos de estilo de verificación". Se usas un xestor de repositorio avanzado (como Artifactory), almacenar eses metadatos sobre a túa versión é doado.
  2. Agora cada solicitude de extracción, cando se crea, compara o número de avisos resultantes co número de avisos dispoñibles na versión actual. Se PR leva a un aumento deste número, entón o código non pasa a porta de calidade para a análise estática. Se o número de avisos diminúe ou non cambia, pasa.
  3. Na próxima versión, o número recalculado de avisos rexistrarase de novo nos metadatos da versión.

Polo que pouco a pouco pero de forma constante (como cando funciona un trinquete), o número de avisos tenderá a cero. Por suposto, o sistema pódese enganar introducindo unha nova advertencia, pero corrixindo a doutra persoa. Isto é normal, porque a longa distancia dá resultados: as advertencias corríxense, por regra xeral, non individualmente, senón nun grupo dun determinado tipo á vez, e todas as advertencias facilmente eliminables elimínanse con bastante rapidez.

Este gráfico mostra o número total de avisos de estilo de verificación durante seis meses de funcionamento de tal "trinquete". un dos nosos proxectos OpenSource. O número de avisos diminuíu nunha orde de magnitude, e isto ocorreu de forma natural, paralelamente ao desenvolvemento do produto.

Implementa análise estática no proceso, en lugar de usala para atopar erros

Utilizo unha versión modificada deste método, contando por separado os avisos por módulo de proxecto e ferramenta de análise, o que resulta nun ficheiro YAML con metadatos de compilación que se parecen a isto:

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 calquera sistema de CI avanzado, o trinquete pódese implementar para calquera ferramenta de análise estática sen depender de complementos e ferramentas de terceiros. Cada analizador produce o seu propio informe nun texto sinxelo ou formato XML que é fácil de analizar. Só queda escribir a lóxica necesaria no script CI. Podes ver como se implementa isto nos nosos proxectos de código aberto baseados en Jenkins e Artifactory aquí ou aquí. Ambos os exemplos dependen da biblioteca trinquetelib: método countWarnings() conta etiquetas xml nos ficheiros xerados por Checkstyle e Spotbugs do xeito habitual, e compareWarningMaps() implementa o mesmo trinquete, lanzando un erro cando aumenta o número de avisos en calquera das categorías.

É posible unha implementación interesante do "trinquete" para analizar a ortografía de comentarios, literais de texto e documentación usando aspell. Como sabes, ao revisar a ortografía, non todas as palabras descoñecidas para o dicionario estándar son incorrectas; pódense engadir ao dicionario do usuario. Se fai un dicionario personalizado parte do código fonte do proxecto, entón a porta de calidade ortográfica pódese formular deste xeito: executando aspell cun dicionario estándar e personalizado non debería non atopar erros de ortografía.

Sobre a importancia de arranxar a versión do analizador

En conclusión, o punto a ter en conta é que non importa como implementes a análise no teu pipeline de entrega, a versión do analizador debe ser corrixida. Se permites que o analizador se actualice espontáneamente, ao montar a seguinte solicitude de extracción, poden aparecer novos defectos que non estean relacionados cos cambios de código, senón que están relacionados co feito de que o novo analizador simplemente pode atopar máis defectos. e isto romperá o teu proceso de aceptación de solicitudes de extracción . A actualización dun analizador debe ser unha acción consciente. Non obstante, a fixación ríxida da versión de cada compoñente de montaxe é xeralmente un requisito necesario e un tema para unha discusión separada.

Descubrimentos

  • A análise estática non atopará erros para ti e non mellorará a calidade do teu produto como resultado dunha única aplicación. Un efecto positivo sobre a calidade só se pode conseguir mediante o seu uso constante durante o proceso de entrega.
  • Buscar erros non é a tarefa principal de análise; a gran maioría das funcións útiles están dispoñibles en ferramentas de código aberto.
  • Implementar portas de calidade baseadas nos resultados da análise estática na primeira etapa do pipeline de entrega, utilizando un "trinquete" para o código heredado.

referencias

  1. Entrega continua
  2. A. Kudryavtsev: Análise de programas: como entender que es un bo programador informe sobre diferentes métodos de análise de código (non só estático!)

Fonte: www.habr.com

Engadir un comentario