Entwicklung eines neuen statischen Analysators: PVS-Studio Java

Bild 3

Der statische Analysator PVS-Studio ist in der Welt von C, C ++ und C # als Werkzeug zum Erkennen von Fehlern und potenziellen Schwachstellen bekannt. Wir haben jedoch nur wenige Kunden aus dem Finanzsektor, da sich herausstellte, dass Java und IBM RPG (!) Dort jetzt gefragt sind. Wir wollten jedoch immer näher an die Enterprise-Welt heranrücken. Deshalb haben wir uns nach einiger Überlegung entschlossen, einen Java-Analysator zu erstellen.

Einführung


Natürlich gab es Bedenken. Es ist einfach, den Analysatormarkt im IBM RPG zu erobern. Ich bin mir überhaupt nicht sicher, ob es vernünftige Werkzeuge für die statische Analyse dieser Sprache gibt. In der Java-Welt sieht das ganz anders aus. Es gibt bereits eine Reihe von Tools für die statische Analyse. Um weiterzukommen, müssen Sie einen wirklich leistungsstarken und coolen Analysator erstellen.

Trotzdem hat unser Unternehmen Erfahrung mit verschiedenen Tools zur statischen Analyse von Java und wir sind sicher, dass wir viele Dinge besser machen können.

Außerdem hatten wir eine Idee, wie wir die volle Leistung unseres C ++ - Analysators in einem Java-Analysator nutzen können. Aber das Wichtigste zuerst.

Baum


Bild 6


Zunächst musste entschieden werden, wie wir den Syntaxbaum und das semantische Modell erhalten.

Der Syntaxbaum ist das Grundelement, um das der Analysator aufgebaut ist. Bei der Durchführung von Überprüfungen durchläuft der Analysator den Syntaxbaum und untersucht seine einzelnen Knoten. Ohne einen solchen Baum ist eine ernsthafte statische Analyse praktisch nicht möglich. Beispielsweise ist die Suche nach Fehlern mit regulären Ausdrücken nicht vielversprechend .

Es ist erwähnenswert, dass nur ein Syntaxbaum nicht ausreicht. Der Analysator benötigt auch semantische Informationen. Zum Beispiel müssen wir die Typen aller Elemente des Baums kennen, zur Variablendeklaration gehen können usw.

Wir haben verschiedene Optionen untersucht, um einen Syntaxbaum und ein semantisches Modell zu erhalten:


Wir haben die Idee, ANTLR zu verwenden, fast sofort aufgegeben, da dies die Entwicklung des Analysators unnötig erschweren würde (die semantische Analyse müsste selbst implementiert werden). Am Ende beschlossen wir, in der Spoon-Bibliothek Halt zu machen:

  • Es ist nicht nur ein Parser, sondern ein ganzes Ökosystem - es bietet nicht nur einen Analysebaum, sondern auch Möglichkeiten zur semantischen Analyse. So können Sie beispielsweise Informationen zu den Variablentypen abrufen, zur Variablendeklaration gehen, Informationen zur übergeordneten Klasse abrufen usw.
  • Es basiert auf Eclipse JDT und kann Code kompilieren.
  • Es unterstützt die neueste Version von Java und wird ständig aktualisiert.
  • Gute Dokumentation und klare API.

Hier ist ein Beispiel für ein Metamodell, das Spoon bereitstellt und mit dem wir beim Erstellen von Diagnoseregeln arbeiten:

Bild 10


Dieses Metamodell entspricht dem folgenden Code:
class TestClass { void test(int a, int b) { int x = (a + b) * 4; System.out.println(x); } } 

Eines der schönen Dinge an Spoon ist, dass es den Syntaxbaum (Entfernen und Hinzufügen von Knoten) vereinfacht, um die Arbeit zu vereinfachen. Gleichzeitig ist die semantische Äquivalenz des vereinfachten Metamodells des Originals gewährleistet.

Für uns bedeutet dies zum Beispiel, dass wir uns beim Überqueren eines Baumes keine Sorgen mehr machen müssen, zusätzliche Klammern zu überspringen. Außerdem wird jeder Ausdruck in einen Block eingefügt, Importe werden angezeigt und einige andere ähnliche Vereinfachungen vorgenommen.

Zum Beispiel ein Code wie dieser:

 for (int i = ((0)); (i < 10); i++) if (cond) return (((42))); 

wird wie folgt dargestellt:

 for (int i = 0; i < 10; i++) { if (cond) { return 42; } } 

Basierend auf dem Syntaxbaum wird eine sogenannte musterbasierte Analyse durchgeführt. Dies ist eine Suche nach Fehlern im Quellcode eines Programms unter Verwendung bekannter Fehlercodemuster. Im einfachsten Fall sucht der Analysator nach Stellen, die wie ein Fehler im Baum aussehen, gemäß den in der entsprechenden Diagnose beschriebenen Regeln. Die Anzahl solcher Muster ist groß und ihre Komplexität kann stark variieren.

Das einfachste Beispiel für einen Fehler, der durch eine musterbasierte Analyse erkannt wurde, ist der folgende Code aus dem jMonkeyEngine-Projekt:

 if (p.isConnected()) { log.log(Level.FINE, "Connection closed:{0}.", p); } else { log.log(Level.FINE, "Connection closed:{0}.", p); } 

Die then- und else- Blöcke der if-Anweisung sind identisch, höchstwahrscheinlich liegt ein logischer Fehler vor.

Hier ist ein weiteres ähnliches Beispiel aus dem Hive-Projekt:

 if (obj instanceof Number) { // widening conversion return ((Number) obj).doubleValue(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof String) { return Double.valueOf(obj.toString()); } else if (obj instanceof Timestamp) { return new TimestampWritable((Timestamp)obj).getDouble(); } else if (obj instanceof HiveDecimal) { // <= return ((HiveDecimal) obj).doubleValue(); } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).doubleValue(); } 

Dieser Code enthält zwei identische Bedingungen in einer Folge der Form if (....) else if (....) else if (....) . Es lohnt sich, diesen Abschnitt des Codes auf einen logischen Fehler zu überprüfen oder den doppelten Code zu entfernen.

Datenflussanalyse


Neben dem Syntaxbaum und dem semantischen Modell benötigt der Analysator einen Mechanismus zur Analyse des Datenflusses .

Durch die Analyse des Datenflusses können Sie die gültigen Werte von Variablen und Ausdrücken an jedem Punkt im Programm berechnen und dadurch Fehler finden. Wir nennen diese gültigen Werte virtuelle Werte.

Bei der ersten Erwähnung werden virtuelle Werte für Variablen, Klassenfelder, Methodenparameter und andere Dinge erstellt. Wenn es sich um eine Zuweisung handelt, berechnet der Datenflussmechanismus den virtuellen Wert durch Analysieren des Ausdrucks auf der rechten Seite. Andernfalls wird der gesamte gültige Wertebereich für diesen Variablentyp als virtueller Wert verwendet. Zum Beispiel:

 void func(byte x) // x: [-128..127] { int y = 5; // y: [5] ... } 

Jedes Mal, wenn sich der Wert einer Variablen ändert, berechnet die Datenfluss-Engine den virtuellen Wert neu. Zum Beispiel:

 void func() { int x = 5; // x: [5] x += 7; // x: [12] ... } 

Die Datenfluss-Engine verarbeitet auch Steueranweisungen:

 void func(int x) // x: [-2147483648..2147483647] { if (x > 3) { // x: [4..2147483647] if (x < 10) { // x: [4..9] } } else { // x: [-2147483648..3] } ... } 

In diesem Beispiel gibt es beim Eingeben der Funktion keine Informationen über den Wertebereich der Variablen x , daher wird sie entsprechend dem Typ der Variablen festgelegt (von -2147483648 bis 2147483647). Dann legt der erste bedingte Block eine Einschränkung x > 3 fest, und die Bereiche werden kombiniert. Infolgedessen liegt der Wertebereich für x im then- Block zwischen 4 und 2147483647 und im else- Block zwischen -2147483648 und 3. Die zweite Bedingung x <10 wird auf die gleiche Weise verarbeitet.

Darüber hinaus müssen Sie rein symbolische Berechnungen durchführen können. Das einfachste Beispiel:

 void f1(int a, int b, int c) { a = c; b = c; if (a == b) // <= always true .... } 

Hier wird der Variablen a der Wert c zugewiesen, der Variablen b wird auch der Wert c zugewiesen, wonach a und b verglichen werden. Um den Fehler zu finden, denken Sie in diesem Fall einfach an das Holzstück auf der rechten Seite.

Hier ist ein etwas komplexeres Beispiel mit Zeichenberechnungen:

 void f2(int a, int b, int c) { if (a < b) { if (b < c) { if (c < a) // <= always false .... } } } 

In solchen Fällen ist es bereits notwendig, das Ungleichungssystem in symbolischer Form zu lösen.

Der Datenflussmechanismus hilft dem Analysator, Fehler zu finden, die mit der musterbasierten Analyse nur sehr schwer zu finden sind.

Diese Fehler umfassen:

  • Überläufe;
  • Das Array ins Ausland gehen;
  • Zugriff durch Null- oder potenziell Nullreferenz;
  • Bedeutungslose Bedingungen (immer wahr / falsch);
  • Speicher- und Ressourcenlecks;
  • Division durch 0;
  • Und einige andere.

Die Datenflussanalyse ist besonders wichtig bei der Suche nach Schwachstellen. Wenn beispielsweise ein bestimmtes Programm Eingaben vom Benutzer empfängt, wird die Eingabe wahrscheinlich verwendet, um einen Denial-of-Service zu verursachen oder die Kontrolle über das System zu erlangen. Beispiele hierfür sind Fehler, die bei einigen Eingaben zu Pufferüberläufen führen, oder beispielsweise SQL-Injektionen. In beiden Fällen müssen der Datenfluss und mögliche Werte von Variablen überwacht werden, damit der statische Analysator solche Fehler und Schwachstellen erkennen kann.

Ich muss sagen, dass der Mechanismus zur Datenflussanalyse ein komplexer und umfassender Mechanismus ist, und in diesem Artikel habe ich nur die Grundlagen angesprochen.

Schauen wir uns einige Beispiele für Fehler an, die mithilfe des Datenflussmechanismus erkannt werden können.

Hive-Projekt:

 public static boolean equal(byte[] arg1, final int start1, final int len1, byte[] arg2, final int start2, final int len2) { if (len1 != len2) { // <= return false; } if (len1 == 0) { return true; } .... if (len1 == len2) { // <= .... } } 

Die Bedingung len1 == len2 ist immer erfüllt, da die entgegengesetzte Prüfung oben bereits durchgeführt wurde.

Ein weiteres Beispiel aus demselben Projekt:

 if (instances != null) { // <= Set<String> oldKeys = new HashSet<>(instances.keySet()); if (oldKeys.removeAll(latestKeys)) { .... } this.instances.keySet().removeAll(oldKeys); this.instances.putAll(freshInstances); } else { this.instances.putAll(freshInstances); // <= } 

Hier im else- Block ist die Dereferenzierung des Nullzeigers garantiert. Hinweis: Hier sind Instanzen dieselben wie this.instances .

Beispiel aus dem JMonkeyEngine-Projekt:

 public static int convertNewtKey(short key) { .... if (key >= 0x10000) { return key - 0x10000; } return 0; } 

Hier wird die Schlüsselvariable mit der Nummer 65536 verglichen, sie ist jedoch vom Typ short , und der maximal mögliche Wert für short ist 32767. Dementsprechend ist die Bedingung niemals erfüllt.

Ein Beispiel aus dem Jenkins-Projekt:
 public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; } 

Die Variable cnt wurde in diesen Code eingeführt, um die Anzahl der Durchgänge auf fünf zu begrenzen. Sie hat jedoch vergessen, sie zu erhöhen, wodurch die Prüfung unbrauchbar wird.

Anmerkungsmechanismus


Zusätzlich benötigt der Analysator einen Anmerkungsmechanismus. Annotations ist ein Markup-System, das dem Analysator zusätzliche Informationen zu den verwendeten Methoden und Klassen liefert, zusätzlich zu den Informationen, die durch die Analyse ihrer Signatur erhalten werden können. Das Markup erfolgt manuell, es ist ein langer und mühsamer Prozess, da zur Erzielung der besten Ergebnisse eine große Anzahl von Standardklassen und -methoden der Java-Sprache mit Anmerkungen versehen werden muss. Es ist auch sinnvoll, beliebte Bibliotheken mit Anmerkungen zu versehen. Im Allgemeinen können Anmerkungen als Wissensbasis des Analysators über die Verträge von Standardmethoden und -klassen betrachtet werden.

Hier ist ein kleines Beispiel für einen Fehler, der mithilfe von Anmerkungen erkannt werden kann:

 int test(int a, int b) { ... return Math.max(a, a); } 

In diesem Beispiel wurde aufgrund eines Tippfehlers dieselbe Variable als zweites Argument an die Math.max- Methode als erstes Argument übergeben. Ein solcher Ausdruck ist bedeutungslos und verdächtig.

In dem Wissen, dass die Argumente der Math.max- Methode immer unterschiedlich sein sollten, kann der statische Analysator eine Warnung für diesen Code ausgeben.

Mit Blick auf die Zukunft werde ich einige Beispiele für unser Markup von integrierten Klassen und Methoden (C ++ - Code) geben:

 Class("java.lang.Math") - Function("abs", Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Returns(Arg1, [](const Int &v) { return v.Abs(); }) - Function("max", Type::Int32, Type::Int32) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(Arg1, Arg2) .Returns(Arg1, Arg2, [](const Int &v1, const Int &v2) { return v1.Max(v2); }) Class("java.lang.String", TypeClassification::String) - Function("split", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotNull(Arg1)) .Returns(Ptr(NotNullPointer)) Class("java.lang.Object") - Function("equals", Type::Pointer) .Pure() .Set(FunctionClassification::NoDiscard) .Requires(NotEquals(This, Arg1)) Class("java.lang.System") - Function("exit", Type::Int32) .Set(FunctionClassification::NoReturn) 

Erklärungen:

  • Klasse - kommentierte Klasse;
  • Funktion - Methode der annotierten Klasse;
  • Reine Anmerkung, die zeigt, dass das Verfahren sauber ist, d.h. deterministisch und ohne Nebenwirkungen;
  • Setzen - Setzen eines beliebigen Flags für die Methode.
  • FunctionClassification :: NoDiscard - Ein Flag, das bedeutet, dass der Rückgabewert der Methode verwendet werden muss.
  • FunctionClassification :: NoReturn - Ein Flag, das angibt, dass die Methode keine Kontrolle zurückgibt.
  • Arg1 , Arg2 , ... , ArgN - Argumente für die Methode;
  • Rückgabe - der Rückgabewert der Methode;
  • Benötigt - einen Vertrag für die Methode.

Es ist erwähnenswert, dass es neben dem manuellen Markup einen anderen Ansatz für die Annotation gibt - die automatische Ausgabe von Verträgen basierend auf Bytecode. Es ist klar, dass Sie mit diesem Ansatz nur bestimmte Vertragsarten anzeigen können, aber es ist möglich, zusätzliche Informationen im Allgemeinen aus allen Abhängigkeiten zu erhalten, und nicht nur aus denen, die manuell mit Anmerkungen versehen wurden.

Übrigens gibt es bereits ein Tool, das Verträge wie @Nullable , NotNull basierend auf Bytecode - FABA anzeigen kann . Soweit ich weiß, wird das FABA-Derivat in IntelliJ IDEA verwendet.

Jetzt erwägen wir auch, eine Bytecode-Analyse hinzuzufügen, um Verträge für alle Methoden zu erhalten, da diese Verträge unsere manuellen Anmerkungen gut ergänzen könnten.

Diagnoseregeln bei der Arbeit beziehen sich häufig auf Anmerkungen. Neben der Diagnose verwenden Anmerkungen den Datenflussmechanismus. Mithilfe der Annotation der Methode java.lang.Math.abs kann beispielsweise der Wert für das Zahlenmodul genau berechnet werden. Gleichzeitig müssen Sie keinen zusätzlichen Code schreiben - markieren Sie einfach die Methode korrekt.

Betrachten Sie ein Beispiel für einen Fehler aus dem Ruhezustand-Projekt, der durch Anmerkungen erkannt werden kann:

 public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } } 

In diesem Code vergleicht die equals () -Methode das purchaseSequence- Objekt mit sich selbst. Sicher ist dies ein Tippfehler und auf der rechten Seite sollte das sein. Kaufsequenz , nicht Kaufsequenz .

Wie Dr. Frankenstein einen Analysator aus Teilen zusammenbaute


Bild 2


Da die Mechanismen des Datenflusses und der Anmerkungen selbst nicht sehr stark an eine bestimmte Sprache gebunden sind, wurde beschlossen, diese Mechanismen aus unserem C ++ - Analysator wiederzuverwenden. Dadurch konnten wir schnell die gesamte Leistung des C ++ - Analysatorkerns in unserem Java-Analysator nutzen. Darüber hinaus wurde diese Entscheidung auch durch die Tatsache beeinflusst, dass diese Mechanismen in modernem C ++ mit einer Reihe von Metaprogrammier- und Vorlagenmagie geschrieben wurden und dementsprechend nicht sehr gut für die Übertragung in eine andere Sprache geeignet sind.

Um den Java-Teil mit dem C ++ - Kernel zu verknüpfen, haben wir uns für SWIG (Simplified Wrapper and Interface Generator) entschieden - ein Tool zum automatischen Generieren von Wrappern und Schnittstellen zum Verknüpfen von C- und C ++ - Programmen mit Programmen, die in anderen Sprachen geschrieben sind. Für Java generiert SWIG JNI- Code (Java Native Interface) .

SWIG eignet sich hervorragend für Fälle, in denen bereits eine große Menge an C ++ - Code in ein Java-Projekt integriert werden muss.

Ich werde ein minimales Beispiel für die Arbeit mit SWIG geben. Angenommen, wir haben eine C ++ - Klasse, die wir in einem Java-Projekt verwenden möchten:

CoolClass.h

 class CoolClass { public: int val; CoolClass(int val); void printMe(); }; 

CoolClass.cpp

 #include <iostream> #include "CoolClass.h" CoolClass::CoolClass(int v) : val(v) {} void CoolClass::printMe() { std::cout << "val: " << val << '\n'; } 

Zuerst müssen Sie eine SWIG-Schnittstellendatei mit einer Beschreibung aller exportierten Funktionen und Klassen erstellen. Auch in dieser Datei werden bei Bedarf zusätzliche Einstellungen vorgenommen.

Beispiel.i

 %module MyModule %{ #include "CoolClass.h" %} %include "CoolClass.h" 

Danach können Sie SWIG ausführen:

 $ swig -c++ -java Example.i 

Es werden die folgenden Dateien generiert:

  • CoolClass.java - eine Klasse, mit der wir direkt in einem Java-Projekt arbeiten werden;
  • MyModule.java - eine Modulklasse, in der alle freien Funktionen und Variablen platziert sind;
  • MyModuleJNI.java - Java-Wrapper;
  • Example_wrap.cxx - C ++ - Wrapper.

Jetzt müssen Sie nur noch die resultierenden Java-Dateien zum Java-Projekt und die CXX-Datei zum C ++ - Projekt hinzufügen.

Schließlich müssen Sie das C ++ - Projekt als dynamische Bibliothek kompilieren und mit System.loadLibary () in das Java-Projekt laden :

App.java

 class App { static { System.loadLibary("example"); } public static void main(String[] args) { CoolClass obj = new CoolClass(42); obj.printMe(); } } 

Schematisch kann dies wie folgt dargestellt werden:

Bild 8


Natürlich ist in einem realen Projekt nicht alles so einfach und Sie müssen sich etwas mehr anstrengen:

  • Um Vorlagenklassen und -methoden aus C ++ verwenden zu können, müssen sie für alle akzeptierten Vorlagenparameter mithilfe der % template- Direktive instanziiert werden.
  • In einigen Fällen müssen Sie möglicherweise Ausnahmen abfangen, die vom C ++ - Teil im Java-Teil ausgelöst werden. Standardmäßig fängt SWIG keine Ausnahmen von C ++ ab (Segfault tritt auf). Dies ist jedoch mit der % -Ausnahme-Direktive möglich.
  • Mit SWIG können Sie den Plus-Code auf der Java-Seite mit der Direktive % expand erweitern . In unserem Projekt fügen wir beispielsweise virtuellen Methoden die Methode toString () hinzu , damit wir sie im Java-Debugger anzeigen können.
  • Um das RAII-Verhalten aus C ++ zu emulieren, ist die AutoClosable- Schnittstelle in allen interessierenden Klassen implementiert.
  • Der Direktorenmechanismus ermöglicht die Verwendung eines sprachübergreifenden Polymorphismus.
  • Bei Typen, die nur in C ++ (in ihrem Speicherpool) zugewiesen sind, werden Konstruktoren und Finalizer entfernt, um die Leistung zu verbessern. Der Garbage Collector ignoriert diese Typen.

Weitere Informationen zu all diesen Mechanismen finden Sie in der SWIG-Dokumentation .

Unser Analysator basiert auf gradle, das CMake aufruft, das wiederum SWIG aufruft und den C ++ - Teil kompiliert. Für Programmierer geschieht dies fast unmerklich, sodass wir während der Entwicklung keine besonderen Unannehmlichkeiten haben.

Der Kern unseres C ++ - Analysators basiert auf Windows, Linux und MacOS, sodass der Java-Analysator auch in diesen Betriebssystemen funktioniert.

Was ist eine Diagnoseregel?


Die Diagnose selbst und der Code für die Analyse sind in Java geschrieben. Dies ist auf die enge Interaktion mit Spoon zurückzuführen. Jede Diagnoseregel ist ein Besucher, dessen Methoden überladen sind und bei dem die für uns interessanten Elemente umgangen werden:

Bild 9

Das V6004-Diagnoseframework sieht beispielsweise folgendermaßen aus:

 class V6004 extends PvsStudioRule { .... @Override public void visitCtIf(CtIf ifElement) { // if ifElement.thenStatement statement is equivalent to // ifElement.elseStatement statement => add warning V6004 } } 

Plugins


Für die einfachste Integration des statischen Analysators in das Projekt haben wir Plugins für die Montagesysteme Maven und Gradle entwickelt. Der Benutzer kann nur unser Plugin zum Projekt hinzufügen.

Für Gradle:

 .... apply plugin: com.pvsstudio.PvsStudioGradlePlugin pvsstudio { outputFile = 'path/to/output.json' .... } 

Für Maven:

 .... <plugin> <groupId>com.pvsstudio</groupId> <artifactId>pvsstudio-maven-plugin</artifactId> <version>0.1</version> <configuration> <analyzer> <outputFile>path/to/output.json</outputFile> .... </analyzer> </configuration> </plugin> 

Danach erhält das Plugin unabhängig die Projektstruktur und startet die Analyse.

Zusätzlich haben wir ein Prototyp-Plugin für IntelliJ IDEA entwickelt.

Bild 1

Dieses Plugin funktioniert auch in Android Studio.

Ein Plugin für Eclipse befindet sich derzeit in der Entwicklung.

Inkrementelle Analyse


Wir haben einen inkrementellen Analysemodus bereitgestellt, mit dem Sie nur geänderte Dateien überprüfen und dadurch die für die Code-Analyse erforderliche Zeit erheblich reduzieren können. Dank dessen können Entwickler die Analyse so oft wie nötig ausführen.

Die inkrementelle Analyse umfasst mehrere Phasen:

  • Löffel Metamodell Caching;
  • Wiederaufbau des geänderten Teils des Metamodells;
  • Analyse geänderter Dateien.

Unser Testsystem


Um den Java Analyzer an realen Projekten zu testen, haben wir ein spezielles Toolkit geschrieben, mit dem Sie mit der Datenbank offener Projekte arbeiten können. Es wurde in ^ W Python + Tkinter geschrieben und ist plattformübergreifend.

Es funktioniert wie folgt:

  • Das Testprojekt einer bestimmten Version wird aus dem Repository auf GitHub heruntergeladen.
  • Das Projekt wird zusammengestellt;
  • Unser Plugin wird zu pom.xml oder build.gradle hinzugefügt (mit git apply);
  • Der statische Analysator wird über das Plugin gestartet.
  • Der resultierende Bericht wird mit dem Benchmark für dieses Projekt verglichen.

Dieser Ansatz stellt sicher, dass durch die Änderung des Analysatorcodes keine guten Antworten verloren gehen. Unten finden Sie die Benutzeroberfläche unseres Testdienstprogramms.

Bild 11

Projekte in Berichten, die vom Standard abweichen, sind rot markiert. Mit der Schaltfläche Genehmigen können Sie die aktuelle Version des Berichts als Referenz speichern.

Fehlerbeispiele


Aus Tradition werde ich einige Fehler aus verschiedenen offenen Projekten zitieren, die unser Java-Analysator gefunden hat. In Zukunft ist geplant, Artikel mit einem detaillierteren Bericht über jedes Projekt zu schreiben.

Projekt im Ruhezustand


PVS-Studio Warnung: V6009 Funktion 'gleich' empfängt ungerade Argumente. Überprüfen Sie die Argumente: dies, 1. PurchaseRecord.java 57

 public boolean equals(Object other) { if (other instanceof Id) { Id that = (Id) other; return purchaseSequence.equals(this.purchaseSequence) && that.purchaseNumber == this.purchaseNumber; } else { return false; } } 

In diesem Code vergleicht die equals () -Methode das purchaseSequence- Objekt mit sich selbst. Höchstwahrscheinlich ist dies ein Tippfehler und auf der rechten Seite sollte das sein. PurchaseSequence , nicht PurchaseSequence .

PVS-Studio Warnung: V6009 Funktion 'gleich' empfängt ungerade Argumente. Überprüfen Sie die Argumente: this, 1. ListHashcodeChangeTest.java 232

 public void removeBook(String title) { for( Iterator<Book> it = books.iterator(); it.hasNext(); ) { Book book = it.next(); if ( title.equals( title ) ) { it.remove(); } } } 

Eine Operation ähnlich der vorherigen - rechts sollte book.title sein , nicht title .

Projektstock


PVS-Studio- Warnung : V6007-Ausdruck 'colOrScalar1.equals ("Column")' ist immer falsch. GenVectorCode.java 2768

PVS-Studio- Warnung : Der V6007-Ausdruck 'colOrScalar1.equals ("Scalar")' ist immer falsch. GenVectorCode.java 2774

PVS-Studio- Warnung : V6007-Ausdruck 'colOrScalar1.equals ("Column")' ist immer falsch. GenVectorCode.java 2785

 String colOrScalar1 = tdesc[4]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column")) { .... } else if (colOrScalar1.equals("Col") && colOrScalar1.equals("Scalar")) { .... } else if (colOrScalar1.equals("Scalar") && colOrScalar1.equals("Column")) { .... } 

Die Operatoren sind hier eindeutig verwirrt und anstelle von ' ||' verwendet ' &&' .

JavaParser-Projekt


PVS-Studio- Warnung : V6001 Links und rechts vom Operator '&&' befinden sich identische Unterausdrücke 'tokenRange.getBegin (). GetRange (). IsPresent ()'. Node.java 213

 public Node setTokenRange(TokenRange tokenRange) { this.tokenRange = tokenRange; if (tokenRange == null || !(tokenRange.getBegin().getRange().isPresent() && tokenRange.getBegin().getRange().isPresent())) { range = null; } else { range = new Range( tokenRange.getBegin().getRange().get().begin, tokenRange.getEnd().getRange().get().end); } return this; } 

Der Analysator stellte fest, dass sich die gleichen Ausdrücke links und rechts vom Operator && befinden (während alle Methoden in der Aufrufkette sauber sind). Im zweiten Fall muss höchstwahrscheinlich tokenRange.getEnd () und nicht tokenRange.getBegin () verwendet werden .

PVS-Studio Warnung: V6016 Verdächtiger Zugriff auf das Element des Objekts 'typeDeclaration.getTypeParameters ()' durch einen konstanten Index innerhalb einer Schleife. ResolvedReferenceType.java 265

 if (!isRawType()) { for (int i=0; i<typeDeclaration.getTypeParams().size(); i++) { typeParametersMap.add( new Pair<>(typeDeclaration.getTypeParams().get(0), typeParametersValues().get(i))); } } 

Der Analysator hat einen verdächtigen Zugriff auf das Sammlungsobjekt an einem konstanten Index innerhalb der Schleife festgestellt. Möglicherweise liegt ein Fehler in diesem Code vor.

Jenkins-Projekt


PVS-Studio- Warnung : V6007 Ausdruck 'cnt <5' ist immer wahr. AbstractProject.java 557

 public final R getSomeBuildWithWorkspace() { int cnt = 0; for (R b = getLastBuild(); cnt < 5 && b ! = null; b = b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws != null) return b; } return null; } 

Die Variable cnt wurde in diesen Code eingeführt , um die Anzahl der Durchgänge auf fünf zu begrenzen. Sie hat jedoch vergessen, sie zu erhöhen, wodurch die Prüfung unbrauchbar wird.

Spark-Projekt


PVS-Studio- Warnung : V6007-Ausdruck 'sparkApplications! = Null' ist immer wahr. SparkFilter.java 127

 if (StringUtils.isNotBlank(applications)) { final String[] sparkApplications = applications.split(","); if (sparkApplications != null && sparkApplications.length > 0) { ... } } 

Die Überprüfung des von der Split- Methode zurückgegebenen Ergebnisses auf Null ist bedeutungslos, da diese Methode immer eine Sammlung zurückgibt und niemals Null zurückgibt .

Löffelprojekt


PVS-Studio Warnung: V6001 Es gibt identische Unterausdrücke '! M.getSimpleName (). StartsWith ("set")' links und rechts vom Operator '&&'. SpoonTestHelpers.java 108

 if (!m.getSimpleName().startsWith("set") && !m.getSimpleName().startsWith("set")) { continue; } 

In diesem Code befinden sich dieselben Ausdrücke links und rechts vom Operator && (alle Methoden in der Aufrufkette sind sauber). Höchstwahrscheinlich enthält der Code einen logischen Fehler.

PVS-Studio- Warnung : V6007 Der Ausdruck 'idxOfScopeBoundTypeParam> = 0' ist immer wahr. MethodTypingContext.java 243

 private boolean isSameMethodFormalTypeParameter(....) { .... int idxOfScopeBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= int idxOfSuperBoundTypeParam = getIndexOfTypeParam(....); if (idxOfScopeBoundTypeParam >= 0) { // <= return idxOfScopeBoundTypeParam == idxOfSuperBoundTypeParam; } } .... } 

Hier versiegelten sie die zweite Bedingung und schrieben anstelle von idxOfSuperBoundTypeParam idxOfScopeBoundTypeParam .

Frühlingssicherheitsprojekt


PVS-Studio Warnung: V6001 Links und rechts vom '||' befinden sich identische Unterausdrücke. Betreiber. Überprüfen Sie die Zeilen: 38, 39. AnyRequestMatcher.java 38

 @Override @SuppressWarnings("deprecation") public boolean equals(Object obj) { return obj instanceof AnyRequestMatcher || obj instanceof security.web.util.matcher.AnyRequestMatcher; } 

Die Operation ähnelt der vorherigen - hier wird der Name derselben Klasse unterschiedlich geschrieben.

PVS-Studio Warnung: V6006 Das Objekt wurde erstellt, wird jedoch nicht verwendet. Das Schlüsselwort 'throw' könnte fehlen. DigestAuthenticationFilter.java 434

 if (!expectedNonceSignature.equals(nonceTokens[1])) { new BadCredentialsException( DigestAuthenticationFilter.this.messages .getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[] { nonceAsPlainText }, "Nonce token compromised {0}")); } 

In diesem Code haben sie vergessen , vor der Ausnahme einen Wurf hinzuzufügen . Infolgedessen wird das BadCredentialsException- Ausnahmeobjekt ausgelöst , aber in keiner Weise verwendet, d. H. Eine Ausnahme wird nicht ausgelöst.

PVS-Studio Warnung: V6030 Die Methode rechts neben dem '|' Operatoren werden unabhängig vom Wert des linken Operanden aufgerufen. Vielleicht ist es besser, '||' zu verwenden. RedirectUrlBuilder.java 38

 public void setScheme(String scheme) { if (!("http".equals(scheme) | "https".equals(scheme))) { throw new IllegalArgumentException("..."); } this.scheme = scheme; } 

In diesem Code wird die Verwendung von | ungerechtfertigt, da bei der Verwendung die rechte Seite berechnet wird, auch wenn die linke Seite bereits wahr ist. In diesem Fall macht dies keinen praktischen Sinn, daher der Bediener | es lohnt sich durch || zu ersetzen .

IntelliJ IDEA-Projekt


PVS-Studio Warnung: V6008 Mögliche Null-Dereferenzierung von 'Editor'. IntroduceVariableBase.java:609

 final PsiElement nameSuggestionContext = editor == null ? null : file.findElementAt(...); // <= final RefactoringSupportProvider supportProvider = LanguageRefactoringSupport.INSTANCE.forLanguage(...); final boolean isInplaceAvailableOnDataContext = supportProvider != null && editor.getSettings().isVariableInplaceRenameEnabled() && // <= ... 

Der Analysator hat festgestellt, dass in diesem Code eine Dereferenzierung des Nullzeigers des Editors auftreten kann . Es lohnt sich, einen zusätzlichen Scheck hinzuzufügen.

PVS-Studio Warnung: V6007 Ausdruck ist immer falsch. RefResolveServiceImpl.java:814

 @Override public boolean contains(@NotNull VirtualFile file) { .... return false & !myProjectFileIndex.isUnderSourceRootOfType(....); } 

Es fällt mir schwer zu sagen, was der Autor vorhatte, aber ein solcher Code sieht sehr verdächtig aus. Auch wenn hier plötzlich kein Fehler mehr auftritt, lohnt es sich, diesen Ort neu zu schreiben, um den Analysator und andere Programmierer nicht zu verwirren.

PVS-Studio- Warnung : V6007 Ausdruck 'Ergebnis [0]' ist immer falsch. CopyClassesHandler.java:298

 final boolean[] result = new boolean[] {false}; // <= Runnable command = () -> { PsiDirectory target; if (targetDirectory instanceof PsiDirectory) { target = (PsiDirectory)targetDirectory; } else { target = WriteAction.compute(() -> ((MoveDestination)targetDirectory).getTargetDirectory( defaultTargetDirectory)); } try { Collection<PsiFile> files = doCopyClasses(classes, map, copyClassName, target, project); if (files != null) { if (openInEditor) { for (PsiFile file : files) { CopyHandler.updateSelectionInActiveProjectView( file, project, selectInActivePanel); } EditorHelper.openFilesInEditor( files.toArray(PsiFile.EMPTY_ARRAY)); } } } catch (IncorrectOperationException ex) { Messages.showMessageDialog(project, ex.getMessage(), RefactoringBundle.message("error.title"), Messages.getErrorIcon()); } }; CommandProcessor processor = CommandProcessor.getInstance(); processor.executeCommand(project, command, commandName, null); if (result[0]) { // <= ToolWindowManager.getInstance(project).invokeLater(() -> ToolWindowManager.getInstance(project) .activateEditorComponent()); } 

Ich vermute, dass sie hier vergessen haben, den Wert im Ergebnis irgendwie zu ändern . Aus diesem Grund meldet der Analysator, dass die Überprüfung, ob (Ergebnis [0]) sinnlos ist.

Fazit


Die Java-Richtung ist sehr vielseitig - sie ist Desktop, Android, Web und vieles mehr, sodass wir viel Raum für Aktivitäten haben. Zunächst werden wir natürlich die Bereiche entwickeln, die am gefragtesten sind.

Hier sind unsere Pläne für die nahe Zukunft:

  • Ausgabeanmerkungen basierend auf Bytecode;
  • Integration in Projekte auf Ant (jemand anderes nutzt es 2018?);
  • Plugin für Eclipse (in Entwicklung);
  • Noch mehr Diagnosen und Anmerkungen;
  • Verbesserung des Datenflussmechanismus.

Ich empfehle auch diejenigen, die am Testen der Alpha-Version unseres Java-Analysators teilnehmen möchten, sobald diese verfügbar ist. Schreiben Sie uns dazu zur Unterstützung . Wir werden Ihren Kontakt zur Liste hinzufügen und Ihnen schreiben, wenn wir die erste Alpha-Version vorbereiten.


Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Egor Bredikhin. Entwicklung eines neuen statischen Analysators:

Haben Sie den Artikel gelesen und eine Frage?

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


All Articles