Les développeurs Java ont accès à un certain nombre d'outils utiles qui aident à écrire du code de haute qualité tels que le puissant IDE IntelliJ IDEA, les analyseurs gratuits SpotBugs, PMD, etc. Les développeurs travaillant sur la plate-forme CUBA ont déjà utilisé tout cela, et cette revue montrera comment le projet peut bénéficier encore plus de l'utilisation de l'analyseur de code statique PVS-Studio.
Quelques mots sur le projet et l'analyseur
La plateforme CUBA est un cadre de haut niveau pour le développement d'applications d'entreprise. La plateforme fait abstraction des développeurs des technologies sous-jacentes afin qu'ils puissent se concentrer sur les tâches métier, tout en conservant une flexibilité totale en offrant un accès illimité au code de bas niveau. Le code source a été téléchargé depuis
GitHub .
PVS-Studio est un outil pour détecter les bogues et les failles de sécurité potentielles dans le code source des programmes écrits en C, C ++, C # et Java. L'analyseur fonctionne sur les systèmes Windows, Linux et macOS 64 bits. Pour faciliter les choses pour les programmeurs Java, nous avons développé des plugins pour Maven, Gradle et IntelliJ IDEA. J'ai vérifié le projet en utilisant le plugin Gradle, et il s'est déroulé sans accroc.
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)); } .... }
La valeur d'attribut extraite de l'élément n'est pas vérifiée. Au lieu de cela, la fonction
isNotEmpty obtient un littéral de chaîne comme argument plutôt que la variable
handleTabKey .
Une erreur similaire a été trouvée 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 retournera
-1 si l'élément n'est pas trouvé dans la liste. La valeur
1 est ensuite ajoutée à l'index, ce qui masque le problème avec l'élément absent. Un autre problème potentiel est lié au fait que la variable
précédenteMenuItemFlatIndex sera toujours supérieure ou égale à zéro. Par exemple, si la liste
menuItemWidgets s'avère vide, le programme se retrouvera avec un dépassement de 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 "); } .... }
Les deux derniers caractères du tampon
orderBy sont supprimés si le nombre total d'éléments est supérieur à zéro, c'est-à-dire si la chaîne contient au moins un caractère. Cependant, la position de départ à partir de laquelle la suppression commence est décalée de 2. Ainsi, si
orderBy contient un caractère, la tentative de suppression
déclenchera 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 . Une ligne plus tôt, les collections ont été comparées par référence, mais le programmeur a probablement voulu que ce soit une comparaison 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; }
Ce cas est similaire au précédent. Les collections sont comparées dans la fonction
isCollectionValuesChanged , et la comparaison de référence n'est peut-être pas non plus ce qui était prévu ici.
Conditions redondantes
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)) {
La deuxième condition vérifie une expression opposée à celle vérifiée dans la première condition. Ce dernier peut donc être supprimé en toute sécurité pour raccourcir 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) {
Après avoir quitté la boucle while, la valeur de la variable de
connecteur ne sera pas égale à
null , donc le contrôle redondant peut être supprimé.
Un autre avertissement suspect de ce type qui doit être examiné:
- 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'exécution de l'appel de
tx1.commit . Ces deux lignes doivent être échangées pour que le code fonctionne correctement.
Il y avait aussi quelques problèmes 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 suspects
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 que vous transmettez avec le mot de passe à une fonction de hachage. Il est principalement utilisé pour protéger le programme contre les attaques de dictionnaire et les attaques de table arc-en-ciel, ainsi que pour masquer des mots de passe identiques. Plus ici:
Salt (cryptographie) .
Dans cette fonction, la chaîne transmise est remplacée juste après la saisie. Ignorer la valeur transmise à la fonction est une vulnérabilité potentielle.
Avertissement 2Cette fonction a déclenché 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); }
C'est un extrait assez curieux. La fonction est appelée avec seulement deux variables comme arguments,
offsetWidth et
offsetHeight , et 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 fonction n'utilise pas la valeur passée comme paramètre de
raccourci . L'interface de la fonction est peut-être devenue obsolète, ou cet avertissement n'est qu'un faux positif.
Encore quelques défauts de ce type:
- 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, 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 les mêmes implémentations. Ce dernier était probablement censé obtenir l'index du dernier élément plutôt que d'obtenir l'élément à l'index 0.
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 des corps étrangement identiques. Je suppose que l'un d'eux était censé fonctionner avec une autre couleur au lieu de
color53 .
Dé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, le programmeur a réussi à écrire un morceau de code très suspect. La méthode
setPopupPosition de l'objet
descriptionPopup est d'abord appelée, puis l'objet est vérifié pour
null . Le premier appel à
setPopupPosition est probablement redondant et potentiellement dangereux. Je suppose que cela résulte d'une mauvaise refactorisation.
Avertissement 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) {
Ce cas est similaire au précédent. Au moment où l'objet
tableModel est vérifié pour
null , il a déjà été consulté plusieurs fois.
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)
Probablement 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 s'est déjà vu attribuer la valeur
true , mais elle lui est encore affectée ultérieurement. Ce doit être une erreur, et l'auteur a probablement voulu dire la valeur
false .
Un exemple similaire à partir d'un fichier différent:
- V6026 Cette valeur est déjà affectée à la variable 'sortAscending'. CubaTreeTableWidget.java (444)
Étranges valeurs de retour
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 à
retourner à la toute première itération d'une boucle
for . Soit cette ligne est incorrecte, soit la boucle doit être réécrite en tant qu'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; }
Cette fonction renvoie
true dans chaque cas, tandis que la dernière ligne appelle évidemment
false . Cela ressemble à une erreur.
Voici une liste complète d'autres fonctions suspectes similaires:
- 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 . Il manque probablement du code pour modifier cette valeur dans l'une des conditions.
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 récursive sans condition d'arrêt. Ce fichier contient de nombreuses fonctions marquées du mot-clé
native et contenant du code commenté. Les développeurs sont probablement en train de réécrire ce fichier et remarqueront bientôt la fonction
isFocused .
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 n'a pas de cas pour la valeur
ADD . C'est la seule valeur qui n'est pas vérifiée, donc les développeurs devraient jeter un œil à ce code.
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
source variable est déclarée mais non utilisée. Peut-être que les auteurs ont oublié d'ajouter la
source à la
mise en
page , tout comme cela s'est produit avec une autre variable de ce type,
comp .
Autres 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 comparaison de classes par nom. Il est incorrect de comparer les classes par nom car, selon la spécification, les noms des classes JVM doivent être uniques uniquement dans un package. Une telle comparaison donne des résultats incorrects et conduit à exécuter le mauvais code.
Commentaire des développeurs de la plateforme CUBA
Tout grand projet contient sûrement des bogues. Sachant cela, nous sommes ravis lorsque l'équipe PVS-Studio a proposé de vérifier notre projet. Le référentiel CUBA contient des fourches de certaines des bibliothèques OSS tierces sous licence Apache 2, et il semble que nous devrions accorder plus d'attention à ce code car l'analyseur a trouvé un certain nombre de problèmes dans ces sources. Nous utilisons actuellement SpotBugs comme analyseur principal et il ne parvient pas à remarquer certains des gros bugs signalés par PVS-Studio. Il semble que nous devrions écrire nous-mêmes quelques diagnostics supplémentaires. Un grand merci à l'équipe PVS-Studio pour le travail.Les développeurs nous ont également dit que les avertissements V6013 et V6054 étaient de faux positifs; c'était leur décision consciente d'écrire ce code comme ils l'ont fait. L'analyseur est conçu pour détecter les fragments de code suspects, et la probabilité de trouver de véritables bogues varie selon les différents diagnostics. Ces avertissements, cependant, peuvent être facilement gérés à l'aide du mécanisme spécial de
suppression des avertissements de masse sans avoir à modifier les fichiers source.
De plus, l'équipe PVS-Studio ne peut pas ignorer la phrase "il semble que nous devrions écrire nous-mêmes quelques diagnostics supplémentaires" et nous passer de cette image :)
Conclusion
PVS-Studio peut être un complément parfait aux outils de contrôle qualité existants utilisés dans votre processus de développement. Cela est particulièrement vrai pour les entreprises comptant des dizaines, des centaines ou des milliers de développeurs. PVS-Studio est conçu non seulement pour détecter les bogues mais aussi pour vous aider à les corriger, et ce que je veux dire par là n'est pas l'édition automatique de code mais des moyens fiables de contrôle de la qualité du code. Dans une grande entreprise, il est impossible pour chaque développeur de vérifier leurs parties respectives du code avec des outils différents, donc une meilleure solution pour ces entreprises serait d'adopter des outils comme PVS-Studio, qui fournissent un contrôle de la qualité du code à chaque étape du développement, pas uniquement du côté programmeur.