BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Error Prone Improves Java Code by Detecting Common Mistakes

Error Prone Improves Java Code by Detecting Common Mistakes

Error Prone, a Java compiler plugin open sourced by Google, performs static analysis during compilation to detect bugs or suggest possible improvements. The plugin contains more than 500 pre-defined bug checks and allows third party and custom plugins. After detecting issues, Error Prone can display a warning or automatically change the code with a predefined solution. Error Prone supports Java 8, 11 and 17 and may be used for bug fixing or large scale refactoring.Installation and configuration instructions for Maven, Bazel, Ant or Gradle can be found in the documentation. The compiler should be configured to use Error Prone as an annotation processor, for example when creating a test project with Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.10.1</version>
    <configuration>
        <release>17</release>
        <encoding>UTF-8</encoding>
        <compilerArgs>
            <arg>-XDcompilePolicy=simple</arg>
            <arg>-Xplugin:ErrorProne</arg>
        </compilerArgs>
        <annotationProcessorPaths>
            <path>
                <groupId>com.google.errorprone</groupId>
                <artifactId>error_prone_core</artifactId>
                <version>2.15.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Now an example class can be created. Consider the following method that uses equals to compare two arrays, more precisely it compares the objects and not the content of the arrays:

public boolean compare(String firstList[], String secondList[]) {
    return firstList.equals(secondList);
}

Executing mvn clean verify triggers the Error Prone analysis and results in the following error message:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:
    compile (default-compile) on project ErrorProne: Compilation failure
[ERROR] …/ErrorProne/src/main/java/org/example/Main.java:[5,28] 
    [ArrayEquals] Reference equality used to compare arrays
[ERROR] 	(see https://errorprone.info/bugpattern/ArrayEquals)
[ERROR]   Did you mean 'return Arrays.equals(firstList, secondList);'?

The ArrayEquals bug pattern is found and Error Prone suggests to change the implementation in order to compare the content of the array instead of the objects:

return Arrays.equals(firstList, secondList);

Receiving an error helps to improve the code, but it's also possible to let Error Prone apply the solution automatically. The -XepPatchChecks argument should contain a comma separated list of bug patterns to apply. In this case, only the ArrayEquals solution is applied to the codebase. The -XepPatchLocation argument is used to specify the location of the solution file. In this case the original file is modified:

<compilerArgs>
    <arg>-XDcompilePolicy=simple</arg>
    <arg>-Xplugin:ErrorProne -XepPatchChecks:ArrayEquals    
        -XepPatchLocation:IN_PLACE</arg>
</compilerArgs>

Now, after executing mvn clean verify the class file is automatically changed to:

public boolean compare(String firstList[], String secondList[]) {
    return Arrays.equals(firstList, secondList);
}

The documentation provides more information about the command-line flags.

Apart from the built-in bug patterns, it's also possible to use patterns from other organizations such as SLF4J or to create a custom plugin. The source code for the built-in rules provides various examples that may be used as the basis for a new plugin. For example, a custom Error Prone plugin can replace the older @Before JUnit annotation with the new JUnit 5 @BeforeEach annotation.

The custom Error Prone plugin should be placed in another Maven module than the example project shown earlier. Error Prone uses the service loader mechanism to load the bug checks. Normally, that requires some configuration, however Google's AutoService project simplifies the configuration by using the @AutoService annotation. The @BugPattern annotation is used to configure the name, summary and severity of the bug. The following example returns Description.NO_MATCH if no @Before annotation is found or the SuggestedFix which replaces the @Before annotation with an @BeforeEach annotation:

@AutoService(BugChecker.class)
@BugPattern(
    name = "BeforeCheck",
    summary = "JUnit 4's @Before is replaced by JUnit 5's @BeforeEach",
    severity = BugPattern.SeverityLevel.SUGGESTION
)
public class BeforeCheck extends BugChecker implements BugChecker.AnnotationTreeMatcher {
    private static final Matcher<AnnotationTree> matcher =    
        isType("org.junit.Before");

    @Override
    public Description matchAnnotation(AnnotationTree annotationTree, 
            VisitorState visitorState) {
        if (!matcher.matches(annotationTree, visitorState)) {
            return Description.NO_MATCH;
    	  }
    	  return describeMatch(annotationTree, 
            SuggestedFix.replace(annotationTree, "@BeforeEach"));
    }
}

Error Prone and AutoService dependencies are required to build the custom Error Prone plugin:

<dependency>
	<groupId>com.google.errorprone</groupId>
	<artifactId>error_prone_annotations</artifactId>
	<version>2.15.0</version>
</dependency>
<dependency>
	<groupId>com.google.errorprone</groupId>
	<artifactId>error_prone_check_api</artifactId>
	<version>2.15.0</version>
</dependency>
<dependency>
	<groupId>com.google.auto.service</groupId>
	<artifactId>auto-service-annotations</artifactId>
	<version>1.0.1</version>
</dependency>

The AutoService should be configured as an annotation processor:

<annotationProcessorPaths>
    <path>
        <groupId>com.google.auto.service</groupId>
        <artifactId>auto-service</artifactId>
        <version>1.0.1</version>
    </path>
</annotationProcessorPaths>

Now the custom Error Prone plugin can be installed to the local Maven repository with the mvn install command. After executing the command, the example project should be configured to use the new custom plugin as an annotation processor:

<annotationProcessorPaths>
    <path>
        <groupId>org.example.custom.plugin</groupId>
    	  <artifactId>ErrorProneBeforeCheck</artifactId>
    	  <version>1.0-SNAPSHOT</version>
    </path>
</annotationProcessorPaths>

The new BeforeCheck should be added to the Error Prone analysis:

<compilerArgs>
	<arg>-XDcompilePolicy=simple</arg>
	<arg>-Xplugin:ErrorProne -XepPatchChecks:BeforeCheck  
          -XepPatchLocation:IN_PLACE</arg>
</compilerArgs>

An example Test class may be added which contains a mix of both @Before and @BeforeEach annotations:

public class ErrorProneTest {
	@Before
	void before() {
	}

	@BeforeEach
	void beforeEach() {
	}
}

When running mvn verify the new custom Error Prone plugin replaces the @Before annotation with the @BeforeEach annotation:

public class ErrorProneTest {
	@BeforeEach
	void before() {
	}

	@BeforeEach
	void beforeEach() {
	}
}

Error Prone uses Java internals, that are nowadays hidden, which might result in errors such as:

java.lang.IllegalAccessError: class com.google.errorprone.BaseErrorProneJavaCompiler 
(in unnamed module @0x1a6cf771) 
cannot access class com.sun.tools.javac.api.BasicJavacTask (in module jdk.compiler) 
because module jdk.compiler does not export 
com.sun.tools.javac.api to unnamed module @0x1a6cf771

The solution for Maven is to expose the Java internals by creating a directory called .mvn in the root of the project containing a jvm.config file with the following content:

--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

Alternatively the --add-exports and --add-opens configuration may be supplied to the Maven Compiler Plugin's arguments in the pom.xml:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.10.1</version>
    <configuration>
        <compilerArgs>
            <arg>--add-exports</arg>
            <arg>jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
	        …

More information about using Error Prone with Bazel, Ant and Gradle can be found in the installation instructions.

About the Author

Rate this Article

Adoption
Style

BT