Pour les programmeurs Java, il existe des outils utiles pour vous aider à écrire du code de haute qualité, par exemple, le puissant environnement de développement IntelliJ IDEA, des SpotBugs gratuits, des analyseurs PMD et autres. Tout cela est déjà utilisé dans le développement du projet CUBA Platform, et dans cette revue des défauts de code trouvés, je vais vous dire comment améliorer la qualité du projet en utilisant l'analyseur de code statique PVS-Studio.
À propos du projet et de l'analyseur
CUBA Platform est un cadre Java de haut niveau pour créer rapidement des applications d'entreprise avec une interface Web complète. La plate-forme soustrait le développeur à des technologies hétérogènes afin que vous puissiez vous concentrer sur la résolution de problèmes commerciaux, en même temps, sans qu'il soit impossible de travailler directement avec eux. Le code source est extrait du référentiel sur
GitHub .
PVS-Studio est un outil pour détecter les erreurs et les vulnérabilités potentielles dans le code source des programmes écrits en C, C ++, C # et Java. Il fonctionne sur les systèmes 64 bits sous Windows, Linux et macOS. Pour la commodité des programmeurs Java, nous avons développé des plugins pour Maven, Gradle et IntelliJ IDEA. Le projet CUBA Platform a été facilement vérifié à l'aide du plugin Gradle.
Erreurs dans les conditions
Avertissement 1V6007 L' expression 'StringUtils.isNotEmpty ("handleTabKey")' est toujours vraie. SourceCodeEditorLoader.java (60)
@Override public void loadComponent() { .... String handleTabKey = element.attributeValue("handleTabKey"); if (StringUtils.isNotEmpty("handleTabKey")) { resultComponent.setHandleTabKey(Boolean.parseBoolean(handleTabKey)); } .... }
Après avoir récupéré la valeur d'attribut d'un certain élément, cette valeur n'est pas vérifiée. Au lieu de cela, une chaîne constante est passée à la fonction
isNotEmpty , mais nous avons dû passer la variable
handleTabKey .
Il existe une autre erreur similaire dans le fichier AbstractTableLoader.java:
- V6007 L'expression 'StringUtils.isNotEmpty ("editable")' est toujours vraie. AbstractTableLoader.java (596)
Avertissement 2V6007 L' expression 'previousMenuItemFlatIndex> = 0' est toujours vraie. 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; }
La fonction
indexOf peut renvoyer
-1 si l'élément n'est pas trouvé dans la liste. Ensuite, un est ajouté à l'index, masquant ainsi la situation lorsque l'élément souhaité est manquant. Un autre problème potentiel peut être le fait que la variable
previousMenuItemFlatIndex sera toujours supérieure ou égale à zéro. Si, par exemple, la liste
menuItemWidgets est vide, il devient possible de dépasser la limite du tableau.
Avertissement 3V6009 La fonction 'supprimer' pourrait recevoir la valeur '-1' alors qu'une valeur non négative est attendue. Inspectez l'argument: 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 "); } .... }
Dans l'ordre du tampon de caractères
By, les 2 derniers caractères sont supprimés si leur nombre total est supérieur à zéro, c'est-à-dire une chaîne contient un ou plusieurs caractères. Mais la position de départ pour la suppression des caractères a été définie avec un décalage de 2 caractères. Ainsi, si
orderBy se compose soudainement de 1 caractère, une tentative de suppression
lèvera une
StringIndexOutOfBoundsException .
Avertissement 4V6013 Les objets 'masterCollection' et 'entités' sont comparés par référence. Une comparaison de l'égalité était peut-être envisagée. 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); } } }
Dans la fonction
updateMasterCollection, les valeurs des
entités sont copiées dans
masterCollection . Avant cela, les collections étaient comparées par référence, mais il était peut-être prévu de les comparer par valeur.
Avertissement 5V6013 Les objets 'value' et 'oldValue' sont comparés par référence. Une comparaison de l'égalité était peut-être envisagée. WebOptionsList.java (278)
protected boolean isCollectionValuesChanged(Collection<I> value, Collection<I> oldValue) { return value != oldValue; }
Cet exemple est similaire au précédent. Voici
isCollectionValuesChanged pour comparer les collections. Peut-être que la comparaison par référence n'est pas non plus la manière attendue.
Conditions excédentaires
Avertissement 1V6007 L' expression «mask.charAt (i + offset)! = PlaceHolder» est toujours vraie. DatePickerDocument.java (238)
private String calculateFormattedString(int offset, String text) .... { .... if ((mask.charAt(i + offset) == placeHolder)) {
Dans la deuxième comparaison, une expression opposée à la première est vérifiée. La deuxième vérification peut être supprimée pour simplifier le code.
V6007 L' expression 'connecteur == null' est toujours fausse. 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) {
Une fois la
boucle while terminée, la valeur de la variable de
connecteur ne sera pas
nulle , par conséquent, la vérification redondante peut être supprimée.
Un autre endroit suspect auquel les développeurs doivent prêter attention:
- V6007 L'expression 'StringUtils.isBlank (strValue)' est toujours vraie. Param.java (818)
Code inaccessible dans les tests
V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. 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();
La fonction
throwException lève une exception qui
empêche l' appel à la fonction
tx1.commit. Peut-être que ces lignes devraient être échangées.
Quelques endroits plus similaires dans d'autres tests:
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TransactionTest.java (218)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TransactionTest.java (163)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TransactionTest.java (203)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TransactionTest.java (137)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. UpdateDetachedTest.java (153)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. EclipseLinkDetachedTest.java (132)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. PersistenceTest.java (223)
Arguments de fonction suspecte
Avertissement 1V6023 Le paramètre 'salt' est toujours réécrit dans le corps de la méthode avant d'être utilisé. BCryptEncryptionModule.java (47)
@Override public String getHash(String content, String salt) { salt = BCrypt.gensalt(); return BCrypt.hashpw(content, salt); }
En cryptographie,
salt est une chaîne de données qui est transmise à une fonction de hachage avec un mot de passe. Il est principalement utilisé pour la protection contre les recherches de dictionnaire et les attaques à l'aide de tables arc-en-ciel, ainsi que pour masquer les mêmes mots de passe. En savoir plus:
Salt (cryptographie) .
Dans cette fonction, la ligne est frottée dès l'entrée dans la fonction. Ignorer la valeur transmise est peut-être une vulnérabilité potentielle.
Avertissement 2Pour la fonction considérée, l'analyseur émet deux avertissements à la fois:
- V6023 Le paramètre 'offsetWidth' est toujours réécrit dans le corps de la méthode avant d'être utilisé. CubaSuggestionFieldWidget.java (433)
- V6023 Le paramètre 'offsetHeight' est toujours réécrit dans le corps de la méthode avant d'être utilisé. 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); }
Code amusant. La fonction ne prend que deux variables:
offsetWidth et
offsetHeight , les deux sont écrasées avant utilisation.
Avertissement 3V6022 Le paramètre 'raccourci' n'est pas utilisé dans le corps du constructeur. 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); }
La valeur du paramètre de
raccourci n'est pas utilisée dans la fonction. L'interface de fonction est peut-être obsolète ou cet avertissement n'est pas une erreur.
Quelques endroits plus similaires:
- V6022 Le paramètre 'type' n'est pas utilisé dans le corps du constructeur. QueryNode.java (36)
- V6022 Le paramètre 'text2' n'est pas utilisé dans le corps du constructeur. MarkerAddition.java (22)
- V6022 Le paramètre 'sélection' n'est pas utilisé à l'intérieur du corps du constructeur. AceEditor.java (114)
- V6022 Le paramètre 'options' n'est pas utilisé dans le corps du constructeur. EntitySerialization.java (379)
Différentes fonctions avec le même code
Avertissement 1V6032 Il est étrange que le corps de la méthode 'firstItemId' soit entièrement équivalent au corps d'une autre méthode '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(); }
Les fonctions
firstItemId et
lastItemId ont la même implémentation. Très probablement, dans ce dernier, il était nécessaire d'obtenir un élément non pas avec l'indice
0 , mais de calculer l'indice du dernier élément.
Avertissement 2V6032 Il est étrange que le corps de la méthode soit entièrement équivalent au corps d'une autre méthode. 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); }
Deux autres fonctions avec une mise en œuvre étrangement identique. Je me risquerais à suggérer que dans l'un d'eux, il était nécessaire d'utiliser une couleur différente de
color53 .
Référence nulle
Avertissement 1V6060 La référence 'descriptionPopup' a été utilisée avant d'être vérifiée par rapport à null. 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); } }
En seulement deux lignes, l'auteur a réussi à écrire un code très suspect. Tout d'abord, la méthode
setPopupPosition est appelée sur l'objet
descriptionPopup , puis l'objet est comparé à
null . Très probablement, le premier appel à la fonction
setPopupPosition est redondant et dangereux. Il ressemble aux conséquences d'un refactoring échoué.
Avertissements 2V6060 La référence 'tableModel' a été utilisée avant d'être vérifiée par rapport à null. DesktopAbstractTable.java (1580), DesktopAbstractTable.java (1564)
protected Column addRuntimeGeneratedColumn(String columnId) {
Une situation similaire existe dans cette fonction. Après de nombreux appels à l'objet
tableModel , une vérification est effectuée pour
voir s'il est
nul ou non.
Un autre exemple:
- V6060 La référence 'tableModel' a été utilisée avant d'être vérifiée par rapport à null. DesktopAbstractTable.java (596), DesktopAbstractTable.java (579)
Peut-être une erreur logique
V6026 Cette valeur est déjà affectée à la variable 'sortAscending'. CubaScrollTableWidget.java (488)
@Override protected void sortColumn() { .... if (sortAscending) { if (sortClickCounter < 2) {
Dans la première condition, la variable
sortAscending est
déjà vraie , mais elle reçoit toujours la même valeur. C'est peut-être une erreur et je voulais attribuer un
faux .
Un exemple similaire d'un autre fichier:
- V6026 Cette valeur est déjà affectée à la variable 'sortAscending'. CubaTreeTableWidget.java (444)
Valeurs de retour de fonction étranges
Avertissement 1V6037 Un «retour» inconditionnel dans une boucle. 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(....)); } .... }
L'analyseur a détecté un appel inconditionnel à l'
instruction return à la première itération de la boucle
for . Il s'agit peut-être d'une erreur ou vous devez réécrire le code pour utiliser l'
instruction if .
Avertissement 2V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. 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; }
Dans tous les cas, cette fonction renvoie
true . Mais ici, dans la toute dernière ligne, je demande le retour du
faux . Il y a peut-être une erreur.
La liste complète des fonctions suspectes avec un code similaire:
- V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. ErrorNodesFinder.java (31)
- V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. FileDownloadController.java (69)
- V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. IdVarSelector.java (73)
- V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. IdVarSelector.java (48)
- V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. IdVarSelector.java (67)
- V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. IdVarSelector.java (46)
- V6014 Il est étrange que cette méthode renvoie toujours une seule et même valeur. JoinVariableNode.java (57)
Avertissement 3V6007 L' expression 'needReload' est toujours fausse. 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; }
La fonction renvoie la variable
needReload , dont la valeur est toujours
false . Très probablement, dans l'une des conditions, ils ont oublié d'ajouter le code pour changer la valeur de la variable.
Avertissement 4V6062 Récursion infinie possible à l'intérieur de la méthode 'isFocused'. GwtAceEditor.java (189), GwtAceEditor.java (190)
public final native void focus() ; public final boolean isFocused() { return this.isFocused(); }
L'analyseur a détecté une fonction appelée récursivement sans condition pour arrêter la récursivité. Ce fichier possède de nombreuses fonctions balisées avec le mot-clé
natif et contenant du code commenté. Très probablement, le fichier est en train d'être écrasé et bientôt les développeurs feront attention à la fonction
isFocused .
Avertissements divers
Avertissement 1V6002 L'instruction switch ne couvre pas toutes les valeurs de l'énumération '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; } }; .... }
L'
instruction switch ne traite pas la valeur de
ADD . C'est le seul qui n'a pas été revu, il vaut donc la peine de vérifier s'il s'agit d'une erreur ou non.
Avertissement 2V6021 La «source» variable n'est pas utilisée. 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();
La variable source est déclarée et n'est pas utilisée dans le code. Peut-être, comme une autre variable
comp du même type, ils ont oublié d'ajouter la
source à la
mise en page .
Plus de fonctions avec des variables inutilisées:
- V6021 La «source» variable n'est pas utilisée. DefaultHorizontalLayoutDropHandler.java (175)
- V6021 La valeur est affectée à la variable 'r' mais n'est pas utilisée. ExcelExporter.java (262)
- V6021 La variable 'over' n'est pas utilisée. DefaultCssLayoutDropHandler.java (49)
- V6021 La variable «transférable» n'est pas utilisée. DefaultHorizontalLayoutDropHandler.java (171)
- V6021 La variable «transférable» n'est pas utilisée. DefaultHorizontalLayoutDropHandler.java (169)
- V6021 La variable 'beanLocator' n'est pas utilisée. ScreenEventMixin.java (28)
Avertissement 3Les classes
V6054 ne doivent pas être comparées par leur nom. 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; }
L'analyseur a détecté une situation lorsque la comparaison des classes se fait par nom. Une telle comparaison est incorrecte, car, selon la spécification, les classes JVM ont un nom unique uniquement à l'intérieur du package. Cela peut entraîner une comparaison et une exécution incorrectes du mauvais code prévu.
Commentaires des développeurs de la plateforme CUBA
Bien sûr, dans tout projet majeur, il y a des bugs. C'est pourquoi nous avons accepté avec plaisir la proposition de l'équipe PVS-Studio de vérifier notre projet. Le référentiel CUBA comprend des fourches de certaines bibliothèques OSS tierces sous la licence Apache 2, et il semble que nous devons prêter plus d'attention à ce code, l'analyseur a trouvé pas mal de problèmes dans ces sources. Maintenant, nous utilisons SpotBugs comme analyseur principal, et il ne trouve pas de problèmes importants trouvés par PVS-Studio. Il est temps d'aller rédiger vous-même des chèques supplémentaires. Un grand merci à l'équipe PVS-Studio pour le travail accompli.Les développeurs ont également noté que les avertissements V6013 et V6054 étaient faux. Le code a été écrit de manière intentionnelle. L'analyseur statique est conçu pour détecter les fragments de code suspects et la probabilité de trouver une erreur est différente pour toutes les inspections. Néanmoins, il est facile de travailler avec de tels avertissements en utilisant le mécanisme pratique de
suppression en masse des avertissements de l'analyseur sans modifier les fichiers de code source.
Une autre équipe de PVS-Studio ne peut pas ignorer la phrase «il est temps d'aller écrire nous-mêmes des chèques supplémentaires» et de ne pas laisser cette image :)
Conclusion
PVS-Studio sera un excellent ajout à votre projet existant pour améliorer les outils de qualité du code. Surtout s'il y a des dizaines, des centaines et des milliers d'employés. PVS-Studio est conçu non seulement pour trouver des erreurs, mais aussi pour les corriger. De plus, nous ne parlons pas d'édition automatique de code, mais d'un contrôle fiable de la qualité du code. Dans une grande entreprise, il est impossible d'imaginer une situation où absolument tous les développeurs vérifieront indépendamment leur code avec divers outils, de sorte que des outils comme PVS-Studio conviennent mieux à ces entreprises, où le contrôle de la qualité du code est fourni à toutes les étapes du développement, pas seulement pour un programmeur ordinaire.

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Svyatoslav Razmyslov.
Analyse du code de la plateforme CUBA avec PVS-Studio