Weit entfernt vom ersten Jahr hat das PVS-Studio-Team über Überprüfungen von Open-Source-Projekten durch den gleichnamigen statischen Code-Analysator gebloggt. Bisher wurden mehr als 300 Projekte geprüft und mehr als 12.000 Fälle in die Datenbank der gefundenen Fehler geschrieben. Zunächst wurde der Analysator implementiert, um C- und C ++ - Code zu testen. Anschließend wurde die Unterstützung der C # -Sprache angezeigt. Daher fällt unter den getesteten Projekten die Mehrheit (> 80%) auf C und C ++. In jüngerer Zeit wurde Java zu unterstützten Sprachen hinzugefügt, was bedeutet, dass PVS-Studio die Türen zu einer neuen Welt öffnet und es Zeit ist, die Datenbank mit Fehlern aus Java-Projekten zu ergänzen.
Die Java-Welt ist riesig und vielfältig, daher weiten sich meine Augen, wenn ich ein Projekt zum Testen eines neuen Analysators auswähle. Letztendlich fiel die Wahl auf die Volltextsuchmaschine und Analytics Elasticsearch. Dies ist ein ziemlich erfolgreiches Projekt, und in erfolgreichen Projekten ist es doppelt oder sogar dreifach angenehmer, Fehler zu finden. Welche Fehler hat PVS-Studio für Java festgestellt? Das Ergebnis der Überprüfung wird im Artikel erläutert.
Oberflächliche Bekanntschaft mit Elasticsearch
Elasticsearch ist eine skalierbare Open Source-Volltextsuch- und Analyse-Engine. Sie können große Datenmengen speichern und eine schnelle Suche und Analyse durchführen (fast in Echtzeit). In der Regel wird es als zugrunde liegender Mechanismus / Technologie verwendet, der Anwendungen komplexe Funktionen und Suchanforderungen bietet.
Unter den wichtigsten Websites, die Elasticsearch verwenden, sind Wikimedia, StumbleUpon, Quora, Foursquare, SoundCloud, GitHub, Netflix, Amazon, IBM und Qbox aufgeführt.
Ich denke mit der Bekanntschaft ist genug.
Wie war es
Es gab keine Probleme mit der Überprüfung. Die Abfolge der Aktionen ist recht einfach und hat nicht viel Zeit in Anspruch genommen:
- Elasticsearch von GitHub heruntergeladen;
- Ich habe die Anweisungen zum Starten des Java-Analysators verwendet und die Analyse gestartet.
- Ich erhielt einen Analysatorbericht, analysierte ihn und hob interessante Fälle hervor.
Kommen wir jetzt zum Punkt.
Achtung Mögliche NullPointerException
V6008 Null-Dereferenzierung von 'Linie'. GoogleCloudStorageFixture.java (451)
private static PathTrie<RequestHandler> defaultHandlers(....) { .... handlers.insert("POST /batch/storage/v1", (request) -> { ....  
Der Fehler in diesem Codefragment besteht darin, dass, wenn die Zeile aus dem Puffer nicht gelesen werden konnte, beim Aufrufen der Methode 
opensWith unter der Bedingung der 
if-Anweisung eine 
NullPointerException ausgelöst wird . Dies ist höchstwahrscheinlich ein Tippfehler, und beim Schreiben der Bedingung war der Operator 
&& anstelle von 
|| gemeint .
V6008 Mögliche Null-Dereferenzierung von 'followIndexMetadata'. TransportResumeFollowAction.java (171), TransportResumeFollowAction.java (170), TransportResumeFollowAction.java (194)
 void start( ResumeFollowAction.Request request, String clusterNameAlias, IndexMetaData leaderIndexMetadata, IndexMetaData followIndexMetadata, ....) throws IOException { MapperService mapperService = followIndexMetadata != null  
Eine weitere Warnung der 
V6008- Diagnose. Bei näherer Betrachtung wurde das 
followIndexMetadata- Objekt nun vernietet. Die 
Startmethode verwendet mehrere Eingabeargumente, einschließlich unseres Verdächtigen. Auf der Grundlage der Überprüfung unseres Objekts auf 
Null wird dann ein neues Objekt gebildet, das an der weiteren Logik der Methode teilnimmt. 
Wenn Sie nach 
null suchen , 
erfahren Sie , dass 
followIndexMetadata immer noch mit einem null-Objekt von außen kommen kann. Ok, schau weiter.
Als nächstes wird die Validierungsmethode aufgerufen, indem viele Argumente gedrückt werden (wiederum das fragliche Objekt). Und wenn Sie sich die Implementierung der Validierungsmethode ansehen, dann passt alles zusammen. Unser potenzielles Nullobjekt wird vom dritten Argument an die 
validate- Methode übergeben, wo es bedingungslos dereferenziert wird. Infolgedessen eine potenzielle 
NullPointerException. static void validate( final ResumeFollowAction.Request request, final IndexMetaData leaderIndex, final IndexMetaData followIndex,  
Es ist nicht bekannt, mit welchen Argumenten die 
Startmethode tatsächlich aufgerufen wird. Es ist möglich, dass alle Argumente irgendwo vor dem Methodenaufruf überprüft werden und wir keine Dereferenzierung des Null-Objekts feststellen. Sie müssen jedoch zugeben, dass eine solche Code-Implementierung immer noch ziemlich unzuverlässig aussieht und Aufmerksamkeit verdient.
V6060 Die ' 
Knoten' -Referenz wurde verwendet, bevor sie gegen Null verifiziert wurde. RestTasksAction.java (152), RestTasksAction.java (151)
 private void buildRow(Table table, boolean fullId, boolean detailed, DiscoveryNodes discoveryNodes, TaskInfo taskInfo) { .... DiscoveryNode node = discoveryNodes.get(nodeId); ....  
Eine andere Diagnoseregel hat hier funktioniert, aber das Problem ist dasselbe: 
NullPointerException . Die Regel lautet: „Leute, was machst du? Wie so? Oh Ärger! Warum verwenden Sie das Objekt zuerst und überprüfen es dann in der nächsten Codezeile auf 
Null ?! ” Es stellt sich also heraus, dass hier ein Nullobjekt dereferenziert wird. Leider hat auch der Kommentar eines der Entwickler nicht geholfen.
V6060 Die Referenz "Ursache" wurde verwendet, bevor sie gegen Null verifiziert wurde. StartupException.java (76), StartupException.java (73)
 private void printStackTrace(Consumer<String> consumer) { Throwable originalCause = getCause(); Throwable cause = originalCause; if (cause instanceof CreationException) { cause = getFirstGuiceCause((CreationException)cause); } String message = cause.toString();  
Hierbei ist zu beachten, dass die 
getCause- Methode der 
Throwable- Klasse 
null zurückgeben kann . Ferner wird das oben betrachtete Problem wiederholt, und es macht keinen Sinn, etwas im Detail zu erklären.
Bedeutungslose Bedingungen
V6007 Ausdruck 's.charAt (i)! =' \ T '' ist immer wahr. Cron.java (1223)
 private static int findNextWhiteSpace(int i, String s) { for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {  
Die betrachtete Funktion gibt den Index des ersten Leerzeichens zurück, beginnend mit Index 
i . Was ist los? Wir haben eine Warnung vom Analysator, dass 
s.charAt (i)! = '\ T' immer wahr ist, was bedeutet, dass es immer Wahrheit und den Ausdruck 
(s.charAt (i)! = '' || s.charAt (i) geben wird! = '\ t') . Ist es so? Ich denke, Sie selbst können dies leicht überprüfen, indem Sie ein beliebiges Zeichen ersetzen.
Infolgedessen gibt diese Methode immer einen Index zurück, der gleich 
s.length () ist , was nicht wahr ist. Ich wage anzunehmen, dass diese etwas höher gelegene Methode schuld ist:
 private static int skipWhiteSpace(int i, String s) { for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) {  
Wir haben diese Methode implementiert, dann kopiert und einige kleinere Korrekturen vorgenommen, um unsere fehlerhafte 
findNextWhiteSpace- Methode zu erhalten. Die Methode wurde angepasst, angepasst, aber nicht angepasst. Um Abhilfe zu schaffen, müssen Sie den Operator 
&& anstelle von 
|| verwenden .
V6007 Der Ausdruck 'verbleibend == 0' ist immer falsch. PemUtils.java (439)
 private static byte[] generateOpenSslKey(char[] password, byte[] salt, int keyLength) { .... int copied = 0; int remaining; while (copied < keyLength) { remaining = keyLength - copied; .... copied += bytesToCopy; if (remaining == 0) {  
Aus dem Zustand des 
kopierten Zyklus 
<keyLength kann festgestellt werden, dass der 
kopierte Zyklus immer kleiner als 
keyLength ist . Daher ist ein Vergleich der Gleichheit der 
verbleibenden Variablen mit 0 sinnlos und führt immer zu einem falschen Ergebnis, und daher wird die Bedingung nicht aus der Schleife verlassen. Lohnt es sich, diesen Code zu löschen, oder müssen Sie die Verhaltenslogik überdenken? Ich denke, nur Entwickler werden in der Lage sein, alle i zu punktieren.
V6007 Der Ausdruck 'healthCheckDn.indexOf (' = ')> 0' ist immer falsch. ActiveDirectorySessionFactory.java (73)
 ActiveDirectorySessionFactory(RealmConfig config, SSLService sslService, ThreadPool threadPool) throws LDAPException { super(...., () -> { if (....) { final String healthCheckDn = ....; if (healthCheckDn.isEmpty() && healthCheckDn.indexOf('=') > 0) { return healthCheckDn; } } return ....; }, ....); .... } 
Wieder ein bedeutungsloser Ausdruck. Damit der Lambda-Ausdruck die Zeichenfolgenvariable 
healthCheckDn zurückgibt, muss die Zeichenfolge 
healthCheckDn entsprechend der Bedingung leer sein und die Zeichenfolge mit dem Zeichen '=' befindet sich nicht an der ersten Position. Fuh, irgendwie erledigt. Und wie Sie richtig verstanden haben, ist dies unmöglich. Wir werden die Logik des Codes nicht verstehen, überlassen Sie es den Entwicklern.
Ich habe nur einige fehlerhafte Beispiele angegeben, aber darüber hinaus gab es viele Fälle, in denen die 
V6007- Diagnose 
ausgelöst wurde , die separat betrachtet und Schlussfolgerungen gezogen werden müssen.
Die Methode ist klein, aber udalenky
 private static byte char64(char x) { if ((int)x < 0 || (int)x > index_64.length) return -1; return index_64[(int)x]; } 
Wir haben also eine winzige Methode mit mehreren Zeilen. Aber Käfer schlafen nicht! Eine Analyse dieser Methode ergab das folgende Ergebnis:
- V6007 Ausdruck '(int) x <0' ist immer falsch. BCrypt.java (429)
- V6025 Möglicherweise ist der Index '(int) x' außerhalb der Grenzen. BCrypt.java (431)
Problem N1. Der Ausdruck 
(int) x <0 ist immer falsch (Ja, ja, wieder 
V6007 ). Die Variable 
x kann nicht negativ sein, da sie vom Typ 
char ist . Der 
Zeichentyp ist eine Ganzzahl ohne Vorzeichen. Dies kann nicht als echter Fehler bezeichnet werden, die Prüfung ist jedoch redundant und kann entfernt werden.
Problem N2. Möglicher Überlauf des Arrays, der zu einer 
ArrayIndexOutOfBoundsException führt . Dann stellt sich die Frage, die an der Oberfläche liegt: "Warten Sie, was ist mit der Überprüfung des Index?"
Wir haben also ein Array mit fester Größe von 128 Elementen:
 private static final byte index_64[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1 }; 
Wenn die Variable 
x in die Eingabe der 
char64- Methode 
eingeht , prüft sie die Gültigkeit des Index. Wo ist die Lücke? Warum ist das Verlassen des Arrays immer noch möglich?
Das Überprüfen von 
(int) x> index_64.length ist nicht ganz korrekt. Wenn 
x mit einem Wert von 128 am Eingang der 
char64- Methode 
ankommt, schützt die Prüfung nicht vor 
ArrayIndexOutOfBoundsException . Vielleicht passiert das in der Realität nie. Die Prüfung ist jedoch falsch geschrieben, und Sie müssen den Operator "größer als" (>) durch "größer als oder gleich" (> =) ersetzen.
Vergleiche, die versucht haben
V6013 Die Nummern 'displaySize' und 'that.displaySize' werden anhand der Referenz verglichen. Möglicherweise war ein Gleichstellungsvergleich beabsichtigt. ColumnInfo.java (122)
 .... private final String table; private final String name; private final String esType; private final Integer displaySize; .... @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ColumnInfo that = (ColumnInfo) o; return displaySize == that.displaySize &&  
Die Unrichtigkeit hierbei ist, dass 
displaySize- Objekte vom Typ 
Integer über den Operator 
== verglichen werden, 
dh sie werden als Referenz verglichen. Es ist durchaus möglich, dass 
ColumnInfo- Objekte verglichen werden, für die die 
displaySize- Felder unterschiedliche Links, aber denselben Inhalt haben. In diesem Fall führt der Vergleich zu einem negativen Ergebnis, während wir die Wahrheit erwartet haben.
Ich würde vorschlagen, dass ein solcher Vergleich das Ergebnis eines fehlgeschlagenen Refactorings sein könnte, und anfangs war das Feld 
displaySize vom Typ 
int .
V6058 Die Funktion 'equals' vergleicht Objekte inkompatibler Typen: Integer, TimeValue. DatafeedUpdate.java (375)
 .... private final TimeValue queryDelay; private final TimeValue frequency; .... private final Integer scrollSize; .... boolean isNoop(DatafeedConfig datafeed) { return (frequency == null || Objects.equals(frequency, datafeed.getFrequency())) && (queryDelay == null || Objects.equals(queryDelay, datafeed.getQueryDelay())) && (scrollSize == null || Objects.equals(scrollSize, datafeed.getQueryDelay()))  
Und wieder falscher Vergleich von Objekten. Vergleichen Sie nun Objekte, deren Typen nicht kompatibel sind ( 
Integer und 
TimeValue ). Das Ergebnis dieses Vergleichs ist offensichtlich und immer falsch. Es ist ersichtlich, dass die Felder der Klasse auf die gleiche Weise miteinander verglichen werden, es ist nur notwendig, die Feldnamen zu ändern. Daher beschloss der Entwickler, das Schreiben von Code durch Kopieren und Einfügen zu beschleunigen, gab sich jedoch den gleichen Fehler. Die Klasse implementiert einen Getter für das 
scrollSize- Feld. 
Um den Fehler zu korrigieren, besteht die richtige Lösung darin, die entsprechende Methode zu verwenden: 
datafeed .getScrollSize () .
Schauen wir uns ein paar weitere Beispiele für Fehler ohne Erklärung an. Das Problem ist bereits offensichtlich.
V6001 Links und rechts vom Operator '==' befinden sich identische Unterausdrücke 'takeInMillis'. TermVectorsResponse.java (152)
 @Override public boolean equals(Object obj) { .... return index.equals(other.index) && type.equals(other.type) && Objects.equals(id, other.id) && docVersion == other.docVersion && found == other.found && tookInMillis == tookInMillis  
V6009 Funktion 'gleich' erhält ein ungerades Argument. Ein Objekt 'shardId.getIndexName ()' wird als Argument für seine eigene Methode verwendet. SnapshotShardFailure.java (208)
 @Override public boolean equals(Object o) { .... return shardId.id() == that.shardId.id() && shardId.getIndexName().equals(shardId.getIndexName()) &&  
Verschiedenes
V6006 Das Objekt wurde erstellt, wird jedoch nicht verwendet. Das Schlüsselwort 'throw' könnte fehlen. JdbcConnection.java (88)
 @Override public void setAutoCommit(boolean autoCommit) throws SQLException { checkOpen(); if (!autoCommit) { new SQLFeatureNotSupportedException(....); } } 
Der Fehler ist offensichtlich und bedarf keiner Erklärung. Der Entwickler hat eine Ausnahme ausgelöst, aber in keiner Weise weiter. Eine solche anonyme Ausnahme wird erfolgreich erstellt und auch erfolgreich und vor allem spurlos zerstört. Der Grund ist das Fehlen einer 
Wurfanweisung .
V6003 Die Verwendung des 
Musters 'if (A) {....} else if (A) {....}' wurde erkannt. Es besteht die Wahrscheinlichkeit eines logischen Fehlers. MockScriptEngine.java (94), MockScriptEngine.java (105)
 @Override public <T> T compile(....) { .... if (context.instanceClazz.equals(FieldScript.class)) { .... } else if (context.instanceClazz.equals(FieldScript.class)) { .... } else if(context.instanceClazz.equals(TermsSetQueryScript.class)) { .... } else if (context.instanceClazz.equals(NumberSortScript.class)) .... } 
In einem 
Konstrukt mit mehreren 
if-else wird eine der Bedingungen zweimal wiederholt, sodass die Situation eine kompetente Überprüfung des Codes erfordert.
V6039 Es gibt zwei ' 
if' -Anweisungen mit identischen bedingten Ausdrücken. Die erste 'if'-Anweisung enthält die Methodenrückgabe. Dies bedeutet, dass die zweite 'if'-Anweisung sinnlos ist. SearchAfterBuilder.java (94), SearchAfterBuilder.java (93)
 public SearchAfterBuilder setSortValues(Object[] values) { .... for (int i = 0; i < values.length; i++) { if (values[i] == null) continue; if (values[i] instanceof String) continue; if (values[i] instanceof Text) continue; if (values[i] instanceof Long) continue; if (values[i] instanceof Integer) continue; if (values[i] instanceof Short) continue; if (values[i] instanceof Byte) continue; if (values[i] instanceof Double) continue; if (values[i] instanceof Float) continue; if (values[i] instanceof Boolean) continue;  
Die gleiche Bedingung wird zweimal hintereinander verwendet. Die zweite Bedingung ist überflüssig oder muss anstelle von 
Boolean ein anderer Typ verwendet werden?
V6009 Die Funktion 'Teilzeichenfolge' empfängt ungerade Argumente. Das Argument 'queryStringIndex + 1' sollte nicht größer als 'queryStringLength' sein. LoggingAuditTrail.java (660)
 LogEntryBuilder withRestUriAndMethod(RestRequest request) { final int queryStringIndex = request.uri().indexOf('?'); int queryStringLength = request.uri().indexOf('#'); if (queryStringLength < 0) { queryStringLength = request.uri().length(); } if (queryStringIndex < 0) { logEntry.with(....); } else { logEntry.with(....); } if (queryStringIndex > -1) { logEntry.with(...., request.uri().substring(queryStringIndex + 1, 
Stellen Sie sich sofort ein 
Fehlerszenario vor , das eine 
StringIndexOutOfBoundsException auslösen könnte . Eine Ausnahme tritt auf, wenn 
request.uri () eine Zeichenfolge zurückgibt, die das Zeichen '#' vor '?' Enthält. In einem solchen Fall gibt es keine Überprüfungen in der Methode, und wenn dies immer noch geschieht, wird eine Katastrophe nicht vermieden. Vielleicht wird dies aufgrund verschiedener Überprüfungen des 
Anforderungsobjekts außerhalb der Methode nie passieren, aber meiner Meinung nach ist es nicht die beste Idee, darauf zu hoffen.
Fazit
Im Laufe der Jahre hat PVS-Studio dazu beigetragen, Fehler im Code von kommerziellen und kostenlosen Open-Source-Projekten zu finden. In jüngerer Zeit wurde Java hinzugefügt, um analysierte Sprachen zu unterstützen. Und einer der ersten Tests für unseren Newcomer war die sich aktiv entwickelnde Elasticsearch. Wir hoffen, dass dieser Check für das Projekt nützlich und für die Leser interessant ist.
Damit sich PVS-Studio für Java schnell an eine neue Welt anpassen kann, benötigen wir neue Tests, neue Benutzer, aktives Feedback und Kunden :). Ich schlage daher vor, unseren Analysator unverzüglich auf Ihren Arbeitsentwurf 
herunterzuladen und zu testen!

Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Maxim Stefanov. 
PVS-Studio für Java macht sich auf den Weg. Nächster Halt ist Elasticsearch