Os 10 principais bugs em projetos Java para 2019


2019 está chegando ao fim, e a equipe PVS-Studio está resumindo os resultados do ano que se inicia. No início de 2019, expandimos os recursos do analisador, suportando a linguagem Java. Portanto, a lista de nossas publicações sobre verificação de projetos abertos foi reabastecida com revisões de projetos Java. Muitos erros foram encontrados durante o ano, e decidimos preparar o Top 10 mais interessante deles.


Décimo lugar: byte icônico


Fonte: Análise do código-fonte da estrutura RPC Apache Dubbo pelo analisador estático PVS-Studio

A expressão V6007 'endKey [i] <0xff' sempre é verdadeira. OptionUtil.java (32)

public static final ByteSequence prefixEndOf(ByteSequence prefix) { byte[] endKey = prefix.getBytes().clone(); for (int i = endKey.length - 1; i >= 0; i--) { if (endKey[i] < 0xff) { // <= endKey[i] = (byte) (endKey[i] + 1); return ByteSequence.from(Arrays.copyOf(endKey, i + 1)); } } return ByteSequence.from(NO_PREFIX_END); } 

Muitos programadores acreditam que um tipo chamado byte não terá sinal. E, de fato, frequentemente em diferentes idiomas, esse é exatamente o caso. Por exemplo, em C #, o tipo de byte não está assinado. Em Java, esse não é o caso.

Na condição endKey [i] <0xff, o autor do método compara uma variável do tipo byte com o número 255 (0xff) representado na representação hexadecimal. Aparentemente, ao escrever o método, o desenvolvedor esqueceu que o intervalo de valores do tipo byte em Java é [-128, 127]. Essa condição é sempre verdadeira, portanto, o loop for sempre processará apenas o último elemento da matriz endKey .

Nono lugar: dois em um


Fonte: PVS-Studio for Java é enviado para o caminho. A próxima parada é a Elasticsearch

A expressão V6007 '(int) x <0' é sempre falsa. BCrypt.java (429)

V6025 Possivelmente o índice '(int) x' está fora dos limites. BCrypt.java (431)

 private static byte char64(char x) { if ((int)x < 0 || (int)x > index_64.length) return -1; return index_64[(int)x]; } 

Hoje temos uma oferta especial! Dois erros em um método ao mesmo tempo. A causa do primeiro erro é o tipo char , que não está assinado em Java, e é por isso que a condição (int) x <0 é sempre falsa. O segundo erro é o banal saindo dos limites da matriz index_64 quando (int) x == index_64.length . Essa situação é possível devido à condição (int) x> index_64.length . Para se livrar dos limites da matriz, é necessário substituir a condição '>' por '> ='. A condição correta seria: (int) x> = index_64.length .

Oitavo lugar: decisão e suas consequências


Fonte: Análise de código da plataforma CUBA com PVS-Studio

A expressão V6007 'previousMenuItemFlatIndex> = 0' sempre é verdadeira. CubaSideMenuWidget.java (328)

 protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) { List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem); int previousMenuItemFlatIndex = menuItemFlatIndex + 1; if (previousMenuItemFlatIndex >= 0) { // <= return menuItemWidgets.get(previousMenuItemFlatIndex); } return null; } 

O autor do método findNextMenuItem deseja se livrar do -1 retornado pelo método indexOf se a lista menuItemWidgets não contiver currentItem . Para fazer isso, ele adiciona um ao resultado indexOf (variável menuItemFlatIndex ) e armazena o valor resultante na variável previousMenuItemFlatIndex , que é mais usada no método. Esta solução para o problema -1 não tem êxito porque leva a vários erros ao mesmo tempo:

  • código nulo return nunca será executado, porque a expressão previousMenuItemFlatIndex> = 0 sempre é verdadeira, o que significa que o retorno do método findNextMenuItem sempre ocorrerá dentro do if ;
  • uma IndexOutOfBoundsException será lançada quando a lista menuItemWidgets estiver vazia, porque o primeiro elemento da lista vazia será acessado;
  • uma exceção IndexOutOfBoundsException ocorrerá quando o argumento currentItem for o último na lista menuItemWidget .

Sétimo lugar: criando um arquivo do nada


Fonte: Huawei Cloud: hoje está nublado no PVS-Studio

V6008 Dereferência nula potencial de 'dataTmpFile'. CacheManager.java (91)

 @Override public void putToCache(PutRecordsRequest putRecordsRequest) { .... if (dataTmpFile == null || !dataTmpFile.exists()) { try { dataTmpFile.createNewFile(); // <= } catch (IOException e) { LOGGER.error("Failed to create cache tmp file, return.", e); return; } } .... } 

Ao escrever o método putToCache, o programador fez um erro de digitação na condição dataTmpFile == null || ! dataTmpFile.exists () antes de criar um novo arquivo dataTmpFile.createNewFile (). Um erro de digitação é o uso do operador '==' em vez de '! ='. Este erro irá gerar um NullPointerException ao chamar o método createNewFile . A condição após a correção de um erro de digitação é assim:

 if (dataTmpFile != null || !dataTmpFile.exists()) 

“O erro foi encontrado, corrigido. Você pode relaxar ”, você pensará. Mas não importa como!

Depois de corrigir um erro, encontramos outro. Agora, uma NullPointerException pode ocorrer ao chamar dataTmpFile.exists () . Agora, para se livrar da exceção, é necessário substituir o operador '||' na condição em '&&'. A condição sob a qual todos os erros desaparecem será a seguinte:

 if (dataTmpFile != null && !dataTmpFile.exists()) 

Sexto lugar: um erro lógico muito estranho


Fonte: PVS-Studio for Java

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

 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'; } 

Este método é interessante, pois contém um erro lógico óbvio. Se o método satisfeitoBy não retornar um valor após o primeiro se , torna-se conhecido que a sequência de texto consiste em pelo menos dois caracteres. Por isso, a primeira verificação "0". Igual (texto) na próxima se não tiver sentido. O que o desenvolvedor realmente quis dizer permanece um mistério.

Quinto lugar: é a vez!


Fonte: PVS-Studio visitando o Apache Hive

V6034 A mudança no valor de 'bitShiftsInWord - 1' pode ser inconsistente com o tamanho do tipo: 'bitShiftsInWord - 1' = [-1 ... 30]. UnsignedInt128.java (1791)

 private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord; // check this because "123 << 32" will be 123. final boolean noRestore = bitShiftsInWord == 0; final int roundCarryNoRestoreMask = 1 << 31; final int roundCarryMask = (1 << (bitShiftsInWord - 1)); // <= .... } 

Com os argumentos de entrada wordShifts = 3 e bitShiftsInWord = 0 , a variável roundCarryMask , que armazena o resultado da troca de bits (1 << (bitShiftsInWord - 1)) , será um número negativo. Talvez o desenvolvedor não esperasse esse comportamento.

Quarto lugar: as exceções sairão para uma caminhada?


Fonte: PVS-Studio visitando o Apache Hive

V6051 O uso da instrução 'return' no bloco 'final' pode levar à perda de exceções não tratadas. ObjectStore.java (9080)

 private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... /*some actions*/ committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } } 

A declaração do método getMPartitionColumnStatistics está mentindo para nós, dizendo que pode gerar uma exceção. Quando qualquer exceção ocorre na tentativa , a variável confirmada permanece falsa ; portanto, no bloco final , a instrução return retorna o valor do método, e todas as exceções lançadas são perdidas e não podem ser processadas fora do método. Portanto, qualquer exceção criada nesse método nunca poderá sair dela.

Terceiro lugar: torço, giro, quero uma nova máscara


Fonte: PVS-Studio visitando o Apache Hive

V6034 A mudança no valor de 'j' pode ser inconsistente com o tamanho do tipo: 'j' = [0 ... 63]. IoTrace.java (272)

 public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j); // <= } .... } .... } 

Outro erro relacionado à mudança bit a bit, mas desta vez não apenas ele estava envolvido no caso. No loop for interno, a variável j [0 ... 63] é usada como contador de loop. Este contador está envolvido em um deslocamento de 1 << j . Nada indica problemas, no entanto, o literal inteiro '1' do tipo int (valor de 32 bits) entra em jogo aqui. Segue-se que os resultados da troca de bits começarão a se repetir depois que j for maior que 31. Se o comportamento descrito for indesejável, a unidade deverá ser representada por muito tempo , por exemplo, 1L << j ou (long) 1 << j .

Segundo lugar: ordem de inicialização


Fonte: Huawei Cloud: hoje está nublado no PVS-Studio

O ciclo de inicialização da classe V6050 está presente. A inicialização de 'INSTANCE' aparece antes da inicialização de 'LOG'. UntrustedSSL.java (32), UntrustedSSL.java (59), UntrustedSSL.java (33)

 public class UntrustedSSL { private static final UntrustedSSL INSTANCE = new UntrustedSSL(); private static final Logger LOG = LoggerFactory.getLogger(UntrustedSSL.class); .... private UntrustedSSL() { try { .... } catch (Throwable t) { LOG.error(t.getMessage(), t); // <= } } } 

A ordem na qual os campos são declarados em uma classe é importante porque os campos são inicializados na ordem em que são declarados. No entanto, quando se esquecem, ocorrem erros sutis, como este.

O analisador indicou que o campo LOG estático é desreferenciado no construtor quando é inicializado como nulo , o que leva à cadeia de exceção NullPointerException -> ExceptionInInitializerError .

“Por que, no momento da chamada do construtor, o campo estático do LOG é nulo ?”, Você pergunta.

A exceção ExceptionInInitializerError é uma dica. O fato é que esse construtor é usado para inicializar o campo estático INSTANCE declarado na classe anterior ao campo LOG . Portanto, no momento da chamada do construtor, o campo LOG ainda não foi inicializado. Para que o código funcione corretamente, é necessário inicializar o campo LOG antes de chamar o construtor.

Primeiro lugar: programação orientada para copiar e colar


Fonte: Código Apache Hadoop Qualidade: teste de produção VS

V6072 Dois fragmentos de código semelhantes foram encontrados. Talvez seja um erro de digitação e a variável 'localFiles' deva ser usada em vez de 'localArchives'. LocalDistributedCacheManager.java (183), LocalDistributedCacheManager.java (178), LocalDistributedCacheManager.java (176), LocalDistributedCacheManager.java (181)

 public synchronized void setup(JobConf conf, JobID jobId) throws IOException { .... // Update the configuration object with localized data. if (!localArchives.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALARCHIVES, StringUtils .arrayToString(localArchives.toArray(new String[localArchives // <= .size()]))); } if (!localFiles.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALFILES, StringUtils .arrayToString(localFiles.toArray(new String[localArchives // <= .size()]))); } .... } 

E o primeiro lugar é tomado por copiar e colar, ou melhor, um erro que surgiu devido ao descuido de quem cometeu essa coisa pecaminosa. É altamente provável que o segundo se tenha sido criado por copiar e colar o primeiro com a substituição de variáveis:

  • localArchives em localFiles ;
  • MRJobConfig.CACHE_LOCALARCHIVES em MRJobConfig.CACHE_LOCALFILES .

No entanto, mesmo com uma operação tão simples, foi cometido um erro, pois a variável localArchives ainda era usada na segunda linha se no segundo analisador , embora o uso de localFiles provavelmente estivesse implícito.

Conclusão


A correção de erros encontrados nas fases posteriores do desenvolvimento ou após o lançamento de um projeto requer recursos significativos. O analisador estático PVS-Studio simplifica a detecção de erros ao escrever código, o que reduz significativamente a quantidade de recursos gastos na sua correção. O uso constante do analisador já simplificou a vida dos desenvolvedores de muitas empresas . Se você deseja programar com grande prazer, tente nosso analisador .

Nossa equipe não para por aí e continuará a melhorar e aprimorar o analisador. Espere novos diagnósticos e artigos com bugs ainda mais interessantes no próximo ano.

Eu assisto você adora aventura! Primeiro, os 10 principais erros nos projetos de C # para 2019 venceram e agora o Java conseguiu superar! Bem-vindo ao próximo nível no artigo sobre os melhores erros de 2019 em projetos C ++ .





Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Valery Komarov. Os 10 principais erros encontrados em projetos Java em 2019 .

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


All Articles