PVS-Studio para Java

PVS-Studio para Java

Na sétima versão do analisador estático PVS-Studio, adicionamos suporte à linguagem Java. É hora de uma breve história de como começamos a dar suporte à linguagem Java, até onde chegamos e o que está em nossos planos futuros. Obviamente, este artigo listará as primeiras avaliações do analisador em projetos de código aberto.

PVS-Studio


Aqui está uma breve descrição do PVS-Studio para desenvolvedores de Java que nunca ouviram falar dele.

Essa ferramenta foi projetada para detectar erros e possíveis vulnerabilidades no código fonte dos programas, escritos em C, C ++, C # e Java. Funciona no ambiente Windows, Linux e macOS.

O PVS-Studio realiza análise de código estático e gera um relatório que ajuda um desenvolvedor a encontrar e eliminar defeitos. Para aqueles que estão interessados ​​em saber exatamente como o PVS-Studio procura por erros, sugiro dar uma olhada no artigo " Tecnologias usadas no analisador de código do PVS-Studio para encontrar bugs e possíveis vulnerabilidades ".

Começo


Eu poderia ter apresentado uma história inteligente de como especulamos sobre o próximo idioma a ser suportado no PVS-Studio. Sobre uma escolha sensata de Java, que se baseia em uma alta popularidade dessa linguagem e assim por diante.

No entanto, como acontece na vida, a escolha foi feita não por uma análise profunda, mas por um experimento :). Sim, ponderamos a direção do desenvolvimento do analisador PVS-Studio. Consideramos linguagens como: Java, PHP, Python, JavaScript, IBM RPG. Estávamos até inclinados à linguagem Java, mas a escolha final não foi feita. Para aqueles cujo olhar se baseava no IBM RPG desconhecido, gostaria de direcioná-lo a esta nota , da qual tudo ficará claro.

No final de 2017, meu colega Egor Bredikhin analisou bibliotecas disponíveis de código de análise (em outras palavras - analisadores) em busca de novas direções de desenvolvimento, interessantes para nós. Eventualmente, ele se deparou com vários projetos para analisar o código Java. Ele conseguiu criar rapidamente um protótipo de analisador com alguns diagnósticos baseados no Spoon . Além disso, ficou claro que poderíamos usar no analisador Java alguns mecanismos do analisador C ++ usando SWIG . Analisamos o que obtivemos e percebemos que nosso próximo analisador seria para Java.

Gostaríamos de agradecer a Egor por seu trabalho e trabalho duro que ele fez no analisador Java. O processo de desenvolvimento em si foi descrito por ele no artigo " Desenvolvimento de um novo analisador estático: PVS-Studio Java ".

E os concorrentes?


Existem muitos analisadores de código estático gratuitos e comerciais para Java em todo o mundo. Não faz sentido listá-las todas no artigo. Vou deixar o link para a " Lista de ferramentas para análise de código estático " (consulte a seção Java e multilíngue).

No entanto, eu sei que, em primeiro lugar, seremos questionados sobre o IntelliJ IDEA, FindBugs e SonarQube (SonarJava).

IntelliJ IDEA

Um analisador de código estático muito poderoso é construído no IntelliJ IDEA. Além disso, o analisador está evoluindo, seus autores acompanham de perto nossas atividades. Portanto, o IntelliJ IDEA é um cookie difícil para nós. Não conseguiremos superar o IntelliJ IDEA em habilidades de diagnóstico, pelo menos por enquanto. Portanto, vamos nos concentrar em nossas outras vantagens.

A análise estática no IntelliJ IDEA é principalmente um dos recursos do ambiente, que impõe certas limitações. Quanto a nós, temos liberdade no que podemos fazer com nosso analisador. Por exemplo, podemos adaptá-lo rapidamente às necessidades específicas do cliente. Suporte rápido e profundo é a nossa vantagem competitiva. Nossos clientes se comunicam diretamente com os desenvolvedores, trabalhando em uma ou outra parte do PVS-Studio.

No PVS-Studio, existem muitas oportunidades para integrá-lo a um ciclo de desenvolvimento de grandes projetos antigos. Por exemplo, é a nossa integração com o SonarQube . Ele também inclui a supressão em massa de avisos do analisador, o que permite que você comece a usar imediatamente a ferramenta em um grande projeto para rastrear bugs apenas no código novo ou modificado. O PVS-Studio pode ser construído em um processo de integração contínua. Acho que esses e outros recursos ajudarão nosso analisador a encontrar um lugar sob o sol no mundo Java.

Findbugs

O projeto FindBugs é abandonado. No entanto, devemos mencioná-lo pelo fato de que talvez seja o analisador estático livre mais famoso do código Java.

SpotBugs pode ser chamado de sucessor do FindBugs. No entanto, é menos popular e ainda não está claro o que acontecerá com ele.

De um modo geral, acreditamos que, embora o FindBugs tenha sido e ainda continue sendo extremamente popular, além de um analisador gratuito, não devemos insistir nele. Este projeto se tornará uma história silenciosamente.

PS A propósito, agora o PVS-Studio também pode ser usado gratuitamente quando se trabalha com projetos abertos.

SonarQube (SonarJava)

Acreditamos que não competimos com o SonarQube, mas complementamos. O PVS-Studio se integra ao SonarQube, que permite que os desenvolvedores encontrem mais bugs e possíveis vulnerabilidades de segurança em seus projetos. Contamos regularmente como integrar a ferramenta PVS-Studio e outros analisadores no SonarQube em master classes que realizamos em termos de diferentes conferências.

Como executar o PVS-Studio for Java


Disponibilizamos as formas mais populares de integração do analisador no sistema de compilação para os usuários:

  • Plugin para Maven;
  • Plugin para Gradle;
  • Plugin para IntelliJ IDEA

Durante a fase de teste, conhecemos muitos usuários que possuem sistemas de construção auto-escritos, especialmente na esfera do desenvolvimento móvel. Eles tiveram a oportunidade de executar o analisador diretamente, listando as fontes e o caminho da classe.

Você pode encontrar informações detalhadas sobre todas as maneiras de executar o analisador na página de documentação " Como executar o PVS-Studio Java ".

Não conseguimos evitar a plataforma SonarQube de controle de qualidade de código, que é tão popular entre os desenvolvedores Java, por isso adicionamos suporte à linguagem Java em nosso plug-in para o SonarQube .

Planos adicionais


Temos muitas idéias que podem exigir uma investigação mais aprofundada, mas alguns planos específicos, inerentes a qualquer um de nossos analisadores, são os seguintes:

  • Criação de novos diagnósticos e aprimoramento dos existentes;
  • Melhoria da análise de fluxo de dados;
  • Aumento da confiabilidade e usabilidade.

Talvez, encontre tempo para adaptar o plugin IntelliJ IDEA ao CLion. Olá aos desenvolvedores de C ++ que leram sobre o analisador de Java :-)

Exemplos de erros encontrados em projetos de código aberto


Tremer minhas madeiras se não estiver mostrando no artigo alguns erros encontrados no novo analisador! Bem, poderíamos ter pegado um grande projeto Java de código aberto e escrever um artigo clássico revisando erros, como costumamos fazer .

No entanto, eu antecipo imediatamente perguntas sobre o que podemos encontrar em projetos como IntelliJ IDEA, FindBugs e assim por diante. Portanto, não tenho outra saída senão começar com esses projetos. Então, decidi verificar e escrever rapidamente vários exemplos interessantes de erros dos seguintes projetos:

  • IntelliJ IDEA Community Edition . Eu acho que não há necessidade de explicar por que esse projeto foi escolhido :).
  • SpotBugs Como escrevi anteriormente, o projeto FindBugs não está progredindo. Então, vamos olhar dentro do projeto SpotBugs, que é o sucessor do FindBugs. SpotBugs é um analisador estático clássico de código Java.
  • Algo dos projetos da empresa SonarSource, que desenvolve software para monitoramento contínuo da qualidade do código. Agora, vamos examinar o SonarQube e o SonarJava .

Escrever sobre bugs desses projetos é um desafio. O fato é que esses projetos são de alta qualidade. Na verdade, não é surpreendente. Nossas observações mostram que os analisadores de código estático são sempre bem testados e verificados usando outras ferramentas.

Apesar de tudo isso, terei que começar exatamente com esses projetos. Não terei a segunda chance de escrever sobre eles. Tenho certeza de que, após o lançamento do PVS-Studio para Java, os desenvolvedores dos projetos listados levarão o PVS-Studio a bordo e começarão a usá-lo para verificações regulares ou, pelo menos, ocasionais de seu código. Por exemplo, eu sei que Tagir Valeev ( lany ), um dos desenvolvedores do JetBrains, trabalhando no analisador de código estático IntelliJ IDEA, no momento, quando estou escrevendo, o artigo já está jogando com a versão beta do PVS-Studio . Ele nos escreveu cerca de 15 e-mails com relatórios de erros e recomendações. Obrigado, Tagir!

Felizmente, não preciso encontrar tantos bugs em um projeto em particular. No momento, minha tarefa é mostrar que o analisador PVS-Studio para Java não apareceu em vão e será capaz de preencher uma linha de outras ferramentas projetadas para melhorar a qualidade do código. Apenas examinei os relatórios do analisador e listei alguns erros que pareciam interessantes. Se possível, tentei citar diferentes tipos de erros. Vamos ver como ficou.

IntelliJ IDEA, Divisão de números inteiros


private static boolean checkSentenceCapitalization(@NotNull String value) { List<String> words = StringUtil.split(value, " "); .... int capitalized = 1; .... return capitalized / words.size() < 0.2; // allow reasonable amount of // capitalized words } 

Aviso do PVS-Studio: V6011 [CWE-682] O literal '0.2' do tipo 'double' é comparado com um valor do tipo 'int'. TitleCapitalizationInspection.java 169

A questão era que a função retornaria verdadeira se menos de 20% das palavras começarem com uma letra maiúscula. Na verdade, a verificação não está funcionando, porque ocorre uma divisão inteira. Como resultado da divisão, podemos obter apenas dois valores: 0 ou 1.

A função retornará false, apenas se todas as palavras começarem com uma letra maiúscula. Em todos os outros casos, a operação de divisão resultará em 0 e a função retornará true.

IDEA IntelliJ, loop suspeito


 public int findPreviousIndex(int current) { int count = myPainter.getErrorStripeCount(); int foundIndex = -1; int foundLayer = 0; if (0 <= current && current < count) { current--; for (int index = count - 1; index >= 0; index++) { // <= int layer = getLayer(index); if (layer > foundLayer) { foundIndex = index; foundLayer = layer; } } .... } 

Aviso do PVS-Studio: V6007 [CWE-571] A expressão 'index> = 0' é sempre verdadeira. Updater.java 184

Primeiro, observe a condição (0 <= atual && current <count) . É executado apenas no caso de o valor da variável de contagem ser maior que 0.

Agora veja o loop:

 for (int index = count - 1; index >= 0; index++) 

O índice da variável é inicializado com uma expressão count - 1 . Como a variável de contagem é maior que 0, o valor inicial da variável de índice sempre será maior ou igual a 0. Acontece que o loop será executado até que ocorra um estouro da variável de índice .

Provavelmente, é apenas um erro de digitação e um decréscimo, não é necessário executar um incremento de uma variável:

 for (int index = count - 1; index >= 0; index--) 

IDEA IntelliJ, copiar e colar


 @NonNls public static final String BEFORE_STR_OLD = "before:"; @NonNls public static final String AFTER_STR_OLD = "after:"; private static boolean isBeforeOrAfterKeyword(String str, boolean trimKeyword) { return (trimKeyword ? LoadingOrder.BEFORE_STR.trim() : LoadingOrder.BEFORE_STR).equalsIgnoreCase(str) || (trimKeyword ? LoadingOrder.AFTER_STR.trim() : LoadingOrder.AFTER_STR).equalsIgnoreCase(str) || LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str) || // <= LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str); // <= } 

Aviso do PVS-Studio: V6001 [CWE-570] Existem sub-expressões idênticas 'LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase (str)' à esquerda e à direita da '||' operador. Verifique as linhas: 127, 128. ExtensionOrderConverter.java 127

Bom efeito antigo da última linha . Um desenvolvedor pulou a arma e, multiplicando a linha de código, esqueceu de corrigi-la. Como resultado, uma string str é comparada com BEFORE_STR_OLD duas vezes. Provavelmente, uma das comparações deve ser com AFTER_STR_OLD .

IntelliJ IDEA, Typo


 public synchronized boolean isIdentifier(@NotNull String name, final Project project) { if (!StringUtil.startsWithChar(name,'\'') && !StringUtil.startsWithChar(name,'\"')) { name = "\"" + name; } if (!StringUtil.endsWithChar(name,'"') && !StringUtil.endsWithChar(name,'\"')) { name += "\""; } .... } 

Aviso do PVS-Studio: V6001 [CWE-571] Existem sub-expressões idênticas '! StringUtil.endsWithChar (name,' "')' à esquerda e à direita do operador '&&'. JsonNamesValidator.java 27

Esse fragmento de código verifica se o nome está entre aspas simples ou duplas. Caso contrário, as aspas duplas são adicionadas automaticamente.

Devido a um erro de digitação, o final do nome é verificado apenas para a presença de aspas duplas. Como resultado, o nome entre aspas simples será processado incorretamente.

O nome

 'Abcd' 

devido à adição de aspas duplas extras se transformará em:

 'Abcd'" 

IntelliJ IDEA, proteção incorreta contra saturação de matriz


 static Context parse(....) { .... for (int i = offset; i < endOffset; i++) { char c = text.charAt(i); if (c == '<' && i < endOffset && text.charAt(i + 1) == '/' && startTag != null && CharArrayUtil.regionMatches(text, i + 2, endOffset, startTag)) { endTagStartOffset = i; break; } } .... } 

Aviso do PVS-Studio: V6007 [CWE-571] A expressão 'i <endOffset' sempre é verdadeira. EnterAfterJavadocTagHandler.java 183

A subexpressão i <endOffset na condição do operador if não faz sentido. A variável i é sempre menor que endOffset em qualquer caso, que segue a condição da execução do loop.

Provavelmente, um desenvolvedor queria proteger-se de uma sobrecarga de string ao chamar funções:

  • text.charAt (i + 1)
  • CharArrayUtil.regionMatches (texto, i + 2, endOffset, startTag)

Nesse caso, a subexpressão para verificar o índice deve ser: (i) <endOffset-2 .

IDEA IntelliJ, verificação repetida


 public static String generateWarningMessage(....) { .... if (buffer.length() > 0) { if (buffer.length() > 0) { buffer.append(" ").append( IdeBundle.message("prompt.delete.and")).append(" "); } } .... } 

Aviso do PVS-Studio: V6007 [CWE-571] A expressão 'buffer.length ()> 0' é sempre verdadeira. DeleteUtil.java 62

Pode ser um código redundante inócuo ou um erro crucial.

Se uma verificação duplicada aparecer acidentalmente, por exemplo, durante a refatoração, não há nada de errado nisso. Você pode simplesmente excluir a segunda verificação.

Outro cenário também é possível. A segunda verificação deve ser bem diferente e o código não se comporta conforme o planejado. Então é um erro real.

Nota A propósito, existem várias verificações redundantes. Bem, muitas vezes fica claro que não é um erro. No entanto, não podemos considerar os avisos do analisador como falsos positivos. Para uma explicação, gostaria de citar um exemplo, também retirado do IntelliJ IDEA:

 private static boolean isMultiline(PsiElement element) { String text = element.getText(); return text.contains("\n") || text.contains("\r") || text.contains("\r\n"); } 

O analisador diz que a função text.contains ("\ r \ n") sempre retorna false. De fato, se o caractere "\ n" e "\ r" não for encontrado, não há sentido em procurar "\ r \ n". Não é um bug, e o código é ruim apenas porque funciona um pouco mais devagar, realizando uma pesquisa sem sentido por uma substring.

Como lidar com esse código, em cada caso, é uma pergunta para os desenvolvedores. Ao escrever artigos, geralmente não presto atenção a esse código.

IntelliJ IDEA, algo está errado


 public boolean satisfiedBy(@NotNull PsiElement element) { .... @NonNls final String text = expression.getText().replaceAll("_", ""); if (text == null || text.length() < 2) { return false; } if ("0".equals(text) || "0L".equals(text) || "0l".equals(text)) { return false; } return text.charAt(0) == '0'; } 

Aviso do PVS-Studio: V6007 [CWE-570] A expressão '"0" .equals (text)' é sempre falsa. ConvertIntegerToDecimalPredicate.java 46

O código contém um erro lógico, com certeza. Acho difícil dizer o que exatamente o programador queria verificar e como corrigir o defeito. Então aqui, vou apontar para uma verificação sem sentido.

No início, deve-se verificar se a sequência contém pelo menos dois símbolos. Se não for assim, a função retornará false .

Em seguida, vem a verificação "0" .equals (texto) . Não faz sentido, porque nenhuma string pode conter apenas um caractere.

Então, algo está errado aqui, e o código deve ser corrigido.

SpotBugs (sucessor de FindBugs), erro de limitação no número de iterações


 public static String getXMLType(@WillNotClose InputStream in) throws IOException { .... String s; int count = 0; while (count < 4) { s = r.readLine(); if (s == null) { break; } Matcher m = tag.matcher(s); if (m.find()) { return m.group(1); } } throw new IOException("Didn't find xml tag"); .... } 

Aviso do PVS-Studio: V6007 [CWE-571] A expressão 'contagem <4' é sempre verdadeira. Util.java 394

Em teoria, uma pesquisa da tag xml deve ser realizada apenas nas quatro primeiras linhas do arquivo. Mas, devido ao fato de que se esqueceu de incrementar a variável count , o arquivo inteiro será lido.

Em primeiro lugar, isso pode ser uma operação muito lenta e, em segundo lugar, em algum lugar no meio do arquivo, pode ser encontrado algo que seria percebido como uma tag xml, não sendo ela.

SpotBugs (sucessor de FindBugs), compensação de um valor


 private void reportBug() { int priority = LOW_PRIORITY; String pattern = "NS_NON_SHORT_CIRCUIT"; if (sawDangerOld) { if (sawNullTestVeryOld) { priority = HIGH_PRIORITY; // <= } if (sawMethodCallOld || sawNumericTestVeryOld && sawArrayDangerOld) { priority = HIGH_PRIORITY; // <= pattern = "NS_DANGEROUS_NON_SHORT_CIRCUIT"; } else { priority = NORMAL_PRIORITY; // <= } } bugAccumulator.accumulateBug( new BugInstance(this, pattern, priority).addClassAndMethod(this), this); } 

Aviso do PVS-Studio: V6021 [CWE-563] O valor é atribuído à variável 'priority', mas não é usado. FindNonShortCircuit.java 197

O valor da variável de prioridade é definido dependendo do valor da variável sawNullTestVeryOld . No entanto, isso não importa. Depois disso, a variável de prioridade receberá outro valor em qualquer caso. Um erro óbvio na lógica da função.

SonarQube, copiar e colar


 public class RuleDto { .... private final RuleDefinitionDto definition; private final RuleMetadataDto metadata; .... private void setUpdatedAtFromDefinition(@Nullable Long updatedAt) { if (updatedAt != null && updatedAt > definition.getUpdatedAt()) { setUpdatedAt(updatedAt); } } private void setUpdatedAtFromMetadata(@Nullable Long updatedAt) { if (updatedAt != null && updatedAt > definition.getUpdatedAt()) { setUpdatedAt(updatedAt); } } .... } 

PVS-Studio: V6032 É estranho que o corpo do método 'setUpdatedAtFromDefinition' seja totalmente equivalente ao corpo de outro método 'setUpdatedAtFromMetadata'. Verifique as linhas: 396, 405. RuleDto.java 396

Um campo de definição é usado no método setUpdatedAtFromMetadata . Provavelmente, o campo de metadados deve ser usado. Isso é muito semelhante aos efeitos de uma falha de copiar e colar.

SonarJava, duplicatas ao inicializar o mapa


 private final Map<JavaPunctuator, Tree.Kind> assignmentOperators = Maps.newEnumMap(JavaPunctuator.class); public KindMaps() { .... assignmentOperators.put(JavaPunctuator.PLUSEQU, Tree.Kind.PLUS_ASSIGNMENT); .... assignmentOperators.put(JavaPunctuator.PLUSEQU, Tree.Kind.PLUS_ASSIGNMENT); .... } 

Aviso do PVS-Studio: V6033 [CWE-462] Um item com a mesma chave 'JavaPunctuator.PLUSEQU' já foi adicionado. Verifique as linhas: 104, 100. KindMaps.java 104

O mesmo par de valores-chave é definido no mapa duas vezes. Muito provavelmente, aconteceu inadvertidamente e, na verdade, não há erro real. No entanto, esse código deve ser verificado em qualquer caso, porque, talvez, alguém tenha esquecido de adicionar qualquer outro par.

Conclusão


Por que escrever conclusão quando é tão óbvio ?! Eu sugiro que todos vocês baixem o PVS-Studio agora e tentem verificar seus projetos de trabalho na linguagem Java! Faça o download do PVS-Studio .

Obrigado a todos pela atenção. Espero que em breve agrademos aos nossos leitores com uma série de artigos sobre a verificação de vários projetos Java de código aberto.

Source: https://habr.com/ru/post/pt436496/


All Articles