
Para variar, hoje vamos falar um pouco sobre o processo de desenvolvimento e finalização de regras de diagnóstico para PVS-Studio Java. Vamos ver por que os acionadores do analisador antigo não flutuam muito de uma versão para outra, e os novos não são muito loucos. E vamos estragar um pouco mais “quais são os planos dos javistas” e mostrar um par de belos (e nem tanto) erros encontrados com a ajuda dos diagnósticos do próximo lançamento.
Diagnóstico e processo de desenvolvimento de autoteste
Naturalmente, cada nova regra de diagnóstico começa com uma ideia. E como o analisador Java é a direção mais jovem no desenvolvimento do PVS-Studio, basicamente roubamos essas ideias dos departamentos C / C ++ e C #. Mas nem tudo é tão ruim: nós também adicionamos regras que foram inventadas por nós (inclusive pelos usuários - obrigado!), Para que depois os mesmos departamentos as roubassem de nós. O ciclo, como dizem.
A própria implementação das regras no código, na maioria dos casos, acaba sendo uma tarefa de pipeline. Você cria um arquivo com vários exemplos sintéticos, marca com as mãos onde devem estar os erros e, com o depurador pronto, percorre a árvore de sintaxe até ficar entediado e cobrir todos os casos inventados. Às vezes, as regras acabam sendo absurdamente simples (por exemplo, V6063consiste literalmente em algumas linhas), e às vezes você tem que pensar bastante sobre a lógica.
No entanto, este é apenas o começo do processo. Como você sabe, não gostamos particularmente de exemplos sintéticos, pois eles refletem muito mal o tipo de gatilhos do analisador em projetos reais. A propósito, uma parte significativa desses exemplos em nossos testes de unidade são extraídos de projetos reais - é quase impossível inventar todos os casos possíveis sozinho. E os testes de unidade também nos permitem não perder gatilhos em exemplos da documentação. Sim, havia precedentes, apenas shh.
Portanto, os aspectos positivos em projetos reais devem ser encontrados de alguma forma primeiro. E você também precisa verificar se:
- A regra não cairá na loucura do código aberto, onde soluções "interessantes" são comuns;
- ( - , );
- data-flow ( ) - ;
- open-source ;
- over 9000%;
- "" , ;
- .
Em geral, aqui, como um cavaleiro a cavalo (um pouco mancando, mas estamos trabalhando nisso), o SelfTester vem à tona. Sua principal e única tarefa é verificar automaticamente um grupo de projetos e mostrar quais gatilhos foram adicionados, desaparecidos ou alterados em relação à "referência" no sistema de controle de versão. Fornece diffs para o relatório do analisador e mostra o código correspondente nos projetos, resumidamente. Atualmente o SelfTester for Java está testando 62 projetos de código aberto de versões barbadas, entre os quais estão, por exemplo, DBeaver, Hibernate e Spring. Uma execução completa de todos os projetos leva de 2 a 2,5 horas, o que é sem dúvida doloroso, mas nada pode ser feito.

Na imagem acima, os projetos "verdes" são aqueles em que nada mudou. Cada diferença em projetos "vermelhos" é revisada manualmente e, se correta, é confirmada pelo mesmo botão "Aprovar". A propósito, o kit de distribuição do analisador será construído apenas se o SelfTester fornecer um resultado verde puro. Em geral, é assim que mantemos a consistência dos resultados entre as diferentes versões.
Além de manter a consistência dos resultados, o SelfTester permite eliminar um grande número de falsos positivos antes mesmo da divulgação dos diagnósticos. Um padrão de desenvolvimento típico se parece com este:
- , . , " double-checked locking" ;
- SelfTester-, ;
- , -;
- SelfTester- , ;
- 3-4, ;
- , , ( , );
- , master.
Felizmente, execuções completas do Autoteste são raras o suficiente e você não precisa esperar "2-2,5 horas" com frequência. De vez em quando, a sorte ignora e os gatilhos aparecem em grandes projetos como Sakai e Apache Hive - é hora de beber café, beber café e beber café. Você também pode estudar a documentação, mas isso não é para todos.
"Por que precisamos de testes de unidade, já que existe essa ferramenta mágica?"
E então os testes são significativamente mais rápidos. Alguns minutos - e já existe um resultado. Eles também permitem que você veja exatamente qual parte da regra caiu. E, além disso, nem sempre todos os acionamentos permitidos de qualquer regra são capturados em projetos de Autoteste, mas sua operacionalidade também deve ser verificada.
Novos problemas em velhos conhecidos
Inicialmente, esta seção do artigo começava com as palavras "As versões de projetos no SelfTester são bastante antigas, então a maioria dos erros apresentados provavelmente foram corrigidos". No entanto, quando decidi ter certeza disso, tive uma surpresa. Cada erro permaneceu no lugar. Tudo. Isso significa duas coisas:
- Esses erros não são supercríticos para o funcionamento do aplicativo. Muitos deles, aliás, estão no código de teste, e testes incorretos dificilmente podem ser chamados de consistentes.
- Esses erros são encontrados em arquivos raramente usados de grandes projetos, que os desenvolvedores dificilmente acessam. Por causa disso, o código incorreto está fadado a permanecer ali por muito tempo: provavelmente, até que algum bug crítico ocorra devido a ele.
Para aqueles que desejam se aprofundar, haverá links para versões específicas que estamos verificando.
PS O que foi dito acima não significa que a análise estática detecta apenas erros inofensivos em código não utilizado. Verificamos o lançamento (e quase lançamento) de versões de projetos - em que os desenvolvedores e testadores (e às vezes, infelizmente, os usuários) encontraram os bugs mais relevantes manualmente, o que é longo, caro e doloroso. Você pode ler mais sobre isso em nosso artigo " Erros que a análise estática de código não consegue encontrar porque não é usada ".
Apache Dubbo e menu em branco
Diagnóstico do GitHub " V6080 Considere verificar se há erros de impressão. É possível que uma variável atribuída deva ser verificada na próxima condição " já foi lançado na versão 7.08, mas ainda não apareceu em nossos artigos, então é hora de corrigi-lo.
Menu.java:40
public class Menu
{
private Map<String, List<String>> menus = new HashMap<String, List<String>>();
public void putMenuItem(String menu, String item)
{
List<String> items = menus.get(menu);
if (item == null) // <=
{
items = new ArrayList<String>();
menus.put(menu, items);
}
items.add(item);
}
....
}
Um exemplo clássico de dicionário de "coleção de chaves" e um erro de digitação igualmente clássico. O desenvolvedor queria criar uma coleção correspondente à chave, se ela ainda não existe, mas ele confundiu o nome da variável e obteve não apenas uma operação incorreta do método, mas também um NullPointerException na última linha. Para Java 8 e posterior, para implementar esses dicionários, você deve usar o método computeIfAbsent :
public class Menu
{
private Map<String, List<String>> menus = new HashMap<String, List<String>>();
public void putMenuItem(String menu, String item)
{
List<String> items = menus.computeIfAbsent(menu, key -> new ArrayList<>());
items.add(item);
}
....
}
Glassfish e travamento de dupla verificação
GitHub
Um dos diagnósticos que serão incluídos na próxima versão é verificar a implementação correta do padrão de "bloqueio com verificação dupla". Glassfish acabou por ser o detentor do recorde para detecções de projetos de autoteste: no total, o PVS-Studio encontrou 10 áreas problemáticas no projeto usando esta regra. Eu sugiro ao leitor que se divirta e procure por dois deles no trecho de código abaixo. Para obter ajuda, consulte a documentação: " V6082 Unsafe double-checks locking ". Bem, ou, se você não quiser, no final do artigo.
EjbComponentAnnotationScanner.java
public class EjbComponentAnnotationScanner
{
private Set<String> annotations = null;
public boolean isAnnotation(String value)
{
if (annotations == null)
{
synchronized (EjbComponentAnnotationScanner.class)
{
if (annotations == null)
{
init();
}
}
}
return annotations.contains(value);
}
private void init()
{
annotations = new HashSet();
annotations.add("Ljavax/ejb/Stateless;");
annotations.add("Ljavax/ejb/Stateful;");
annotations.add("Ljavax/ejb/MessageDriven;");
annotations.add("Ljavax/ejb/Singleton;");
}
....
}
SonarQube e fluxo de dados
GitHub
Melhorar os diagnósticos não se trata apenas de alterar diretamente o código para detectar mais locais suspeitos ou remover falsos positivos. A marcação manual de métodos para fluxo de dados também desempenha um papel importante no desenvolvimento do analisador - por exemplo, você pode escrever que tal e tal método de biblioteca sempre retorna não nulo. Ao escrever um novo diagnóstico, descobrimos acidentalmente que o método Map # clear () não foi marcado . Além do código obviamente estúpido de que o diagnóstico " Coleção V6009 está vazia. A chamada da função 'limpar' não tem sentido " começou a detectar , conseguimos encontrar um grande erro de digitação.
MetricRepositoryRule.java:90
protected void after()
{
this.metricsById.clear();
this.metricsById.clear();
}
À primeira vista, limpar o dicionário novamente não é um erro. E poderíamos até pensar que esta é uma linha duplicada aleatoriamente, se nosso olhar não tivesse caído um pouco mais para baixo - literalmente para o próximo método.
protected void after()
{
this.metricsById.clear();
this.metricsById.clear();
}
public Metric getByKey(String key)
{
Metric res = metricsByKey.get(key);
....
}
Exatamente. A classe tem dois campos com nomes semelhantes metricsById e metricsByKey . Tenho certeza de que no método posterior o desenvolvedor queria limpar os dois dicionários, mas ou o preenchimento automático falhou ou ele inseriu o mesmo nome inerte. Assim, os dois dicionários que contêm dados relacionados ficarão fora de sincronia após a chamada para after .
Sakai e coleções vazias
GitHub
Outro novo diagnóstico que será incluído na próxima versão é " V6084 Retorno suspeito de uma coleção sempre vazia ". É fácil esquecer de adicionar itens à coleção, especialmente quando cada item precisa ser inicializado primeiro. Por experiência pessoal, esses erros geralmente não levam ao travamento do aplicativo, mas a um comportamento estranho ou à ausência de qualquer funcionalidade.
DateModel.java:361
public List getDaySelectItems()
{
List selectDays = new ArrayList();
Integer[] d = this.getDays();
for (int i = 0; i < d.length; i++)
{
SelectItem selectDay = new SelectItem(d[i], d[i].toString());
}
return selectDays;
}
A propósito, a mesma classe contém métodos muito semelhantes sem o mesmo erro. Por exemplo:
public List getMonthSelectItems()
{
List selectMonths = new ArrayList();
Integer[] m = this.getMonths();
for (int i = 0; i < m.length; i++)
{
SelectItem selectMonth = new SelectItem(m[i], m[i].toString());
selectMonths.add(selectMonth);
}
return selectMonths;
}
Planos para o futuro
Além de várias coisas internas não muito interessantes, estamos pensando em adicionar diagnósticos para o Spring Framework ao analisador Java. Não é apenas o pão principal dos javistas, mas também contém muitos momentos não óbvios nos quais se pode tropeçar. Ainda não temos muita certeza de que forma esses diagnósticos eventualmente aparecerão, quando isso acontecerá e se acontecerá. Mas temos certeza de que precisamos de ideias para eles e projetos de código aberto usando Spring for SelfTester. Então, se você tem algo em mente, sugira (em comentários ou mensagens privadas, você também pode)! E quanto mais dessa bondade coletarmos, mais prioridade será transferida para ela.
E, finalmente, há erros na implementação de bloqueio verificada duas vezes do Glassfish:
- O campo não é declarado 'volátil'.
- O objeto é publicado primeiro e, em seguida, inicializado.
Por que tudo isso é ruim - novamente, você pode ver na documentação .

Se você deseja compartilhar este artigo com um público que fala inglês, use o link de tradução: Nikita Lazeba. Sob o capô do PVS-Studio para Java: como desenvolvemos diagnósticos .