Top 10 des bugs trouvés dans les projets Java en 2019


2019 touche à sa fin et l'équipe de PVS-Studio revient sur les réalisations de cette année. Au début de 2019, nous avons amélioré les capacités de diagnostic de notre analyseur en ajoutant la prise en charge Java, ce qui nous a également permis de vérifier et d'examiner les projets Java. Nous avons trouvé beaucoup de bogues au cours de cette année, et voici notre Top 10 des bogues trouvés dans les projets Java.


Non. 10: octet signé


Source: Analyse du framework RPC Apache Dubbo par l'analyseur de code statique PVS-Studio

V6007 L' expression 'endKey [i] <0xff' est toujours vraie. 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); } 

De nombreux programmeurs pensent que le type d' octet n'est pas signé. C'est en effet le cas dans de nombreux langages de programmation. Par exemple, cela est vrai pour C #. Mais ce n'est pas le cas en Java.

Dans la condition endKey [i] <0xff , une variable de type octet est comparée au nombre 255 (0xff) représenté sous forme hexadécimale. Le développeur a probablement oublié que la plage du type d' octet Java est [-128, 127]. Cette condition sera toujours vraie et la boucle for ne traitera toujours que le dernier élément du tableau endKey .

Non. 9: Deux en un


Source: PVS-Studio pour Java prend la route. Le prochain arrêt est Elasticsearch

V6007 L' expression '(int) x <0' est toujours fausse. BCrypt.java (429)

V6025 L' index '(int) x' est peut-être hors 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]; } 

Remise! Une méthode - deux bugs! Le premier concerne le type char : puisqu'il n'est pas signé en Java, la condition (int) x <0 sera toujours fausse. Le deuxième bogue est l'indexation ordinaire au-delà des limites du tableau index_64 lorsque (int) x == index_64.length . Cela se produit en raison de la condition (int) x> index_64.length . Le bogue peut être corrigé en changeant l'opérateur '>' en '> =': (int) x> = index_64.length .

Non. 8: Une solution et ses implications


Source: Analyse du code de la plateforme CUBA avec PVS-Studio

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

L'auteur de la méthode findNextMenuItem voulait se débarrasser de la valeur -1 renvoyée par la méthode indexOf lorsque la liste menuItemWidgets ne contient pas currentItem . Pour ce faire, le programmeur ajoute 1 au résultat d' indexOf (la variable menuItemFlatIndex ) et écrit la valeur résultante dans la variable previousMenuItemFlatIndex , qui est ensuite utilisée dans la méthode. L'ajout de -1 s'avère être une mauvaise solution car il conduit à plusieurs erreurs à la fois:

  • L'instruction return null ne sera jamais exécutée car l'expression précédenteMenuItemFlatIndex> = 0 sera toujours vraie; par conséquent, la méthode findNextMenuItem retournera toujours à partir de l'instruction if ;
  • une IndexOutOfBoundsException sera levée lorsque la liste menuItemWidgets est devenue vide puisque le programme tentera d'accéder au premier élément de la liste vide;
  • une IndexOutOfBoundsException sera levée lorsque l'argument currentItem est devenu le dernier élément de la liste menuItemWidget .

Non. 7: Créer un fichier à partir de rien


Source: Huawei Cloud: c'est nuageux dans PVS-Studio aujourd'hui

V6008 Déréférence nulle potentielle 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; } } .... } 

Lors de l'écriture de la méthode putToCache , le programmeur a fait une faute de frappe dans la condition dataTmpFile == null || ! dataTmpFile.exists () avant de créer un nouveau fichier dataTmpFile.createNewFile () : ils ont écrit l'opérateur '==' au lieu de '! ='. Cette faute de frappe entraînera la levée d'une exception NullPointerException lors de l'appel de la méthode createNewFile . Voici à quoi ressemble la condition avec la faute de frappe corrigée:

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

Vous pourriez penser: «Eh bien, d'accord, nous pouvons nous détendre maintenant.» Pas encore!

Maintenant qu'un bug a été corrigé, un autre est apparu. Une exception NullPointerException peut être levée lors de l'appel de la méthode dataTmpFile.exists () . Pour résoudre ce problème, nous devons remplacer le '||' opérateur avec '&&'. Il s'agit de la version finale de la condition, après toutes les corrections:

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

Non. 6: Une erreur logique très étrange


Source: PVS-Studio pour Java

V6007 [CWE-570] L'expression "" 0 ".equals (texte)" est toujours fausse. 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'; } 

La chose intéressante à propos de cette méthode est qu'elle contient une erreur logique évidente. Si la méthode satisfaiteBy ne retourne pas après la première instruction if , cela signifie que la chaîne de texte comporte au moins deux caractères. Cela signifie également que la toute première vérification "0" .equals (texte) dans la prochaine instruction if n'a pas de sens. Ce que le programmeur voulait réellement dire par là reste un mystère.

Non. 5: Quelle torsion!


Source: PVS-Studio visite Apache Hive

V6034 Le décalage de la valeur de 'bitShiftsInWord - 1' peut ne pas correspondre à la taille du type: '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)); // <= .... } 

Avec les arguments d'entrée wordShifts = 3 et bitShiftsInWord = 0 , la variable roundCarryMask , qui stocke le résultat du décalage au niveau du bit (1 << (bitShiftsInWord - 1)) , deviendra un nombre négatif. Le développeur ne s'y attendait probablement pas.

Non. 4: Pouvons-nous voir les exceptions s'il vous plaît?


Source: PVS-Studio visite Apache Hive

V6051 L'utilisation de l'instruction 'return' dans le bloc 'finalement' peut entraîner la perte d'exceptions non gérées. 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(); } } } 

La déclaration de la méthode getMPartitionColumnStatistics est trompeuse car elle dit qu'elle pourrait lever une exception. En réalité, quelle que soit l'exception générée dans le bloc try , la variable validée sera toujours fausse , donc l'instruction return dans le bloc finally renverra une valeur, tandis que toutes les exceptions levées seront perdues et ne pourront pas être traitées en dehors de la méthode. Ainsi, aucune des exceptions levées dans cette méthode ne pourra la quitter.

Non. 3: Hocus-pocus, ou essayer d'obtenir un nouveau masque


Source: PVS-Studio visite Apache Hive

V6034 Le décalage de la valeur de 'j' peut être incompatible avec la taille du type: '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); // <= } .... } .... } 

Ce bug, aussi, a à voir avec un décalage au niveau du bit, mais pas seulement. La variable j est utilisée comme compteur sur la plage [0 ... 63] dans la boucle for interne. Ce compteur participe à un décalage au niveau du bit 1 << j . Tout semble correct, mais voici où le littéral entier '1' de type int (une valeur de 32 bits) entre en jeu. À cause de cela, le décalage au niveau du bit commencera à renvoyer les valeurs précédemment renvoyées lorsque la valeur de la variable j a dépassé 31. Si ce comportement n'est pas ce que le programmeur voulait, la valeur 1 doit être représentée aussi longtemps : 1L << j ou (long) 1 << j .

Non. 2: Ordre d'initialisation


Source: Huawei Cloud: c'est nuageux dans PVS-Studio aujourd'hui

Le cycle d'initialisation de la classe V6050 est présent. L'initialisation de 'INSTANCE' apparaît avant l'initialisation 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); // <= } } } 

L'ordre dans lequel les champs sont déclarés dans une classe fait une différence car ils sont initialisés dans le même ordre qu'ils sont déclarés. Oublier ce fait conduit à des bugs insaisissables comme celui ci-dessus.

L'analyseur souligne que le champ statique LOG est déréférencé dans le constructeur au moment où il est initialisé à la valeur null , ce qui conduit à lever une série d'exceptions NullPointerException -> ExceptionInInitializerError .

Mais pourquoi le champ statique LOG a-t-il la valeur null au moment d'appeler le constructeur?

L' ExceptionInInitializerError est l'indice. Le constructeur initialise le champ statique INSTANCE , qui est déclaré avant le champ LOG . C'est pourquoi le champ LOG n'est pas encore initialisé lorsque le constructeur est appelé. Pour que ce code fonctionne correctement, le champ LOG doit être initialisé avant l'appel.

Premièrement: programmation orientée copier-coller


Source: Apache Hadoop Code Qualité: Production VS Test

V6072 Deux fragments de code similaires ont été trouvés. Il s'agit peut-être d'une faute de frappe et la variable «localFiles» devrait être utilisée à la place 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()]))); } .... } 

La première place sur notre liste Top 10 est attribuée au copier-coller, ou plutôt à un bug qui découle de l'utilisation imprudente de cette technique. La seconde instruction if ressemble beaucoup à une copie de la première, avec certaines des variables modifiées:

  • localArchives a été remplacé par localFiles ;
  • MRJobConfig.CACHE_LOCALARCHIVES a été remplacé par MRJobConfig.CACHE_LOCALFILES .

Mais le programmeur a réussi à faire une erreur même dans cette opération simple: l'instruction if dans la ligne indiquée par l'analyseur utilise toujours la variable localArchives au lieu de la variable localFiles apparemment voulue.

Conclusion


La correction des bogues détectés aux stades de développement ultérieurs ou après la publication nécessite beaucoup de ressources. L'analyseur statique PVS-Studio vous permet de détecter les bogues au stade du codage, ce qui le rend beaucoup plus facile et moins cher. De nombreuses entreprises ont déjà simplifié la vie de leurs développeurs en commençant à utiliser régulièrement l'analyseur. Si vous voulez vraiment apprécier votre travail, essayez PVS-Studio .

Nous n'allons pas nous arrêter à cela et prévoyons continuer à améliorer et à améliorer notre outil. Restez à l'écoute pour de nouveaux diagnostics et articles avec des bogues encore plus intéressants l'année prochaine.

Je te vois comme des aventures! Tout d'abord, vous avez vaincu les 10 principaux bogues du projet C # en 2019 et maintenant vous avez également géré Java! Bienvenue au niveau suivant - l'article sur les meilleures erreurs dans les projets C ++ en 2019 .

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


All Articles