PVS-Studio pour Java

PVS-Studio pour Java

Dans la septième version de l'analyseur statique PVS-Studio, nous avons ajouté la prise en charge du langage Java. Il est temps de parler un peu de la façon dont nous avons commencé à prendre en charge le langage Java, de ce que nous avons fait et de nos projets futurs. Et, bien sûr, l'article montrera les premiers tests de l'analyseur sur des projets ouverts.

PVS-Studio


Pour les développeurs Java qui n'ont jamais entendu parler de l'outil PVS-Studio auparavant, je vais en donner une brève description.

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 Windows, Linux et macOS.

PVS-Studio effectue une analyse de code statique et génère un rapport qui aide le programmeur à trouver et à corriger les défauts. Pour ceux qui s'intéressent exactement à la façon dont PVS-Studio recherche les erreurs, je vous suggère de lire l'article " Technologies utilisées dans l'analyseur de code PVS-Studio pour trouver les erreurs et les vulnérabilités potentielles ".

Commencer


Je pourrais proposer une histoire intelligente, car nous réfléchissons depuis deux ans à la prochaine langue à prendre en charge dans PVS-Studio. Le fait que Java soit un choix raisonnable basé sur la grande popularité de ce langage et ainsi de suite.

Cependant, comme cela se passe dans la vie, tout a été décidé non pas par une analyse approfondie, mais par l'expérience :). Oui, nous pensions dans quelle direction l'analyseur PVS-Studio devait être développé. Des langages de programmation tels que: Java, PHP, Python, JavaScript, IBM RPG ont été considérés. Et nous étions enclins au langage Java, mais le choix final n'a pas encore été fait. Ceux dont les yeux sont bloqués sur un RPG IBM inconnu, je me réfère à cette note ici , à partir de laquelle tout deviendra clair.

À la fin de 2017, le collègue Egor Bredikhin a examiné les bibliothèques prêtes à l'emploi pour l'analyse du code (en d'autres termes, les analyseurs) disponibles pour de nouvelles directions qui nous intéressent. Et je suis tombé sur plusieurs projets pour analyser le code Java. Basé sur Spoon , il a rapidement réussi à créer un prototype d'analyseur avec quelques diagnostics. De plus, il est devenu clair que nous pouvons utiliser certains mécanismes de l'analyseur C ++ à l'aide de SWIG dans l'analyseur Java. Nous avons regardé ce qui s'est passé et réalisé que notre prochain analyseur serait pour Java.

Merci à Egor pour son engagement et son travail actif sur l'analyseur Java. Comment le développement s'est poursuivi, il a décrit dans l'article " Développement d'un nouvel analyseur statique: PVS-Studio Java ".

Concurrents?


Il existe de nombreux analyseurs de code statique gratuits et commerciaux pour Java dans le monde. Cela n'a pas de sens de les énumérer tous dans l'article, et je laisse juste un lien vers " Liste d'outils pour l'analyse de code statique " (voir la section Java et multilingue).

Cependant, je sais que tout d'abord, nous serons interrogés sur IntelliJ IDEA, FindBugs et SonarQube (SonarJava).

IntelliJ IDEA

IntelliJ IDEA intègre un analyseur de code statique très puissant. De plus, l'analyseur se développe et ses auteurs suivent de près nos activités. Avec IntelliJ IDEA, nous serons les plus difficiles. Nous ne pourrons pas dépasser IntelliJ IDEA en termes de capacités de diagnostic, du moins pour l'instant. Par conséquent, nous essaierons de nous concentrer sur nos autres avantages.

L'analyse statique dans IntelliJ IDEA est, tout d'abord, l'une des puces de l'environnement de développement, qui lui impose certaines restrictions. Nous sommes libres de ce que nous pouvons faire avec notre analyseur. Par exemple, nous pouvons rapidement adapter l'analyseur aux besoins spécifiques du client. Un soutien rapide et approfondi est notre avantage concurrentiel. Nos clients communiquent directement avec les programmeurs développant une partie particulière de PVS-Studio.

PVS-Studio a de nombreuses possibilités pour l'intégrer dans le cycle de développement de grands projets anciens. Il s'agit de l' intégration avec SonarQube . Il s'agit d'une suppression massive des messages de l' analyseur, qui vous permet de commencer immédiatement à utiliser l'analyseur dans un grand projet pour suivre les erreurs uniquement dans le code nouveau ou modifié. PVS-Studio est intégré dans le processus d'intégration continue. Je pense que ces fonctionnalités et d'autres aideront notre analyseur à trouver une place sous le soleil dans le monde Java.

Findbugs

Le projet FindBugs est abandonné . Mais il ne faut pas oublier que c'est peut-être l'analyseur statique gratuit le plus connu du code Java.

Le successeur de FindBugs est le projet SpotBugs . Cependant, il est moins populaire et ce qui lui arrivera n'est pas encore tout à fait clair.

En général, nous pensons que bien que FindBugs ait été et reste extrêmement populaire, et également un analyseur gratuit, nous ne devrions pas y penser. Ce projet appartient simplement et tranquillement au passé.

PS Soit dit en passant, maintenant PVS-Studio peut également être utilisé gratuitement lorsque vous travaillez avec des projets ouverts.

SonarQube (SonarJava)

Nous pensons ne pas concurrencer SonarQube, mais le compléter. PVS-Studio s'intègre à SonarQube, ce qui permet aux développeurs de trouver plus d'erreurs et de vulnérabilités potentielles dans leurs projets. Comment intégrer l'outil PVS-Studio et d'autres analyseurs dans SonarQube, nous discutons régulièrement lors de master classes que nous organisons lors de différentes conférences ( exemple ).

Comment démarrer PVS-Studio pour Java


Nous avons mis à la disposition des utilisateurs les moyens les plus courants d'intégrer l'analyseur dans le système d'assemblage:

  • Plugin pour Maven;
  • Plugin pour Gradle;
  • Plugin pour IntelliJ IDEA

Au stade des tests, nous avons rencontré de nombreux utilisateurs qui ont des systèmes d'assemblage auto-écrits, en particulier dans le développement mobile. Ils ont aimé la possibilité d'exécuter l'analyseur directement, répertoriant les sources et le chemin de classe.

Vous pouvez trouver des informations détaillées sur toutes les méthodes de démarrage de l'analyseur sur la page de documentation " Comment démarrer PVS-Studio Java ".

Nous ne pouvions pas ignorer la plate-forme de contrôle de la qualité du code SonarQube , si populaire parmi les développeurs Java, nous avons donc ajouté la prise en charge du langage Java à notre plug-in SonarQube .

Plans supplémentaires


Nous avons de nombreuses idées qui nécessitent une étude plus approfondie, mais certains plans spécifiques à l'un de nos analyseurs ressemblent à ceci:

  • Création de nouveaux diagnostics et amélioration de ceux existants;
  • Développement de l'analyse des flux de données;
  • Amélioration de la fiabilité et de la convivialité.

Nous pouvons trouver le temps d'adapter le plugin IntelliJ IDEA pour CLion. Salut C ++ aux développeurs qui lisent l'analyseur Java :-)

Exemples d'erreurs trouvées dans les projets open source


Je ne serai pas moi si je ne montre aucune erreur trouvée en utilisant le nouvel analyseur dans l'article. Nous pourrions prendre un gros projet Java open source et écrire un article classique avec une analyse des erreurs, comme nous le faisons habituellement .

Cependant, je prévois immédiatement les questions de savoir si nous pouvons trouver quelque chose dans des projets tels que IntelliJ IDEA, FindBugs et ainsi de suite. Par conséquent, je n'ai tout simplement pas d'issue et je vais commencer précisément avec ces projets. J'ai donc décidé de vérifier rapidement et d'écrire quelques exemples intéressants d'erreurs des projets suivants:

  • IntelliJ IDEA Community Edition . Je pense qu'il n'est pas nécessaire d'expliquer pourquoi ce projet a été choisi :).
  • SpotBugs Comme je l'ai écrit plus tôt, le projet FindBugs ne se développe pas. Jetez donc un œil au projet SpotBugs, qui est le successeur de FindBugs. SpotBugs est un analyseur de code Java statique classique.
  • Certains des projets de SonarSource, qui développe des logiciels pour le contrôle continu de la qualité du code. Jetez un œil aux projets SonarQube et SonarJava .

Écrire sur les bugs de ces projets est une tâche difficile. Le fait est que ces projets sont de très haute qualité. En fait, ce n'est pas surprenant. Comme le montrent nos observations, le code des analyseurs statiques est toujours bien testé et vérifié à l'aide d'autres outils.

Malgré tout cela, je dois commencer par ces mêmes projets. Je n'aurai pas une seconde chance d'écrire quelque chose à leur sujet. Je suis sûr qu'après la sortie de PVS-Studio pour Java, les développeurs de ces projets mettront PVS-Studio en service et commenceront à l'utiliser pour des contrôles réguliers ou au moins périodiques de leur code. Par exemple, je sais que Tagir Valeyev ( lany ), l'un des développeurs JetBrains engagé dans l'analyseur de code statique IntelliJ IDEA, jouait déjà avec la version bêta de PVS-Studio au moment où j'écris l'article. Il nous a déjà écrit environ 15 lettres avec des rapports de bugs et des recommandations. Merci Tagir!

Heureusement, je n'ai pas besoin de trouver autant d'erreurs que possible dans un projet particulier. Maintenant, ma tâche est de montrer que l'analyseur PVS-Studio pour Java n'est pas apparu en vain et sera en mesure de reconstituer la gamme d'autres outils conçus pour améliorer la qualité du code. Je viens de parcourir les rapports de l'analyseur et d'écrire quelques erreurs qui me semblent intéressantes. Dans la mesure du possible, j'ai essayé d'écrire des erreurs de différents types. Voyons ce qui s'est passé.

IntelliJ IDEA Integer Division


private static boolean checkSentenceCapitalization(@NotNull String value) { List<String> words = StringUtil.split(value, " "); .... int capitalized = 1; .... return capitalized / words.size() < 0.2; // allow reasonable amount of // capitalized words } 

Avertissement PVS-Studio: V6011 [CWE-682] Le littéral '0,2' du type 'double' est comparé à une valeur du type 'int'. TitleCapitalizationInspection.java 169

Comme prévu, une fonction doit retourner true si moins de 20% des mots commencent par une majuscule. En fait, la vérification ne fonctionne pas, car une division entière se produit. En raison de la division, seules deux valeurs peuvent être obtenues: 0 ou 1.

La fonction ne renverra une valeur fausse que si tous les mots commencent par une majuscule. Dans tous les autres cas, la division produira 0 et la fonction renverra la vraie valeur.

Cycle suspect d'IntelliJ IDEA


 public int findPreviousIndex(int current) { int count = myPainter.getErrorStripeCount(); int foundIndex = -1; int foundLayer = 0; if (0 <= current && current < count) { current--; for (int index = count - 1; index >= 0; index++) { // <= int layer = getLayer(index); if (layer > foundLayer) { foundIndex = index; foundLayer = layer; } } .... } 

Avertissement PVS-Studio: V6007 [CWE-571] L'expression 'index> = 0' est toujours vraie. Updater.java 184

Tout d'abord, regardez la condition (0 <= courant && courant <compte) . Il n'est exécuté que si la valeur de la variable de comptage est supérieure à 0.

Regardez maintenant la boucle:

 for (int index = count - 1; index >= 0; index++) 

L' index variable est initialisé par l'expression count - 1 . Puisque la variable de comptage est supérieure à 0, la valeur initiale de la variable d' index est toujours supérieure ou égale à 0. Il s'avère que la boucle sera exécutée jusqu'à ce que la variable d' index déborde.

Très probablement, ce n'est qu'une faute de frappe et l'incrément ne doit pas être exécuté, mais le décrément de la variable:

 for (int index = count - 1; index >= 0; index--) 

IntelliJ IDEA, copier-coller


 @NonNls public static final String BEFORE_STR_OLD = "before:"; @NonNls public static final String AFTER_STR_OLD = "after:"; private static boolean isBeforeOrAfterKeyword(String str, boolean trimKeyword) { return (trimKeyword ? LoadingOrder.BEFORE_STR.trim() : LoadingOrder.BEFORE_STR).equalsIgnoreCase(str) || (trimKeyword ? LoadingOrder.AFTER_STR.trim() : LoadingOrder.AFTER_STR).equalsIgnoreCase(str) || LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str) || // <= LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str); // <= } 

Avertissement PVS-Studio: V6001 [CWE-570] Il existe des sous-expressions identiques 'LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase (str)' à gauche et à droite de '||' opérateur. Vérifiez les lignes: 127, 128. ExtensionOrderConverter.java 127

Bon vieil effet de la dernière ligne . Le programmeur se précipita et, ayant multiplié une ligne de code, oublia de le réparer. Par conséquent, deux fois la chaîne str est comparée à BEFORE_STR_OLD . Très probablement, l'une des comparaisons devrait être avec AFTER_STR_OLD .

IntelliJ IDEA typo


 public synchronized boolean isIdentifier(@NotNull String name, final Project project) { if (!StringUtil.startsWithChar(name,'\'') && !StringUtil.startsWithChar(name,'\"')) { name = "\"" + name; } if (!StringUtil.endsWithChar(name,'"') && !StringUtil.endsWithChar(name,'\"')) { name += "\""; } .... } 

Avertissement PVS-Studio: V6001 [CWE-571] Il existe des sous-expressions identiques '! StringUtil.endsWithChar (nom,' "')' à gauche et à droite de l'opérateur '&&'. JsonNamesValidator.java 27

Ce morceau de code vérifie que le nom est entre guillemets simples ou doubles. Si ce n'est pas le cas, des guillemets doubles sont ajoutés automatiquement.

En raison d'une faute de frappe, la fin du nom n'est vérifiée que pour les guillemets doubles. Par conséquent, le nom pris entre guillemets simples ne sera pas traité correctement.

Prénom

 'Abcd' 

en raison de l'ajout de guillemets supplémentaires, il se transformera en:

 'Abcd'" 

IntelliJ IDEA, protection incorrecte contre les débordements de baies


 static Context parse(....) { .... for (int i = offset; i < endOffset; i++) { char c = text.charAt(i); if (c == '<' && i < endOffset && text.charAt(i + 1) == '/' && startTag != null && CharArrayUtil.regionMatches(text, i + 2, endOffset, startTag)) { endTagStartOffset = i; break; } } .... } 

Avertissement PVS-Studio: V6007 [CWE-571] L'expression 'i <endOffset' est toujours vraie. EnterAfterJavadocTagHandler.java 183

La sous-expression i <endOffset dans la condition de l' instruction if n'a pas de sens. La variable i est toujours inférieure à endOffset , comme suit à partir de la condition d'exécution de la boucle.

Très probablement, le programmeur voulait se protéger contre les sorties de ligne lors de l'appel de fonctions:

  • text.charAt (i + 1)
  • CharArrayUtil.regionMatches (texte, i + 2, endOffset, startTag)

Dans ce cas, la sous-expression pour vérifier l'index doit être comme ceci: i <endOffset - 2 .

IntelliJ IDEA Repeat Check


 public static String generateWarningMessage(....) { .... if (buffer.length() > 0) { if (buffer.length() > 0) { buffer.append(" ").append( IdeBundle.message("prompt.delete.and")).append(" "); } } .... } 

Avertissement PVS-Studio: V6007 [CWE-571] L'expression 'buffer.length ()> 0' est toujours vraie. DeleteUtil.java 62

Cela peut être soit un code redondant inoffensif, soit une grave erreur.

Si un chèque en double est apparu par hasard, par exemple, lors d'une refactorisation, il n'y a rien de mal à cela. Le deuxième chèque peut simplement être supprimé.

Mais un autre scénario est possible. La deuxième vérification doit être complètement différente et le code ne se comporte pas comme prévu. Alors c'est une vraie erreur.

Remarque Soit dit en passant, il existe de nombreux contrôles redondants différents. De plus, on voit souvent que ce n'est pas une erreur. Cependant, les messages de l'analyseur ne peuvent pas non plus être appelés faux positifs. Pour clarifier, voici un exemple, également tiré d'IntelliJ IDEA:

 private static boolean isMultiline(PsiElement element) { String text = element.getText(); return text.contains("\n") || text.contains("\r") || text.contains("\r\n"); } 

L'analyseur indique que la fonction text.contains ("\ r \ n") renvoie toujours false. En effet, si les symboles "\ n" et "\ r" ne sont pas trouvés, il ne sert à rien de rechercher "\ r \ n". Ce n'est pas une erreur, et le code est mauvais uniquement parce qu'il fonctionne un peu plus lentement, effectuant une recherche vide de sens pour une sous-chaîne.

Comment gérer un tel code, dans chaque cas, c'est aux programmeurs de décider. Lors de la rédaction d'articles, en règle générale, je ne fais tout simplement pas attention à ce code.

IntelliJ IDEA, quelque chose ne va pas


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

Avertissement PVS-Studio: V6007 [CWE-570] L'expression "" 0 ".equals (texte)" est toujours fausse. ConvertIntegerToDecimalPredicate.java 46

Ce code contient définitivement une erreur logique. Mais j'ai du mal à dire ce que le programmeur voulait vérifier et comment corriger le défaut. Par conséquent, je ne signalerai ici qu'un chèque sans signification.

Au début, il est vérifié que la chaîne doit contenir au moins deux caractères. Si ce n'est pas le cas, la fonction renvoie false .

Ce qui suit est une vérification "0" .equals (texte) . Cela n'a pas de sens, car une chaîne ne peut contenir qu'un seul caractère.

En général, quelque chose ne va pas ici et le code doit être corrigé.

SpotBugs (successeur de FindBugs), erreur de limite d'itération


 public static String getXMLType(@WillNotClose InputStream in) throws IOException { .... String s; int count = 0; while (count < 4) { s = r.readLine(); if (s == null) { break; } Matcher m = tag.matcher(s); if (m.find()) { return m.group(1); } } throw new IOException("Didn't find xml tag"); .... } 

Avertissement PVS-Studio: V6007 [CWE-571] L'expression 'count <4' est toujours vraie. Util.java 394

Comme prévu, la recherche de la balise xml ne doit être effectuée que dans les quatre premières lignes du fichier. Mais du fait qu'ils ont oublié d'incrémenter le nombre de variables, le fichier entier sera lu.

Premièrement, cela peut s'avérer être une opération très lente, et deuxièmement, quelque part au milieu du fichier, quelque chose peut être trouvé qui sera interprété comme une balise xml, mais ce ne sera pas le cas.

SpotBugs (successeur de FindBugs), écrasement des valeurs


 private void reportBug() { int priority = LOW_PRIORITY; String pattern = "NS_NON_SHORT_CIRCUIT"; if (sawDangerOld) { if (sawNullTestVeryOld) { priority = HIGH_PRIORITY; // <= } if (sawMethodCallOld || sawNumericTestVeryOld && sawArrayDangerOld) { priority = HIGH_PRIORITY; // <= pattern = "NS_DANGEROUS_NON_SHORT_CIRCUIT"; } else { priority = NORMAL_PRIORITY; // <= } } bugAccumulator.accumulateBug( new BugInstance(this, pattern, priority).addClassAndMethod(this), this); } 

Avertissement PVS-Studio: V6021 [CWE-563] La valeur est affectée à la variable 'priorité' mais n'est pas utilisée. FindNonShortCircuit.java 197

La valeur de la variable prioritaire est définie en fonction de la valeur de la variable sawNullTestVeryOld . Cependant, cela ne joue aucun rôle. De plus, la variable de priorité se verra attribuer une valeur différente dans tous les cas. Une erreur évidente dans la logique de la fonction.

SonarQube, copier-coller


 public class RuleDto { .... private final RuleDefinitionDto definition; private final RuleMetadataDto metadata; .... private void setUpdatedAtFromDefinition(@Nullable Long updatedAt) { if (updatedAt != null && updatedAt > definition.getUpdatedAt()) { setUpdatedAt(updatedAt); } } private void setUpdatedAtFromMetadata(@Nullable Long updatedAt) { if (updatedAt != null && updatedAt > definition.getUpdatedAt()) { setUpdatedAt(updatedAt); } } .... } 

PVS-Studio: V6032 Il est étrange que le corps de la méthode 'setUpdatedAtFromDefinition' soit entièrement équivalent au corps d'une autre méthode 'setUpdatedAtFromMetadata'. Vérifiez les lignes: 396, 405. RuleDto.java 396

La méthode setUpdatedAtFromMetadata utilise le champ de définition . Très probablement, le champ de métadonnées doit être utilisé. Ceci est très similaire aux conséquences de l'échec du copier-coller.

SonarJava, doublons sur l'initialisation de la carte


 private final Map<JavaPunctuator, Tree.Kind> assignmentOperators = Maps.newEnumMap(JavaPunctuator.class); public KindMaps() { .... assignmentOperators.put(JavaPunctuator.PLUSEQU, Tree.Kind.PLUS_ASSIGNMENT); .... assignmentOperators.put(JavaPunctuator.PLUSEQU, Tree.Kind.PLUS_ASSIGNMENT); .... } 

Avertissement PVS-Studio: V6033 [CWE-462] Un élément avec la même clé 'JavaPunctuator.PLUSEQU' a déjà été ajouté. Vérifiez les lignes: 104, 100. KindMaps.java 104

La même paire clé-valeur est placée deux fois dans la carte. Très probablement, cela s'est avéré inattentif, et en fait il n'y a pas de véritable erreur. Cependant, dans tous les cas, ce code doit être vérifié, car vous avez peut-être oublié d'ajouter une autre paire.

Conclusion


Mais quelle conclusion peut-il y avoir?! J'invite tout le monde, sans délai, à télécharger PVS-Studio et à essayer de tester vos projets de travail en Java! Téléchargez PVS-Studio .

Merci à tous pour votre attention. J'espère que bientôt nous ravirons les lecteurs avec une série d'articles consacrés à la vérification de divers projets Java ouverts.



Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Andrey Karpov. PVS-Studio pour Java .

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


All Articles