Implemente análise estática no processo, em vez de usá-la para encontrar bugs

Fui levado a escrever este artigo pela grande quantidade de materiais sobre análise estática que cada vez mais chamam minha atenção. Em primeiro lugar, este Blog do estúdio PVS, que se promove ativamente no Habré com a ajuda de análises de erros encontrados por sua ferramenta em projetos de código aberto. Recentemente PVS-studio implementado Suporte Javae, claro, os desenvolvedores do IntelliJ IDEA, cujo analisador integrado é provavelmente o mais avançado para Java atualmente, não conseguia ficar longe.

Ao ler essas resenhas, você tem a sensação de que estamos falando de um elixir mágico: aperte o botão e aqui está - uma lista de defeitos diante de seus olhos. Parece que à medida que os analisadores melhoram, mais e mais bugs serão encontrados automaticamente, e os produtos escaneados por esses robôs ficarão cada vez melhores, sem nenhum esforço de nossa parte.

Mas não existem elixires mágicos. Gostaria de falar sobre o que normalmente não é falado em posts como “aqui estão as coisas que nosso robô pode encontrar”: o que os analisadores não podem fazer, qual é o seu real papel e lugar no processo de entrega de software e como implementá-los corretamente .

Implemente análise estática no processo, em vez de usá-la para encontrar bugs
Catraca (fonte: wikipedia).

O que os analisadores estáticos nunca podem fazer

O que é análise de código-fonte, do ponto de vista prático? Fornecemos algum código fonte como entrada, e como saída, em um curto espaço de tempo (muito menor que a execução de testes) obtemos algumas informações sobre nosso sistema. A limitação fundamental e matematicamente intransponível é que só podemos obter desta forma uma classe bastante restrita de informação.

O exemplo mais famoso de problema que não pode ser resolvido usando análise estática é problema de desligamento: Este é um teorema que prova que é impossível desenvolver um algoritmo geral que possa determinar a partir do código-fonte de um programa se ele entrará em loop ou terminará em um tempo finito. Uma extensão deste teorema é Teorema de arroz, que afirma que para qualquer propriedade não trivial de funções computáveis, determinar se um programa arbitrário avalia uma função com tal propriedade é um problema algoritmicamente intratável. Por exemplo, é impossível escrever um analisador que possa determinar, a partir de qualquer código-fonte, se o programa que está sendo analisado é uma implementação de um algoritmo que calcula, digamos, a quadratura de um número inteiro.

Assim, a funcionalidade dos analisadores estáticos tem limitações intransponíveis. Um analisador estático nunca será capaz de detectar em todos os casos coisas como, por exemplo, a ocorrência de uma "exceção de ponteiro nulo" em linguagens que permitem o valor nulo, ou em todos os casos determinar a ocorrência de um " atributo não encontrado" em linguagens de tipo dinâmico. Tudo o que o analisador estático mais avançado pode fazer é destacar casos especiais, cujo número, entre todos os possíveis problemas com o seu código-fonte, é, sem exagero, uma gota no oceano.

A análise estática não consiste em encontrar bugs

Do exposto, segue-se a conclusão: a análise estática não é um meio de reduzir o número de defeitos em um programa. Atrevo-me a dizer: quando aplicado ao seu projeto pela primeira vez, ele encontrará lugares “interessantes” no código, mas, muito provavelmente, não encontrará nenhum defeito que afete a qualidade do seu programa.

Os exemplos de defeitos encontrados automaticamente pelos analisadores são impressionantes, mas não devemos esquecer que esses exemplos foram encontrados através da varredura de um grande conjunto de grandes bases de código. Pelo mesmo princípio, os hackers que têm a oportunidade de tentar várias senhas simples em um grande número de contas acabam encontrando as contas que possuem uma senha simples.

Isso significa que a análise estática não deve ser usada? Claro que não! E exatamente pelo mesmo motivo que vale a pena verificar cada nova senha para ter certeza de que ela está incluída na lista de senhas “simples”.

A análise estática é mais do que encontrar bugs

Na verdade, os problemas praticamente resolvidos pela análise são muito mais amplos. Afinal, em geral, análise estática é qualquer verificação de códigos-fonte realizada antes de serem lançados. Aqui estão algumas coisas que você pode fazer:

  • Verificando o estilo de codificação no sentido mais amplo da palavra. Isso inclui verificar a formatação, procurar o uso de parênteses vazios/extras, definir limites em métricas como número de linhas/complexidade ciclomática de um método, etc. - qualquer coisa que potencialmente impeça a legibilidade e a manutenção do código. Em Java, essa ferramenta é Checkstyle, em Python - flake8. Os programas desta classe são geralmente chamados de “linters”.
  • Não apenas o código executável pode ser analisado. Arquivos de recursos como JSON, YAML, XML, .properties podem (e devem!) ser verificados automaticamente quanto à validade. Afinal, é melhor descobrir que a estrutura JSON está quebrada devido a algumas cotações não pareadas em um estágio inicial da verificação automática do Pull Request do que durante a execução do teste ou tempo de execução? Ferramentas apropriadas estão disponíveis: por ex. YAMLlint, JSONLint.
  • Compilação (ou análise para linguagens de programação dinâmicas) também é um tipo de análise estática. Em geral, os compiladores são capazes de produzir avisos que indicam problemas com a qualidade do código-fonte e não devem ser ignorados.
  • Às vezes, a compilação é mais do que apenas compilar código executável. Por exemplo, se você tiver documentação no formato AsciiDoctor, então no momento de transformá-lo em HTML/PDF o manipulador AsciiDoctor (Plugin Maven) pode emitir avisos, por exemplo, sobre links internos quebrados. E esse é um bom motivo para não aceitar o Pull Request com alterações na documentação.
  • A verificação ortográfica também é um tipo de análise estática. Utilitário um feitiço é capaz de verificar a ortografia não apenas na documentação, mas também em códigos-fonte de programas (comentários e literais) em diversas linguagens de programação, incluindo C/C++, Java e Python. Um erro ortográfico na interface do usuário ou na documentação também é um defeito!
  • Testes de configuração (sobre o que são - veja. este и este relatórios), embora executados em um tempo de execução de teste unitário como o pytest, são na verdade também um tipo de análise estática, uma vez que não executam códigos-fonte durante sua execução.

Como você pode ver, a busca por bugs nesta lista desempenha o papel menos importante, e todo o resto está disponível usando ferramentas gratuitas de código aberto.

Qual desses tipos de análise estática você deve usar em seu projeto? Claro, quanto mais, melhor! O principal é implementá-lo corretamente, o que será discutido mais adiante.

Pipeline de entrega como filtro de vários estágios e análise estática como primeiro estágio

A metáfora clássica para integração contínua é um pipeline através do qual as mudanças fluem, desde as alterações no código-fonte até a entrega à produção. A sequência padrão de estágios neste pipeline é semelhante a esta:

  1. análise estática
  2. compilação
  3. testes unitários
  4. testes de integração
  5. Testes de IU
  6. verificação manual

As alterações rejeitadas no enésimo estágio do pipeline não são transferidas para o estágio N+1.

Por que exatamente desta forma e não de outra forma? Na parte de teste do pipeline, os testadores reconhecerão a conhecida pirâmide de testes.

Implemente análise estática no processo, em vez de usá-la para encontrar bugs
Pirâmide de teste. Fonte: artigo Martin Fowler.

Na base desta pirâmide estão os testes que são mais fáceis de escrever, mais rápidos de executar e que não têm tendência a falhar. Portanto, deveria haver mais deles, deveriam cobrir mais código e ser executados primeiro. No topo da pirâmide, o oposto é verdadeiro, portanto o número de testes de integração e UI deve ser reduzido ao mínimo necessário. A pessoa dessa cadeia é o recurso mais caro, lento e pouco confiável, por isso ela está bem no final e só executa o trabalho se as etapas anteriores não encontraram nenhum defeito. No entanto, os mesmos princípios são usados ​​para construir um pipeline em partes não diretamente relacionadas aos testes!

Gostaria de oferecer uma analogia na forma de um sistema de filtragem de água em vários estágios. Na entrada é fornecida água suja (mudanças com defeitos), na saída devemos receber água limpa, na qual todos os contaminantes indesejados foram eliminados.

Implemente análise estática no processo, em vez de usá-la para encontrar bugs
Filtro multiestágio. Fonte: Wikimedia Commons

Como você sabe, os filtros de limpeza são projetados para que cada cascata subsequente possa filtrar uma fração cada vez mais fina de contaminantes. Ao mesmo tempo, cascatas de purificação mais grosseiras têm maior rendimento e menor custo. Em nossa analogia, isso significa que os portões de qualidade de entrada são mais rápidos, exigem menos esforço para serem iniciados e são, eles próprios, mais despretensiosos na operação – e esta é a sequência em que são construídos. O papel da análise estática, que, como entendemos agora, é capaz de eliminar apenas os defeitos mais grosseiros, é o papel da grade de “lama” logo no início da cascata de filtros.

A análise estática por si só não melhora a qualidade do produto final, assim como um “filtro de lama” não torna a água potável. E, no entanto, em conjunto com outros elementos do pipeline, a sua importância é óbvia. Embora em um filtro multiestágio os estágios de saída sejam potencialmente capazes de capturar tudo o que os estágios de entrada fazem, é claro quais consequências resultarão de uma tentativa de se contentar apenas com estágios de purificação fina, sem estágios de entrada.

O objetivo da “armadilha de lama” é evitar que as cascatas subsequentes capturem defeitos muito grosseiros. Por exemplo, no mínimo, a pessoa que faz a revisão do código não deve se distrair com códigos formatados incorretamente e violações dos padrões de codificação estabelecidos (como parênteses extras ou ramificações muito aninhadas). Bugs como NPEs devem ser detectados por testes unitários, mas se antes mesmo do teste o analisador nos indicar que um bug está prestes a acontecer, isso irá acelerar significativamente sua correção.

Acredito que agora esteja claro por que a análise estática não melhora a qualidade do produto se usada ocasionalmente e deve ser usada constantemente para filtrar alterações com defeitos grosseiros. A questão de saber se o uso de um analisador estático melhorará a qualidade do seu produto é aproximadamente equivalente a perguntar: “A água retirada de um lago sujo melhorará a qualidade do consumo se passar por uma peneira?”

Implementação em um projeto legado

Uma questão prática importante: como implementar a análise estática no processo de integração contínua como uma “porta de qualidade”? No caso dos testes automáticos, tudo é óbvio: existe um conjunto de testes, a falha em algum deles é motivo suficiente para acreditar que a montagem não passou pela porta da qualidade. Uma tentativa de instalar um portão da mesma forma com base nos resultados de uma análise estática falha: há muitos avisos de análise no código legado, você não quer ignorá-los completamente, mas também é impossível parar de enviar um produto só porque contém avisos do analisador.

Ao ser utilizado pela primeira vez, o analisador produz um grande número de avisos sobre qualquer projeto, a grande maioria dos quais não relacionados ao bom funcionamento do produto. É impossível corrigir todos esses comentários de uma só vez e muitos deles não são necessários. Afinal, sabemos que nosso produto como um todo funciona, antes mesmo de introduzir a análise estática!

Como resultado, muitos se limitam ao uso ocasional de análise estática, ou a utilizam apenas no modo de informação, quando um relatório do analisador é simplesmente emitido durante a montagem. Isso equivale à ausência de qualquer análise, pois se já tivermos muitos avisos, então a ocorrência de outro (por mais grave que seja) ao alterar o código passa despercebida.

Os seguintes métodos de introdução de portões de qualidade são conhecidos:

  • Definir um limite para o número total de avisos ou o número de avisos dividido pelo número de linhas de código. Isso funciona mal, porque tal portão permite a passagem livre de alterações com novos defeitos, desde que seu limite não seja excedido.
  • Corrigindo, em um determinado momento, todos os avisos antigos no código como ignorados e recusando a construção quando novos avisos ocorrerem. Esta funcionalidade é fornecida pelo PVS-studio e alguns recursos online, por exemplo, Codacy. Não tive oportunidade de trabalhar no PVS-studio, pois pela minha experiência com Codacy, o principal problema deles é que determinar o que é um erro “antigo” e o que é um “novo” é um algoritmo bastante complexo que nem sempre funciona corretamente, especialmente se os arquivos forem muito modificados ou renomeados. Na minha experiência, a Codacy poderia ignorar novos avisos em uma solicitação pull e, ao mesmo tempo, não passar uma solicitação pull devido a avisos que não estavam relacionados a alterações no código de um determinado PR.
  • Na minha opinião, a solução mais eficaz é aquela descrita no livro Entrega Contínua “método de catraca”. A ideia básica é que o número de avisos de análise estática seja uma propriedade de cada release, sendo permitidas apenas alterações que não aumentem o número total de avisos.

chave catraca

Funciona assim:

  1. Na fase inicial, é feito um registro nos metadados sobre a liberação da quantidade de avisos no código encontrados pelos analisadores. Portanto, quando você constrói o upstream, seu gerenciador de repositório escreve não apenas “versão 7.0.2”, mas “versão 7.0.2 contendo 100500 avisos de estilo de verificação”. Se você usar um gerenciador de repositório avançado (como Artifactory), será fácil armazenar esses metadados sobre sua versão.
  2. Agora, cada solicitação pull, quando criada, compara o número de avisos resultantes com o número de avisos disponíveis na versão atual. Se o PR levar a um aumento nesse número, o código não passa pelo portão de qualidade para análise estática. Se o número de avisos diminuir ou não mudar, ele será aprovado.
  3. Na próxima versão, o número recalculado de avisos será registrado novamente nos metadados da versão.

Assim, pouco a pouco, mas de forma constante (como quando uma catraca funciona), o número de avisos tenderá a zero. É claro que o sistema pode ser enganado introduzindo um novo aviso, mas corrigindo o de outra pessoa. Isso é normal, porque à distância dá resultados: os avisos são corrigidos, via de regra, não individualmente, mas em um grupo de um determinado tipo ao mesmo tempo, e todos os avisos facilmente removíveis são eliminados rapidamente.

Este gráfico mostra o número total de avisos Checkstyle durante seis meses de operação de tal “catraca” em um de nossos projetos OpenSource. O número de avisos diminuiu numa ordem de grandeza e isso aconteceu naturalmente, em paralelo com o desenvolvimento do produto!

Implemente análise estática no processo, em vez de usá-la para encontrar bugs

Eu uso uma versão modificada desse método, contando avisos separadamente por módulo de projeto e ferramenta de análise, resultando em um arquivo YAML com metadados de construção parecido com 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

Em qualquer sistema CI avançado, a catraca pode ser implementada para qualquer ferramenta de análise estática sem depender de plug-ins e ferramentas de terceiros. Cada analisador produz seu próprio relatório em formato de texto simples ou XML que é fácil de analisar. Resta apenas escrever a lógica necessária no script de CI. Você pode ver como isso é implementado em nossos projetos de código aberto baseados em Jenkins e Artifactory aqui ou aqui. Ambos os exemplos dependem da biblioteca catracalib: método countWarnings() conta tags xml em arquivos gerados por Checkstyle e Spotbugs da maneira usual, e compareWarningMaps() implementa a mesma catraca, gerando um erro quando o número de avisos em qualquer uma das categorias aumenta.

Uma implementação interessante da "catraca" é possível para analisar a ortografia de comentários, literais de texto e documentação usando aspell. Como você sabe, ao verificar a ortografia, nem todas as palavras desconhecidas do dicionário padrão estão incorretas, elas podem ser adicionadas ao dicionário do usuário. Se você tornar um dicionário personalizado parte do código-fonte do projeto, o portão de qualidade ortográfica poderá ser formulado desta forma: executando aspell com um dicionário padrão e personalizado não deveria não encontre erros ortográficos.

Sobre a importância de consertar a versão do analisador

Concluindo, o ponto a ser observado é que não importa como você implementa a análise em seu pipeline de entrega, a versão do analisador deve ser corrigida. Se você permitir que o analisador seja atualizado espontaneamente, ao montar a próxima solicitação pull, novos defeitos poderão “aparecer” que não estão relacionados a alterações de código, mas estão relacionados ao fato de que o novo analisador é simplesmente capaz de encontrar mais defeitos - e isso interromperá seu processo de aceitação de solicitações pull. Atualizar um analisador deve ser uma ação consciente. Contudo, a fixação rígida da versão de cada componente da montagem é geralmente um requisito necessário e um tópico para uma discussão separada.

Descobertas

  • A análise estática não encontrará bugs para você e não melhorará a qualidade do seu produto como resultado de uma única aplicação. Um efeito positivo na qualidade só pode ser alcançado através do seu uso constante durante o processo de entrega.
  • Encontrar bugs não é a principal tarefa da análise; a grande maioria das funções úteis está disponível em ferramentas de código aberto.
  • Implemente portões de qualidade com base nos resultados da análise estática logo no primeiro estágio do pipeline de entrega, usando uma “catraca” para código legado.

referências

  1. Entrega Contínua
  2. A. Kudryavtsev: Análise de programas: como entender que você é um bom programador relatório sobre diferentes métodos de análise de código (não apenas estático!)

Fonte: habr.com

Adicionar um comentário