Desenvolvimento de um novo analisador estático: PVS-Studio Java

Quadro 3

O analisador estático PVS-Studio é conhecido no mundo de C, C ++ e C # como uma ferramenta para detectar erros e possíveis vulnerabilidades. No entanto, temos poucos clientes do setor financeiro, pois Java e IBM RPG (!) Agora estão sendo procurados lá. Mas sempre quisemos nos aproximar do mundo corporativo; portanto, depois de pensar um pouco, decidimos começar a criar um analisador Java.

1. Introdução


Claro, havia preocupações. É fácil conquistar o mercado de analisadores no IBM RPG. Não tenho certeza de que existem ferramentas decentes para análise estática dessa linguagem. No mundo Java, as coisas são completamente diferentes. Já existe uma linha de ferramentas para análise estática e, para avançar, você precisa criar um analisador realmente poderoso e interessante.

No entanto, nossa empresa teve experiência no uso de várias ferramentas para análise estática de Java e temos certeza de que podemos fazer muitas coisas melhor.

Além disso, tínhamos uma idéia de como usar toda a potência do nosso analisador C ++ em um analisador Java. Mas as primeiras coisas primeiro.

Árvore


Quadro 6


Antes de tudo, era necessário decidir como obteríamos a árvore de sintaxe e o modelo semântico.

A árvore de sintaxe é o elemento básico em torno do qual o analisador é construído. Ao executar verificações, o analisador se move pela árvore de sintaxe e examina seus nós individuais. Sem essa árvore, a análise estática séria é praticamente impossível. Por exemplo, procurar erros usando expressões regulares não é promissor .

Vale a pena notar que apenas uma árvore de sintaxe não é suficiente. O analisador também precisa de informações semânticas. Por exemplo, precisamos conhecer os tipos de todos os elementos da árvore, poder acessar a declaração da variável etc.

Examinamos várias opções para obter uma árvore de sintaxe e um modelo semântico:


Abandonamos a idéia de usar o ANTLR quase imediatamente, pois isso complicaria desnecessariamente o desenvolvimento do analisador (a análise semântica teria que ser implementada por conta própria). No final, decidimos parar na biblioteca Spoon:

  • Não é apenas um analisador, mas um ecossistema inteiro - fornece não apenas uma árvore de análise, mas também oportunidades para análise semântica, por exemplo, permite obter informações sobre os tipos de variáveis, acessar a declaração de variáveis, obter informações sobre a classe pai e assim por diante.
  • É baseado no Eclipse JDT e é capaz de compilar código.
  • Ele suporta a versão mais recente do Java e é constantemente atualizado.
  • Boa documentação e API clara.

Aqui está um exemplo de um metamodelo fornecido pelo Spoon e com o qual trabalhamos ao criar regras de diagnóstico:

Quadro 10


Esse metamodelo corresponde ao seguinte código:
class TestClass { void test(int a, int b) { int x = (a + b) * 4; System.out.println(x); } } 

Uma das coisas boas do Spoon é que ele simplifica a árvore de sintaxe (remover e adicionar nós) para facilitar o trabalho. Ao mesmo tempo, é garantida a equivalência semântica do metamodelo simplificado do original.

Para nós, isso significa, por exemplo, que não precisamos mais nos preocupar em pular colchetes extras ao atravessar uma árvore. Além disso, cada expressão é colocada em um bloco, as importações são reveladas e outras simplificações semelhantes são feitas.

Por exemplo, um código como este:

 for (int i = ((0)); (i < 10); i++) if (cond) return (((42))); 

será apresentado da seguinte forma:

 for (int i = 0; i < 10; i++) { if (cond) { return 42; } } 

Com base na árvore de sintaxe, é realizada uma chamada análise baseada em padrões. Esta é uma busca por erros no código fonte de um programa usando padrões de código de erro conhecidos. No caso mais simples, o analisador procura lugares que parecem um erro na árvore, de acordo com as regras descritas nos diagnósticos correspondentes. O número de tais padrões é grande e sua complexidade pode variar bastante.

O exemplo mais simples de um erro detectado pela análise baseada em padrões é o seguinte código do projeto jMonkeyEngine:

 if (p.isConnected()) { log.log(Level.FINE, "Connection closed:{0}.", p); } else { log.log(Level.FINE, "Connection closed:{0}.", p); } 

Os blocos then e else da instrução if são os mesmos; provavelmente, há um erro lógico.

Aqui está outro exemplo semelhante do projeto Hive:

 if (obj instanceof Number) { // widening conversion return ((Number) obj).doubleValue(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof String) { return Double.valueOf(obj.toString()); } else if (obj instanceof Timestamp) { return new TimestampWritable((Timestamp)obj).getDouble(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).doubleValue(); } 

Este código contém duas condições idênticas em uma sequência do formulário if (....) else if (....) else if (....) . Vale a pena verificar esta seção do código em busca de um erro lógico ou remover o código duplicado.

Análise de fluxo de dados


Além da árvore de sintaxe e do modelo semântico, o analisador precisa de um mecanismo para analisar o fluxo de dados .

A análise do fluxo de dados permite calcular os valores válidos de variáveis ​​e expressões em cada ponto do programa e, graças a isso, encontrar erros. Chamamos esses valores válidos de valores virtuais.

Os valores virtuais são criados para variáveis, campos de classe, parâmetros de método e outras coisas na primeira menção. Se essa é uma atribuição, o mecanismo de Fluxo de Dados calcula o valor virtual analisando a expressão à direita; caso contrário, todo o intervalo válido de valores para esse tipo de variável é considerado o valor virtual. Por exemplo:

 void func(byte x) // x: [-128..127] { int y = 5; // y: [5] ... } 

Sempre que o valor de uma variável é alterado, o mecanismo de Fluxo de Dados recalcula o valor virtual. Por exemplo:

 void func() { int x = 5; // x: [5] x += 7; // x: [12] ... } 

O mecanismo de fluxo de dados também processa instruções de controle:

 void func(int x) // x: [-2147483648..2147483647] { if (x > 3) { // x: [4..2147483647] if (x < 10) { // x: [4..9] } } else { // x: [-2147483648..3] } ... } 

Neste exemplo, ao inserir a função, não há informações sobre o intervalo de valores da variável x , portanto, ela é definida de acordo com o tipo da variável (de -2147483648 a 2147483647). Então o primeiro bloco condicional impõe uma restrição x > 3, e os intervalos são combinados. Como resultado, no bloco then o intervalo de valores para x é de 4 a 2147483647 e no bloco else de -2147483648 a 3. A segunda condição x <10 é processada da mesma maneira.

Além disso, você deve poder executar cálculos puramente simbólicos. O exemplo mais simples:

 void f1(int a, int b, int c) { a = c; b = c; if (a == b) // <= always true .... } 

Aqui, a variável a recebe o valor c , a variável b também recebe o valor c , após o qual aeb são comparados. Nesse caso, para encontrar o erro, lembre-se do pedaço de madeira correspondente ao lado direito.

Aqui está um exemplo um pouco mais complexo com cálculos de caracteres:

 void f2(int a, int b, int c) { if (a < b) { if (b < c) { if (c < a) // <= always false .... } } } 

Nesses casos, já é necessário resolver o sistema de desigualdades de forma simbólica.

O mecanismo de fluxo de dados ajuda o analisador a encontrar erros que são muito difíceis de encontrar usando a análise baseada em padrões.

Esses erros incluem:

  • Estouros;
  • Indo para o exterior a matriz;
  • Acesso por referência nula ou potencialmente nula;
  • Condições sem sentido (sempre verdadeiro / falso);
  • Vazamentos de memória e recursos;
  • Divisão por 0;
  • E alguns outros.

A análise do fluxo de dados é especialmente importante ao procurar vulnerabilidades. Por exemplo, se um determinado programa recebe entrada do usuário, é provável que a entrada seja usada para causar uma negação de serviço ou para obter o controle do sistema. Os exemplos incluem erros que causam estouros de buffer para algumas entradas ou, por exemplo, injeções de SQL. Nos dois casos, para que o analisador estático detecte esses erros e vulnerabilidades, é necessário monitorar o fluxo de dados e os possíveis valores das variáveis.

Devo dizer que o mecanismo de análise de fluxo de dados é um mecanismo complexo e extenso e, neste artigo, abordamos apenas o básico.

Vejamos alguns exemplos de erros que podem ser detectados usando o mecanismo de fluxo de dados.

Projeto Hive:

 public static boolean equal(byte[] arg1, final int start1, final int len1, byte[] arg2, final int start2, final int len2) { if (len1 != len2) { // <= return false; } if (len1 == 0) { return true; } .... if (len1 == len2) { // <= .... } } 

A condição len1 == len2 é sempre satisfeita, pois a verificação oposta já foi realizada acima.

Outro exemplo do mesmo projeto:

 if (instances != null) { // <= Set<String> oldKeys = new HashSet<>(instances.keySet()); if (oldKeys.removeAll(latestKeys)) { .... } this.instances.keySet().removeAll(oldKeys); this.instances.putAll(freshInstances); } else { this.instances.putAll(freshInstances); // <= } 

Aqui, no bloco else , a desreferência do ponteiro nulo é garantida. Nota: aqui as instâncias são iguais a this.instances .

Exemplo do projeto JMonkeyEngine:

 public static int convertNewtKey(short key) { .... if (key >= 0x10000) { return key - 0x10000; } return 0; } 

Aqui, a variável- chave é comparada com o número 65536, no entanto, é do tipo curto , e o valor máximo possível para o curto é 32767. Portanto, a condição nunca é atendida.

Um exemplo do projeto Jenkins:
 public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; } 

A variável cnt foi introduzida neste código para limitar o número de passagens para cinco, mas esqueceu de incrementá-la, como resultado, a verificação é inútil.

Mecanismo de anotação


Além disso, o analisador precisa de um mecanismo de anotação. Annotations é um sistema de marcação que fornece ao analisador informações adicionais sobre os métodos e classes usadas, além do que pode ser obtido analisando sua assinatura. A marcação é feita manualmente, é um processo longo e trabalhoso, pois para obter os melhores resultados, é necessário anotar um grande número de classes e métodos Java padrão. Também faz sentido anotar bibliotecas populares. Em geral, as anotações podem ser consideradas como a base de conhecimento do analisador sobre os contratos de métodos e classes padrão.

Aqui está um pequeno exemplo de um erro que pode ser detectado usando anotações:

 int test(int a, int b) { ... return Math.max(a, a); } 

Neste exemplo, devido a um erro de digitação, a mesma variável foi passada como o segundo argumento para o método Math.max como o primeiro argumento. Essa expressão é sem sentido e suspeita.

Sabendo que os argumentos do método Math.max sempre devem ser diferentes, o analisador estático poderá emitir um aviso para esse código.

No futuro, darei alguns exemplos de nossa marcação de classes e métodos internos (código C ++):

 Class("java.lang.Math") - Function("abs", Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Returns(Arg1, [](const Int &v) { return v.Abs(); }) - Function("max", Type::Int32, Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(Arg1, Arg2) .Returns(Arg1, Arg2, [](const Int &v1, const Int &v2) { return v1.Max(v2); }) Class("java.lang.String", TypeClassification::String) - Function("split", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotNull(Arg1)) .Returns(Ptr(NotNullPointer)) Class("java.lang.Object") - Function("equals", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(This, Arg1)) Class("java.lang.System") - Function("exit", Type::Int32) .Set(FunctionClassification::NoReturn) 

Explicações:

  • Classe - classe anotada;
  • Função - método da classe anotada;
  • Pura - anotação mostrando que o método é limpo, ou seja, determinístico e sem efeitos colaterais;
  • Conjunto - definindo um sinalizador arbitrário para o método.
  • FunctionClassification :: NoDiscard - um sinalizador que significa que o valor de retorno do método deve ser usado;
  • FunctionClassification :: NoReturn - um sinalizador indicando que o método não retorna controle;
  • Arg1 , Arg2 , ... , ArgN - argumentos para o método;
  • Retornos - o valor de retorno do método;
  • Requer - um contrato para o método.

Vale ressaltar que, além da marcação manual, existe outra abordagem para anotação - saída automática de contratos com base no código de bytes. É claro que essa abordagem permite exibir apenas certos tipos de contratos, mas possibilita obter informações adicionais em geral de todas as dependências e não apenas daquelas que foram anotadas manualmente.

A propósito, já existe uma ferramenta que pode exibir contratos como @Nullable , NotNull baseado em bytecode - FABA . Pelo que entendi, o derivado FABA é usado no IntelliJ IDEA.

Agora, também estamos considerando adicionar análises de código de código para obter contratos para todos os métodos, pois esses contratos podem muito bem complementar nossas anotações manuais.

As regras de diagnóstico no trabalho geralmente se referem às anotações. Além do diagnóstico, as anotações usam o mecanismo de fluxo de dados. Por exemplo, usando a anotação do método java.lang.Math.abs , ele pode calcular com precisão o valor do módulo numérico. Ao mesmo tempo, você não precisa escrever nenhum código adicional - basta marcar o método corretamente.

Considere um exemplo de erro do projeto Hibernate que pode ser detectado através da anotação:

 public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } } 

Nesse código, o método equals () compara o objeto purchaseSequence com ele próprio. Certamente este é um erro de digitação e, à direita, deve ser isso . Compre a Sequence , não compre a Sequence .

Como o Dr. Frankenstein montou um analisador a partir de peças


Quadro 2


Como os mecanismos do fluxo de dados e das anotações em si não estão muito vinculados a uma linguagem específica, foi decidido reutilizar esses mecanismos em nosso analisador C ++. Isso nos permitiu obter rapidamente todo o poder do núcleo do analisador C ++ em nosso analisador Java. Além disso, essa decisão também foi influenciada pelo fato de que esses mecanismos foram escritos em C ++ moderno com um monte de metaprogramação e magia de modelo e, portanto, não são muito adequados para a transferência para outro idioma.

Para associar a parte Java ao kernel C ++, decidimos usar o SWIG (Simplified Wrapper and Interface Generator) - uma ferramenta para gerar automaticamente invólucros e interfaces para vincular programas C e C ++ a programas escritos em outras linguagens. Para Java, SWIG gera código JNI (Java Native Interface) .

SWIG é ótimo para casos em que já existe uma grande quantidade de código C ++ que precisa ser integrado a um projeto Java.

Vou dar um exemplo mínimo de trabalho com o SWIG. Suponha que tenhamos uma classe C ++ que queremos usar em um projeto Java:

CoolClass.h

 class CoolClass { public: int val; CoolClass(int val); void printMe(); }; 

CoolClass.cpp

 #include <iostream> #include "CoolClass.h" CoolClass::CoolClass(int v) : val(v) {} void CoolClass::printMe() { std::cout << "val: " << val << '\n'; } 

Primeiro, você precisa criar um arquivo de interface SWIG com uma descrição de todas as funções e classes exportadas. Também neste arquivo, se necessário, são feitas configurações adicionais.

Example.i

 %module MyModule %{ #include "CoolClass.h" %} %include "CoolClass.h" 

Depois disso, você pode executar o SWIG:

 $ swig -c++ -java Example.i 

Irá gerar os seguintes arquivos:

  • CoolClass.java - uma classe com a qual trabalharemos diretamente em um projeto Java;
  • MyModule.java - uma classe de módulo na qual todas as funções e variáveis ​​livres são colocadas;
  • MyModuleJNI.java - wrappers Java;
  • Example_wrap.cxx - wrappers C ++.

Agora você só precisa adicionar os arquivos .java resultantes ao projeto Java e o arquivo .cxx ao projeto C ++.

Por fim, você precisa compilar o projeto C ++ como uma biblioteca dinâmica e carregá-lo no projeto Java usando System.loadLibary () :

App.java

 class App { static { System.loadLibary("example"); } public static void main(String[] args) { CoolClass obj = new CoolClass(42); obj.printMe(); } } 

Esquematicamente, isso pode ser representado da seguinte maneira:

Quadro 8


Obviamente, em um projeto real, nem tudo é tão simples e você precisa fazer um pouco mais de esforço:

  • Para usar classes e métodos de modelo do C ++, eles devem ser instanciados para todos os parâmetros de modelo aceitos usando a diretiva % template ;
  • Em alguns casos, pode ser necessário capturar exceções lançadas da parte C ++ na parte Java. Por padrão, o SWIG não captura exceções do C ++ (ocorre um segfault); no entanto, é possível fazer isso usando a diretiva % exception ;
  • SWIG permite estender o código positivo no lado Java usando a diretiva % extend . Por exemplo, em nosso projeto, adicionamos o método toString () aos valores virtuais, para que possamos visualizá-los no depurador Java;
  • Para emular o comportamento RAII do C ++, a interface AutoClosable é implementada em todas as classes de interesse;
  • O mecanismo de diretores permite o uso de polimorfismo entre idiomas;
  • Para tipos que são alocados apenas dentro do C ++ (em seu conjunto de memórias), construtores e finalizadores são removidos para melhorar o desempenho. O coletor de lixo ignorará esses tipos.

Você pode ler mais sobre todos esses mecanismos na documentação do SWIG .

Nosso analisador é construído usando gradle, que chama CMake, que, por sua vez, chama SWIG e compila a parte C ++. Para os programadores, isso acontece quase imperceptivelmente, por isso não experimentamos nenhum inconveniente especial durante o desenvolvimento.

O núcleo do nosso analisador C ++ é construído no Windows, Linux, macOS, portanto o analisador Java também funciona nesses sistemas operacionais.

O que é uma regra de diagnóstico?


Os próprios diagnósticos e o código para análise são escritos em Java. Isso ocorre devido à interação próxima com o Spoon. Cada regra de diagnóstico é um visitante, cujos métodos estão sobrecarregados, nos quais os elementos de nosso interesse são contornados:

Quadro 9

Por exemplo, a estrutura de diagnóstico V6004 se parece com isso:

 class V6004 extends PvsStudioRule { .... @Override public void visitCtIf(CtIf ifElement) { // if ifElement.thenStatement statement is equivalent to // ifElement.elseStatement statement => add warning V6004 } } 

Plugins


Para facilitar a integração do analisador estático ao projeto, desenvolvemos plugins para os sistemas de montagem Maven e Gradle. O usuário pode apenas adicionar nosso plugin ao projeto.

Para Gradle:

 .... apply plugin: com.pvsstudio.PvsStudioGradlePlugin pvsstudio { outputFile = 'path/to/output.json' .... } 

Para Maven:

 .... <plugin> <groupId>com.pvsstudio</groupId> <artifactId>pvsstudio-maven-plugin</artifactId> <version>0.1</version> <configuration> <analyzer> <outputFile>path/to/output.json</outputFile> .... </analyzer> </configuration> </plugin> 

Depois disso, o plug-in receberá independentemente a estrutura do projeto e iniciará a análise.

Além disso, desenvolvemos um protótipo de plug-in para o IntelliJ IDEA.

Quadro 1

Este plugin também funciona no Android Studio.

Um plugin para Eclipse está atualmente em desenvolvimento.

Análise incremental


Fornecemos um modo de análise incremental que permite verificar apenas arquivos modificados e, assim, reduzir significativamente o tempo necessário para a análise de código. Graças a isso, os desenvolvedores poderão executar a análise quantas vezes forem necessárias.

A análise incremental inclui várias etapas:

  • Armazenamento em cache do metamodelo da colher;
  • Reconstruindo a parte alterada do metamodelo;
  • Análise de arquivos alterados.

Nosso sistema de teste


Para testar o analisador Java em projetos reais, escrevemos um kit de ferramentas especial que permite trabalhar com o banco de dados de projetos abertos. Foi escrito em ^ W Python + Tkinter e é multiplataforma.

Funciona da seguinte maneira:

  • O projeto de teste de uma versão específica é baixado do repositório no GitHub;
  • O projeto está sendo montado;
  • Nosso plugin é adicionado ao pom.xml ou build.gradle (usando git apply);
  • O analisador estático é iniciado usando o plug-in;
  • O relatório resultante é comparado com a referência deste projeto.

Essa abordagem garante que boas respostas não sejam perdidas como resultado da alteração do código do analisador. Abaixo está a interface do nosso utilitário de teste.

Quadro 11

Os projetos em relatórios que apresentam diferenças com o padrão são marcados em vermelho. O botão Aprovar permite salvar a versão atual do relatório como referência.

Exemplos de erro


Por tradição, citarei vários erros de vários projetos abertos que nosso analisador Java encontrou. No futuro, está planejado escrever artigos com um relatório mais detalhado sobre cada projeto.

Projeto Hibernate


PVS-Studio Warning: V6009 A função 'igual' recebe argumentos ímpares. Inspecione argumentos: this, 1. PurchaseRecord.java 57

 public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } } 

Nesse código, o método equals () compara o objeto purchaseSequence com ele próprio. Provavelmente, esse é um erro de digitação e, à direita, deve ser isso . BuySequence , não purchaseSequence .

PVS-Studio Warning: V6009 A função 'igual' recebe argumentos ímpares. Inspecione argumentos: this, 1. ListHashcodeChangeTest.java 232

 public void removeBook(String title) { for( Iterator<Book> it = books.iterator(); it.hasNext(); ) { Book book = it.next(); if ( title.equals( title ) ) { it.remove(); } } } 

Uma operação semelhante à anterior - à direita deve ser book.title , não title .

Colmeia do projeto


Aviso do PVS-Studio: A expressão V6007 'colOrScalar1.equals ("Column")' é sempre falsa. GenVectorCode.java 2768

Aviso do PVS-Studio: A expressão V6007 'colOrScalar1.equals ("Scalar")' é sempre falsa. GenVectorCode.java 2774

Aviso do PVS-Studio: A expressão V6007 'colOrScalar1.equals ("Column")' é sempre falsa. GenVectorCode.java 2785

 String colOrScalar1 = tdesc[4]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column")) { .... } else if (colOrScalar1.equals("Col") && colOrScalar1.equals("Scalar")) { .... } else if (colOrScalar1.equals("Scalar") && colOrScalar1.equals("Column")) { .... } 

Os operadores estão claramente confusos aqui e em vez de ' ||' usado ' &&' .

Projeto JavaParser


Aviso do PVS-Studio: V6001 Existem sub-expressões idênticas 'tokenRange.getBegin (). GetRange (). IsPresent ()' à esquerda e à direita do operador '&&'. Node.java 213

 public Node setTokenRange(TokenRange tokenRange) { this.tokenRange = tokenRange; if (tokenRange == null || !(tokenRange.getBegin().getRange().isPresent() && tokenRange.getBegin().getRange().isPresent())) { range = null; } else { range = new Range( tokenRange.getBegin().getRange().get().begin, tokenRange.getEnd().getRange().get().end); } return this; } 

O analisador descobriu que as mesmas expressões estão à esquerda e à direita do operador && (enquanto todos os métodos da cadeia de chamadas estão limpos). Provavelmente, no segundo caso, é necessário usar tokenRange.getEnd () e não tokenRange.getBegin () .

PVS-Studio Warning: V6016 Acesso suspeito ao elemento do objeto 'typeDeclaration.getTypeParameters ()' por um índice constante dentro de um loop. ResolvedReferenceType.java 265

 if (!isRawType()) { for (int i=0; i<typeDeclaration.getTypeParams().size(); i++) { typeParametersMap.add( new Pair<>(typeDeclaration.getTypeParams().get(0), typeParametersValues().get(i))); } } 

O analisador detectou acesso suspeito ao item de coleção em um índice constante dentro do loop. Pode haver um erro neste código.

Jenkins Project


Aviso do PVS-Studio: A expressão V6007 'cnt <5' sempre é verdadeira. AbstractProject.java 557

 public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; } 

A variável cnt foi introduzida neste código para limitar o número de passagens para cinco, mas esqueceu de incrementá-la, como resultado, a verificação é inútil.

Projeto Spark


Aviso do PVS-Studio: A expressão V6007 'sparkApplications! = Null' sempre é verdadeira. SparkFilter.java 127

 if (StringUtils.isNotBlank(applications)) { final String[] sparkApplications = applications.split(","); if (sparkApplications != null && sparkApplications.length > 0) { ... } } 

A verificação de nulo do resultado retornado pelo método split não faz sentido, pois esse método sempre retorna uma coleção e nunca retorna nulo .

Projeto Colher


Aviso do PVS-Studio: V6001 Existem sub-expressões idênticas '! M.getSimpleName (). StartsWith ("set")' à esquerda e à direita do operador '&&'. SpoonTestHelpers.java 108

 if (!m.getSimpleName().startsWith("set") && !m.getSimpleName().startsWith("set")) { continue; } 

Nesse código, as mesmas expressões estão à esquerda e à direita do operador && (todos os métodos na cadeia de chamadas estão limpos). Provavelmente, o código contém um erro lógico. Aviso do

PVS-Studio: A expressão V6007 'idxOfScopeBoundTypeParam> = 0' sempre é verdadeira. MethodTypingContext.java 243

 private boolean isSameMethodFormalTypeParameter(....) { .... int idxOfScopeBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= int idxOfSuperBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= return idxOfScopeBoundTypeParam == idxOfSuperBoundTypeParam; } } .... } 

Aqui eles selaram na segunda condição e, em vez de idxOfSuperBoundTypeParam, escreveram idxOfScopeBoundTypeParam .

Projeto de Segurança da Primavera


PVS-Studio Warning: V6001 Existem subexpressões idênticas à esquerda e à direita do '||' operador. Verifique as linhas: 38, 39. AnyRequestMatcher.java 38

 @Override @SuppressWarnings("deprecation") public boolean equals(Object obj) { return obj instanceof AnyRequestMatcher || obj instanceof security.web.util.matcher.AnyRequestMatcher; } 

A operação é semelhante à anterior - aqui o nome da mesma classe é escrito de maneiras diferentes.

PVS-Studio Warning: V6006 O objeto foi criado, mas não está sendo usado. A palavra-chave 'throw' pode estar ausente. DigestAuthenticationFilter.java 434

 if (!expectedNonceSignature.equals(nonceTokens[1])) { new BadCredentialsException( DigestAuthenticationFilter.this.messages .getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[] { nonceAsPlainText }, "Nonce token compromised {0}")); } 

Nesse código, eles esqueceram de adicionar throw antes da exceção. Como resultado, o objeto de exceção BadCredentialsException é lançado , mas não é usado de forma alguma, ou seja, uma exceção não é lançada.

PVS-Studio Warning: V6030 O método localizado à direita do '|' operadores serão chamados independentemente do valor do operando esquerdo. Talvez seja melhor usar '||'. RedirectUrlBuilder.java 38

 public void setScheme(String scheme) { if (!("http".equals(scheme) | "https".equals(scheme))) { throw new IllegalArgumentException("..."); } this.scheme = scheme; } 

Nesse código, o uso do | injustificado, pois ao usá-lo, o lado direito será calculado mesmo se o lado esquerdo já for verdadeiro. Nesse caso, isso não faz sentido prático; portanto, o operador | vale a pena substituir por || .

Projeto IntelliJ IDEA


PVS-Studio Warning: V6008 Desreferência nula potencial de 'editor'. IntroduceVariableBase.java:609

 final PsiElement nameSuggestionContext = editor == null ? null : file.findElementAt(...); // <= final RefactoringSupportProvider supportProvider = LanguageRefactoringSupport.INSTANCE.forLanguage(...); final boolean isInplaceAvailableOnDataContext = supportProvider != null && editor.getSettings().isVariableInplaceRenameEnabled() && // <= ... 

O analisador detectou que nesse código pode ocorrer desreferência do ponteiro nulo do editor . Vale a pena adicionar um cheque adicional.

Aviso do PVS-Studio: A expressão V6007 é sempre falsa. RefResolveServiceImpl.java:814

 @Override public boolean contains(@NotNull VirtualFile file) { .... return false & !myProjectFileIndex.isUnderSourceRootOfType(....); } 

É difícil para mim dizer o que o autor tinha em mente, mas esse código parece muito suspeito. Mesmo que de repente não haja nenhum erro aqui, acho que vale a pena reescrever este local para não confundir o analisador e outros programadores. Aviso do

PVS-Studio: A expressão V6007 'resultado [0]' é sempre falsa. CopyClassesHandler.java:298

 final boolean[] result = new boolean[] {false}; // <= Runnable command = () -> { PsiDirectory target; if (targetDirectory instanceof PsiDirectory) { target = (PsiDirectory)targetDirectory; } else { target = WriteAction.compute(() -> ((MoveDestination)targetDirectory).getTargetDirectory( defaultTargetDirectory)); } try { Collection<PsiFile> files = doCopyClasses(classes, map, copyClassName, target, project); if (files != null) { if (openInEditor) { for (PsiFile file : files) { CopyHandler.updateSelectionInActiveProjectView( file, project, selectInActivePanel); } EditorHelper.openFilesInEditor( files.toArray(PsiFile.EMPTY_ARRAY)); } } } catch (IncorrectOperationException ex) { Messages.showMessageDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"), Messages.getErrorIcon()); } }; CommandProcessor processor = CommandProcessor.getInstance(); processor.executeCommand(project, command, commandName, null); if (result[0]) { // <= ToolWindowManager.getInstance(project).invokeLater(() -> ToolWindowManager.getInstance(project) .activateEditorComponent()); } 

Eu suspeito que aqui eles esqueceram de alguma forma alterar o valor no resultado . Por esse motivo, o analisador relata que verificar se (resultado [0]) é inútil.

Conclusão


A direção do Java é muito versátil - é desktop, android, web e muito mais, por isso temos muito espaço para atividades. Antes de tudo, é claro, vamos desenvolver as áreas que serão mais procuradas.

Aqui estão nossos planos para o futuro próximo:

  • Anotações de saída baseadas em bytecode;
  • Integração em projetos no Ant (alguém o usa em 2018?);
  • Plugin para Eclipse (em desenvolvimento);
  • Ainda mais diagnósticos e anotações;
  • Melhorando o mecanismo do fluxo de dados.

Sugiro também aqueles que desejam participar do teste da versão alfa do nosso analisador Java, quando ela estiver disponível. Para fazer isso, escreva-nos em suporte . Adicionaremos seu contato à lista e escreveremos para você quando prepararmos a primeira versão alfa.


Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Egor Bredikhin. Desenvolvimento de um novo analisador estático:

Você leu o artigo e tem uma pergunta?

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


All Articles