Fehler entfernen mit Error Prone

„Dumme und Gescheite unterscheiden sich dadurch, dass der Dumme immer dieselben Fehler macht und der Gescheite immer neue.“

Kurt Tucholsky

Menschen machen Fehler und Software Entwickler benehmen sich in dieser Hinsicht auch sehr menschlich. Um die Zahl der Fehler in den Programmen zu reduzieren wurden daher schon früh verschiedene Vermeidungsstrategien entwickelt. Dies bekanntesten Strategien sind Tests und die statische Code Analyse.

Die Hauptaufgabe der Tests ist die Prüfung der korrekten Arbeitsweise der Software. Dafür wird das tatsächliche Verhalten mit dem erwünschten Verhalten verglichen. Die statische Code Analyse verfolgt eine andere Zielsetzung. Hier wird der Quellcode nach fehlerhafter oder unvorteilhafter Verwendung diverser Sprach-Konstrukte durchsucht. Bekannte Tools zur statischen Code Analyse sind Checkstyle, PMD und SonarLint. In diesem Beitrag geht es um das Tool Error Prone aus dem Hause Google.

Im folgenden Beispiel wird eine IllegalArgumentException erzeugt aber nicht geworfen.

@Override
public Void visit(Response response, String code, Void input) {
  Map<String, Content> content = response.getContent();
  if (content != null) {
    content.forEach((contentType, value) -> value.accept(this, contentType, input));
    new IllegalArgumentException("content is not null" + new Integer(23));
  }
  return input;
}

Dieser Fehler kann leicht übersehen werden und wird üblicherweise auch nicht vom Compiler angemahnt. Damit Error Prone diesen Fehler entdecken kann, muss es als Plugin in den Java Compiler integriert werden. Dies geschieht unter Maven mit den folgenden Zeilen.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <compilerArgs>
      <arg>-XDcompilePolicy=simple</arg>
      <arg>-Xplugin:ErrorProne</arg>
    </compilerArgs>
    <annotationProcessorPaths>
      <path>
        <groupId>com.google.errorprone</groupId>
        <artifactId>error_prone_core</artifactId>
        <version>2.8.0</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

Wird der Quellcode mit Unterstützung durch das Error Prone Plugin kompiliert, erhält der Benutzer folgende Fehlermeldung.

[WARNING] /rest-markdown/src/main/java/de/schegge/rest/markdown/EndPointUpdater.java:[89,56] [BoxedPrimitiveConstructor] valueOf or autoboxing provides better time and space performance
    (see https://errorprone.info/bugpattern/BoxedPrimitiveConstructor)
  Did you mean 'new RuntimeException("content is not null: " + 23);'?
[ERROR] /rest-markdown/src/main/java/de/schegge/rest/markdown/EndPointUpdater.java:[89,9] [DeadException] Exception created but not thrown
    (see https://errorprone.info/bugpattern/DeadException)
  Did you mean 'throw new RuntimeException("content is not null: " + new Integer(23));'?

Das Plugin erkennt nicht nur den Fehler und behandelt ihn als Compiler-Fehler, es wird auch ein Korrekturhinweis geliefert. Außerdem wurde das Bug Pattern BoxedPrimitiveConstructor erkannt, weil eine unnötige Integer Instanz erzeugt wurde. Das Bug Pattern DeadException wird als Compiler-Fehler handelt, für das Bug Pattern BoxedPrimitiveConstructor wird nur eine Warnung ausgeben.

Wirklich interessant wird Error Prone durch zwei weitere Features. Zum einen können eigene Bug Pattern geprüft werden und zum anderen kann Error Prone automatische Refactorings vornehmen. Wie eigene Bug Pattern geprüft werden können sprengt hier den Rahmen und wird Inhalt eines späteren Beitrags.

Viele Bug Pattern von Error Prone beinhalten neben der Erkennung eines Pattern auch einen Korrekturvorschlag. Statt einer Compiler Meldung kann auch ein Patch für den Fehler generiert werden. Dazu müssen zwei weitere Compiler-Flags angegeben werden.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <compilerArgs>
      <arg>-XDcompilePolicy=simple</arg>
      <arg>-Xplugin:ErrorProne -XepPatchChecks:DefaultCharset,DeadException,BoxedPrimitiveConstructor
           -XepPatchLocation:${project.basedir}/src/main/java</arg>
    </compilerArgs>
    <annotationProcessorPaths>
      <path>
        <groupId>com.google.errorprone</groupId>
        <artifactId>error_prone_core</artifactId>
        <version>2.8.0</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

Mit dem Flag epPatchLocation wird der Speicherort für die Patch-Datei angegeben und mit dem Flag epPatchChecks alle Bug Pattern, die gepatcht werden sollen. In diesem Fall DefaultCharset, DeadException und BoxedPrimitiveConstructor. Wird nun der Compiler aufgerufen, erzeugt das Error Prone Plugin eine Datei error-prone.patch mit folgendem Inhalt.

--- de\schegge\rest\markdown\EndPointUpdater.java
+++ de\schegge\rest\markdown\EndPointUpdater.java
@@ -79,5 +79,5 @@
     if (content != null) {
       content.forEach((contentType, value) -> value.accept(this, contentType, input));
-        new RuntimeException("content is not null: " + new Integer(23));
+        throw new RuntimeException("content is not null: " + 23);
     }
     return input;

Wird das patch Tool mit dieser Datei aufgerufen, dann wird die einfache Instanziierung der IllegalArgumentException durch das korrekte Werfen der IllegalArgumentException ersetzt. Zusätzlich wird auch die Integer Instanziierung entfernt.

Wird eine Bug Pattern einmal fälschlicherweise erkannt, dann kann dies mit einer @SuppressWarning Annotation behoben werden.

@Override
@SuppressWarnings("DeadException")
public Void visit(Response response, String code, Void input) {
  Map<String, Content> content = response.getContent();
  if (content != null) {
    content.forEach((contentType, value) -> value.accept(this, contentType, input));
    new IllegalArgumentException("content is not null" + new Integer(23));
  }
  return input;
}

In diesem Beispiel mahnt Error Prone nur das BoxedPrimitiveConstructor Pattern an und ignoriert das DeadException Pattern.

Bei derzeit etwa 500 Bug Pattern, die von Error Prone geprüft werden können, ist sicherlich für jeden Entwickler etwas dabei. Und wenn das nicht reicht, dann werden bald auch eigene Bug Pattern hinzugefügt.