Os desenvolvedores de Java têm acesso a várias ferramentas úteis que ajudam a escrever códigos de alta qualidade, como o poderoso IDE IntelliJ IDEA, analisadores gratuitos SpotBugs, PMD e similares. Os desenvolvedores que trabalham na Plataforma CUBA já estão usando tudo isso, e esta revisão mostrará como o projeto pode se beneficiar ainda mais do uso do analisador de código estático PVS-Studio.
Algumas palavras sobre o projeto e o analisador
A Plataforma CUBA é uma estrutura de alto nível para o desenvolvimento de aplicativos corporativos. A plataforma abstrai os desenvolvedores das tecnologias subjacentes, para que possam se concentrar nas tarefas de negócios, mantendo total flexibilidade, fornecendo acesso irrestrito ao código de baixo nível. O código fonte foi baixado do 
GitHub .
O PVS-Studio é uma ferramenta para detectar bugs e possíveis vulnerabilidades de segurança no código fonte dos programas escritos em C, C ++, C # e Java. O analisador é executado em sistemas Windows, Linux e macOS de 64 bits. Para facilitar as coisas para programadores Java, desenvolvemos plugins para Maven, Gradle e IntelliJ IDEA. Eu verifiquei o projeto usando o plug-in Gradle, e ele saiu sem problemas.
Erros nas condições
Aviso 1A expressão 
V6007 'StringUtils.isNotEmpty ("handleTabKey")' sempre é verdadeira. SourceCodeEditorLoader.java (60)
@Override public void loadComponent() { .... String handleTabKey = element.attributeValue("handleTabKey"); if (StringUtils.isNotEmpty("handleTabKey")) { resultComponent.setHandleTabKey(Boolean.parseBoolean(handleTabKey)); } .... } 
O valor do atributo extraído do elemento não é verificado. Em vez disso, a função 
isNotEmpty obtém uma string literal como argumento, em vez da variável 
handleTabKey .
Um erro semelhante foi encontrado no arquivo AbstractTableLoader.java:
- A expressão V6007 'StringUtils.isNotEmpty ("editable")' sempre é verdadeira. AbstractTableLoader.java (596)
Aviso 2A 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; } 
A função 
indexOf retornará 
-1 se o elemento não for encontrado na lista. O valor 
1 é então adicionado ao índice, o que disfarça o problema com o elemento ausente. Outro problema em potencial tem a ver com o fato de que a variável 
previousMenuItemFlatIndex sempre será maior ou igual a zero. Por exemplo, se a lista 
menuItemWidgets estiver vazia, o programa terminará com uma saturação de matriz.
Aviso 3V6009 A função 'delete' pode receber o valor '-1' enquanto se espera um valor não negativo. Argumento de inspeção: 1. AbstractCollectionDatasource.java (556)
 protected DataLoadContextQuery createDataQuery(....) { .... StringBuilder orderBy = new StringBuilder(); .... if (orderBy.length() > 0) { orderBy.delete(orderBy.length() - 2, orderBy.length()); orderBy.insert(0, " order by "); } .... } 
Os dois últimos caracteres do buffer 
orderBy serão excluídos se o número total de elementos for maior que zero, ou seja, se a sequência contiver pelo menos um caractere. No entanto, a posição inicial de onde a exclusão começa é deslocada por 2. Portanto, se 
orderBy contiver um caractere, tentar excluí-lo aumentará uma 
StringIndexOutOfBoundsException .
Aviso 4V6013 Os objetos 'masterCollection' e 'entidades' são comparados por referência. Possivelmente uma comparação de igualdade foi planejada. CollectionPropertyContainerImpl.java (81)
 @Override public void setItems(@Nullable Collection<E> entities) { super.setItems(entities); Entity masterItem = master.getItemOrNull(); if (masterItem != null) { MetaProperty masterProperty = getMasterProperty(); Collection masterCollection = masterItem.getValue(masterProperty.getName()); if (masterCollection != entities) { updateMasterCollection(masterProperty, masterCollection, entities); } } } 
Na função 
updateMasterCollection , os valores das 
entidades são copiados para 
masterCollection . Uma linha anterior, as coleções foram comparadas por referência, mas o programador provavelmente pretendia que fosse uma comparação por valor.
Aviso 5V6013 Os objetos 'value' e 'oldValue' são comparados por referência. Possivelmente uma comparação de igualdade foi planejada. WebOptionsList.java (278)
 protected boolean isCollectionValuesChanged(Collection<I> value, Collection<I> oldValue) { return value != oldValue; } 
Este caso é semelhante ao anterior. As coleções são comparadas na função 
isCollectionValuesChanged e a comparação de referência talvez também não seja o que foi planejado aqui.
Condições redundantes
Aviso 1A expressão 
V6007 'mask.charAt (i + deslocamento)! = PlaceHolder' sempre é verdadeira. DatePickerDocument.java (238)
 private String calculateFormattedString(int offset, String text) .... { .... if ((mask.charAt(i + offset) == placeHolder)) {  
A segunda condição verifica uma expressão que é oposta à verificada na primeira condição. O último pode, portanto, ser removido com segurança para encurtar o código.
A expressão 
V6007 'conector == nulo' é sempre falsa. HTML5Support.java (169)
 private boolean validate(NativeEvent event) { .... while (connector == null) { widget = widget.getParent(); connector = Util.findConnectorFor(widget); } if (this.connector == connector) { return true; } else if (connector == null) {  
Depois de sair do loop while, o valor da variável do 
conector não será igual a 
nulo ; portanto, a verificação redundante pode ser excluída.
Outro aviso suspeito desse tipo que precisa ser examinado:
- A expressão V6007 'StringUtils.isBlank (strValue)' sempre é verdadeira. Param.java (818)
Código inacessível em testes
V6019 Código inacessível detectado. É possível que haja um erro. TransactionTest.java (283)
 private void throwException() { throw new RuntimeException(TEST_EXCEPTION_MSG); } @Test public void testSuspendRollback() { Transaction tx = cont.persistence().createTransaction(); try { .... Transaction tx1 = cont.persistence().createTransaction(); try { EntityManager em1 = cont.persistence().getEntityManager(); assertTrue(em != em1); Server server1 = em1.find(Server.class, server.getId()); assertNull(server1); throwException();  
A função 
throwException lança uma exceção que impede a execução da chamada de 
tx1.commit . Essas duas linhas devem ser trocadas para que o código funcione corretamente.
Também houve alguns problemas semelhantes em outros testes:
- V6019 Código inacessível detectado. É possível que haja um erro. TransactionTest.java (218)
- V6019 Código inacessível detectado. É possível que haja um erro. TransactionTest.java (163)
- V6019 Código inacessível detectado. É possível que haja um erro. TransactionTest.java (203)
- V6019 Código inacessível detectado. É possível que haja um erro. TransactionTest.java (137)
- V6019 Código inacessível detectado. É possível que haja um erro. UpdateDetachedTest.java (153)
- V6019 Código inacessível detectado. É possível que haja um erro. EclipseLinkDetachedTest.java (132)
- V6019 Código inacessível detectado. É possível que haja um erro. PersistenceTest.java (223)
Argumentos suspeitos
Aviso 1V6023 O parâmetro 'salt' é sempre reescrito no corpo do método antes de ser usado. BCryptEncryptionModule.java (47)
 @Override public String getHash(String content, String salt) { salt = BCrypt.gensalt(); return BCrypt.hashpw(content, salt); } 
Na criptografia, 
salt é uma cadeia de dados que você passa junto com a senha para uma função hash. É usado principalmente para proteger o programa contra ataques de dicionário e ataques de tabelas arco-íris, além de ocultar senhas idênticas. Mais aqui: 
Sal (criptografia) .
Nesta função, a sequência passada é substituída logo após a entrada. Ignorar o valor passado para a função é uma vulnerabilidade em potencial.
Aviso 2Esta função disparou dois avisos ao mesmo tempo:
- V6023 O parâmetro 'offsetWidth' é sempre reescrito no corpo do método antes de ser usado. CubaSuggestionFieldWidget.java (433)
- V6023 O parâmetro 'offsetHeight' é sempre reescrito no corpo do método antes de ser usado. CubaSuggestionFieldWidget.java (433)
 @Override public void setPosition(int offsetWidth, int offsetHeight) { offsetHeight = getOffsetHeight(); .... if (offsetHeight + getPopupTop() > ....)) { .... } .... offsetWidth = containerFirstChild.getOffsetWidth(); if (offsetWidth + getPopupLeft() > ....)) { .... } else { left = getPopupLeft(); } setPopupPosition(left, top); } 
Esse é um trecho bastante curioso. A função é chamada com apenas duas variáveis como argumentos, 
offsetWidth e 
offsetHeight , e ambas são substituídas antes do uso.
Aviso 3V6022 O parâmetro 'atalho' não é usado dentro do corpo do construtor. DeclarativeTrackingAction.java (47)
 public DeclarativeTrackingAction(String id, String caption, String description, String icon, String enable, String visible, String methodName, @Nullable String shortcut, ActionsHolder holder) { super(id); this.caption = caption; this.description = description; this.icon = icon; setEnabled(enable == null || Boolean.parseBoolean(enable)); setVisible(visible == null || Boolean.parseBoolean(visible)); this.methodName = methodName; checkActionsHolder(holder); } 
A função não utiliza o valor passado como parâmetro de 
atalho . Talvez a interface da função tenha se tornado obsoleta ou esse aviso seja apenas um falso positivo.
Mais alguns defeitos desse tipo:
- V6022 O parâmetro 'type' não é usado dentro do corpo do construtor. QueryNode.java (36)
- V6022 O parâmetro 'text2' não é usado dentro do corpo do construtor. MarkerAddition.java (22)
- V6022 O parâmetro 'seleção' não é usado dentro do corpo do construtor. AceEditor.java (114)
- V6022 O parâmetro 'options' não é usado dentro do corpo do construtor. EntitySerialization.java (379)
Funções diferentes, mesmo código
Aviso 1V6032 É estranho que o corpo do método 'firstItemId' seja totalmente equivalente ao corpo de outro método 'lastItemId'. ContainerTableItems.java (213), ContainerTableItems.java (219)
 @Override public Object firstItemId() { List<E> items = container.getItems(); return items.isEmpty() ? null : items.get(0).getId(); } @Override public Object lastItemId() { List<E> items = container.getItems(); return items.isEmpty() ? null : items.get(0).getId(); } 
As funções 
firstItemId e 
lastItemId têm as mesmas implementações. O último provavelmente foi criado para obter o índice do último elemento em vez de obtê-lo no índice 0.
Aviso 2V6032 É estranho que o corpo do método seja totalmente equivalente ao corpo de outro método. SearchComboBoxPainter.java (495), SearchComboBoxPainter.java (501)
 private void paintBackgroundDisabledAndEditable(Graphics2D g) { rect = decodeRect1(); g.setPaint(color53); g.fill(rect); } private void paintBackgroundEnabledAndEditable(Graphics2D g) { rect = decodeRect1(); g.setPaint(color53); g.fill(rect); } 
Mais duas funções com corpos suspeitosamente idênticos. Meu palpite é que um deles foi criado para funcionar com alguma outra cor em vez de com a 
cor53 .
Desreferência nula
Aviso 1V6060 A referência 'descriptionPopup' foi utilizada antes de ser verificada com relação a nulo. SuggestPopup.java (252), SuggestPopup.java (251)
 protected void updateDescriptionPopupPosition() { int x = getAbsoluteLeft() + WIDTH; int y = getAbsoluteTop(); descriptionPopup.setPopupPosition(x, y); if (descriptionPopup!=null) { descriptionPopup.setPopupPosition(x, y); } } 
Em apenas duas linhas, o programador conseguiu escrever um pedaço de código altamente suspeito. Primeiro, o método 
setPopupPosition do objeto 
descriptionPopup é chamado e, em seguida, o objeto é verificado como 
nulo . A primeira chamada para 
setPopupPosition provavelmente é redundante e potencialmente perigosa. Eu acho que resulta de uma refatoração ruim.
Aviso 2V6060 A referência 'tableModel' foi utilizada antes de ser verificada com relação a nulo. DesktopAbstractTable.java (1580), DesktopAbstractTable.java (1564)
 protected Column addRuntimeGeneratedColumn(String columnId) {  
Este caso é semelhante ao anterior. Quando o objeto 
tableModel é verificado como 
nulo , ele já foi acessado várias vezes.
Outro exemplo:
- V6060 A referência 'tableModel' foi utilizada antes de ser verificada com relação a nulo. DesktopAbstractTable.java (596), DesktopAbstractTable.java (579)
Provavelmente um erro lógico
V6026 Este valor já está atribuído à variável 'sortAscending'. CubaScrollTableWidget.java (488)
 @Override protected void sortColumn() { .... if (sortAscending) { if (sortClickCounter < 2) {  
Na primeira condição, a variável 
sortAscending já recebeu o valor 
true , mas ainda assim, o mesmo valor novamente. Isso deve ser um erro, e o autor provavelmente quis dizer o valor 
falso .
Um exemplo semelhante de um arquivo diferente:
- V6026 Este valor já está atribuído à variável 'sortAscending'. CubaTreeTableWidget.java (444)
Valores de retorno estranhos
Aviso 1V6037 Um 'retorno' incondicional dentro de um loop. QueryCacheManager.java (128)
 public <T> T getSingleResultFromCache(QueryKey queryKey, List<View> views) { .... for (Object id : queryResult.getResult()) { return (T) em.find(metaClass.getJavaClass(), id, views.toArray(....)); } .... } 
O analisador detectou uma chamada incondicional para 
retornar na primeira iteração de um loop 
for . Essa linha está incorreta ou o loop deve ser reescrito como uma declaração 
if .
Aviso 2V6014 É estranho que esse método sempre retorne um e o mesmo valor. DefaultExceptionHandler.java (40)
 @Override public boolean handle(ErrorEvent event, App app) { Throwable t = event.getThrowable(); if (t instanceof SocketException || ExceptionUtils.getRootCause(t) instanceof SocketException) { return true; } if (ExceptionUtils.getThrowableList(t).stream() .anyMatch(o -> o.getClass().getName().equals("...."))) { return true; } if (StringUtils.contains(ExceptionUtils.getMessage(t), "....")) { return true; } AppUI ui = AppUI.getCurrent(); if (ui == null) { return true; } if (t != null) { if (app.getConnection().getSession() != null) { showDialog(app, t); } else { showNotification(app, t); } } return true; } 
Essa função retorna 
true em cada caso, enquanto a última linha obviamente exige 
false . Parece um erro.
Aqui está uma lista completa de outras funções suspeitas semelhantes:
- V6014 É estranho que esse método sempre retorne um e o mesmo valor. ErrorNodesFinder.java (31)
- V6014 É estranho que esse método sempre retorne um e o mesmo valor. FileDownloadController.java (69)
- V6014 É estranho que esse método sempre retorne um e o mesmo valor. IdVarSelector.java (73)
- V6014 É estranho que esse método sempre retorne um e o mesmo valor. IdVarSelector.java (48)
- V6014 É estranho que esse método sempre retorne um e o mesmo valor. IdVarSelector.java (67)
- V6014 É estranho que esse método sempre retorne um e o mesmo valor. IdVarSelector.java (46)
- V6014 É estranho que esse método sempre retorne um e o mesmo valor. JoinVariableNode.java (57)
Aviso 3A expressão 
V6007 'needReload' é sempre falsa. WebAbstractTable.java (2702)
 protected boolean handleSpecificVariables(Map<String, Object> variables) { boolean needReload = false; if (isUsePresentations() && presentations != null) { Presentations p = getPresentations(); if (p.getCurrent() != null && p.isAutoSave(p.getCurrent()) && needUpdatePresentation(variables)) { Element e = p.getSettings(p.getCurrent()); saveSettings(e); p.setSettings(p.getCurrent(), e); } } return needReload; } 
A função retorna a variável 
needReload cujo valor é sempre 
falso . Algum código para alterar esse valor provavelmente está ausente de uma das condições.
Aviso 4V6062 Possível recursão infinita dentro do método 'isFocused'. GwtAceEditor.java (189), GwtAceEditor.java (190)
 public final native void focus() ; public final boolean isFocused() { return this.isFocused(); } 
O analisador detectou uma função recursiva sem condição de parada. Este arquivo contém muitas funções marcadas com a palavra-chave 
nativa e contendo código comentado. Os desenvolvedores provavelmente estão reescrevendo esse arquivo agora e em breve notarão a função 
isFocused também.
Diversos
Aviso 1V6002 A instrução switch não cobre todos os valores da enumeração 'Operation': ADD. DesktopAbstractTable.java (665)
  enum Operation { REFRESH, CLEAR, ADD, REMOVE, UPDATE } @Override public void setDatasource(final CollectionDatasource datasource) { .... collectionChangeListener = e -> { switch (e.getOperation()) { case CLEAR: case REFRESH: fieldDatasources.clear(); break; case UPDATE: case REMOVE: for (Object entity : e.getItems()) { fieldDatasources.remove(entity); } break; } }; .... } 
A instrução 
switch não tem nenhum argumento para o valor 
ADD . É o único valor que não está sendo verificado, portanto, os desenvolvedores devem dar uma olhada neste código.
Aviso 2V6021 A 'fonte' variável não é usada. DefaultHorizontalLayoutDropHandler.java (177)
 @Override protected void handleHTML5Drop(DragAndDropEvent event) { LayoutBoundTransferable transferable = (LayoutBoundTransferable) event .getTransferable(); HorizontalLayoutTargetDetails details = (HorizontalLayoutTargetDetails) event .getTargetDetails(); AbstractOrderedLayout layout = (AbstractOrderedLayout) details .getTarget(); Component source = event.getTransferable().getSourceComponent();  
A 
fonte da variável é declarada, mas não usada. Talvez os autores tenham esquecido de adicionar 
fonte ao 
layout , assim como aconteceu com outra variável desse tipo, 
comp .
Outras funções com variáveis não utilizadas:
- V6021 A 'fonte' variável não é usada. DefaultHorizontalLayoutDropHandler.java (175)
- V6021 O valor é atribuído à variável 'r', mas não é usado. ExcelExporter.java (262)
- V6021 A variável 'over' não é usada. DefaultCssLayoutDropHandler.java (49)
- V6021 A variável 'transferível' não é usada. DefaultHorizontalLayoutDropHandler.java (171)
- V6021 A variável 'transferível' não é usada. DefaultHorizontalLayoutDropHandler.java (169)
- V6021 A variável 'beanLocator' não é usada. ScreenEventMixin.java (28)
Aviso 3As classes 
V6054 não devem ser comparadas pelo nome. MessageTools.java (283)
 public boolean hasPropertyCaption(MetaProperty property) { Class<?> declaringClass = property.getDeclaringClass(); if (declaringClass == null) return false; String caption = getPropertyCaption(property); int i = caption.indexOf('.'); if (i > 0 && declaringClass.getSimpleName().equals(caption.substring(0, i))) return false; else return true; } 
O analisador detectou uma comparação de classe por nome. É incorreto comparar as classes pelo nome, pois, de acordo com a especificação, os nomes das classes da JVM devem ser exclusivos apenas dentro de um pacote. Essa comparação gera resultados incorretos e leva à execução do código errado.
Comentado por desenvolvedores da plataforma CUBA
Qualquer projeto grande certamente possui bugs. Sabendo disso, estamos de bom grado quando a equipe do PVS-Studio se ofereceu para verificar nosso projeto. O repositório CUBA contém garfos de algumas das bibliotecas OSS de terceiros licenciadas sob o Apache 2, e parece que deveríamos prestar mais atenção a esse código, pois o analisador encontrou vários problemas nessas fontes. Atualmente, usamos o SpotBugs como nosso analisador principal e ele não percebe alguns dos grandes erros relatados pelo PVS-Studio. Parece que deveríamos escrever alguns diagnósticos adicionais. Muito obrigado à equipe PVS-Studio pelo trabalho.Os desenvolvedores também nos disseram que os avisos V6013 e V6054 eram falsos positivos; foi a decisão consciente deles de escrever esse código da maneira que eles escreveram. O analisador foi projetado para detectar fragmentos de código suspeitos, e a probabilidade de encontrar erros genuínos varia de acordo com diferentes diagnósticos. Esses avisos, no entanto, podem ser facilmente manipulados usando o mecanismo especial de 
supressão de avisos em massa sem a necessidade de modificar os arquivos de origem.
Além disso, a equipe do PVS-Studio não pode ignorar a frase "parece que deveríamos escrever alguns diagnósticos adicionais" e ficar sem essa imagem :)
Conclusão
O PVS-Studio pode ser um complemento perfeito para as ferramentas de controle de qualidade existentes usadas em seu processo de desenvolvimento. É especialmente verdade para empresas com dezenas, centenas ou milhares de desenvolvedores. O PVS-Studio foi projetado não apenas para detectar bugs, mas também para ajudá-lo a corrigi-los, e o que quero dizer com isso não é edição automática de código, mas meios confiáveis de controle de qualidade de código. Em uma grande empresa, é impossível para todo desenvolvedor verificar suas respectivas partes do código com ferramentas diferentes; portanto, uma solução melhor para essas empresas seria adotar ferramentas como o PVS-Studio, que fornece controle de qualidade do código em todas as etapas do desenvolvimento, não somente no lado do programador.