1. Introduction
Ensuring code quality is crucial for the successful deployment of our applications. The presence of bugs and errors can significantly hamper the functionality and stability of software. Here comes one valuable tool that aids in identifying such errors: Error Prone.
Error Prone is a library maintained and used internally by Google. It assists Java developers in detecting and fixing common programming mistakes during the compilation phase.
In this tutorial, we explore the functionalities of the Error Prone library, from installation to customization, and the benefits it offers in enhancing code quality and robustness.
2. Installation
The library is available in the Maven Central repository. We’ll add a new build configuration to configure our application compiler to run the Error Prone checks:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</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.23.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Due to the strong encapsulations of the JDK internals added in version 16, we’ll need to add some flags to allow the plugin to run. One option would be creating a new file .mvn/jvm.config if it doesn’t already exist and adding the required flags for the plugin:
--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
If our maven-compiler-plugin uses an external executable or the maven-toolchains-plugin is enabled, we should add the exports and opens as compilerArgs:
<compilerArgs>
// ...
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
</compilerArgs>
3. Bug Patterns
Identifying and understanding common bug patterns is essential for maintaining the stability and reliability of our software. By recognizing these patterns early in our development process, we can proactively implement strategies to prevent them and improve our code’s overall quality.
3.1. Pre-defined Bug Patterns
The plugin contains more than 500 pre-defined bug patterns. One of those bugs is the DeadException, which we’ll exemplify:
public static void main(String[] args) {
if (args.length == 0 || args[0] != null) {
new IllegalArgumentException();
}
// other operations with args[0]
}
In the code above, we want to ensure that our program receives a non-null parameter. Otherwise, we want to throw an IllegalArgumentException. However, due to carelessness, we just created the exception and forgot to throw it. In many cases, without a bug-checking tool, this case could go unnoticed.
We can run the Error Prone checks on our code using the maven clean verify command. If we do so, we’ll get the following compilation error:
[ERROR] /C:/Dev/incercare_2/src/main/java/org/example/Main.java:[6,12] [DeadException] Exception created but not thrown
(see https://errorprone.info/bugpattern/DeadException)
Did you mean 'throw new IllegalArgumentException();'?
We can see that the plugin not only detected our error but also provided us with a solution for it.
3.2. Custom Bug Patterns
Another notable feature of Error Prone is its ability to support the creation of custom bug checkers. These custom bug checkers enable us to tailor the tool to our specific codebase and address domain-specific issues efficiently.
To create our custom checks, we need to initialize a new project. Let’s call it my-bugchecker-plugin. We’ll start by adding the configuration for the bug checker:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotation</artifactId>
<version>2.23.0</version>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_check_api</artifactId>
<version>2.23.0</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<version>1.0.1</version>
</dependency>
</dependencies>
We added some more dependencies this time. As we can see, besides the Error Prone dependencies, we added Google AutoService. Google AutoService is an open-source code generator tool developed under the Google Auto project. This will discover and load our custom checks.
Now we’ll create our custom check, which will verify if we have any empty methods in our code base:
@AutoService(BugChecker.class)
@BugPattern(name = "EmptyMethodCheck", summary = "Empty methods should be deleted", severity = BugPattern.SeverityLevel.ERROR)
public class EmptyMethodChecker extends BugChecker implements BugChecker.MethodTreeMatcher {
@Override
public Description matchMethod(MethodTree methodTree, VisitorState visitorState) {
if (methodTree.getBody()
.getStatements()
.isEmpty()) {
return describeMatch(methodTree, SuggestedFix.delete(methodTree));
}
return Description.NO_MATCH;
}
}
First, The annotation BugPattern contains the name, a short summary, and the severity of the bug. Next, the BugChecker itself is an implementation of MethodTreeMatcher because we want to match methods that have an empty body. Lastly, the logic in matchMethod() should return a match if the method tree body does not have any statements.
To use our custom bug checker in another project, we should compile it into a separate JAR. We’ll do it by running the maven clean install command. After that, we should include the generated JAR as a dependency in the build configuration of our main project by adding it to the annotationProcessorPaths:
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.23.0</version>
</path>
<path>
<groupId>com.baeldung</groupId>
<artifactId>my-bugchecker-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
This way, our bug checker also becomes reusable. Now, if we write a new class with an empty method:
public class ClassWithEmptyMethod {
public void theEmptyMethod() {
}
}
If we run the maven clean verify command again, we’ll get an error:
[EmptyMethodCheck] Empty methods should be deleted
4. Customizing Checks
Google Error Prone is a wonderful tool that can help us eliminate many bugs even before they are introduced into the code. However, it can be too harsh sometimes with our code. Let’s say we want to throw an exception for our empty function, but only for this one. We can add the SuppressWarnings annotation with the name of the check that we want to bypass:
@SuppressWarnings("EmptyMethodCheck")
public void emptyMethod() {}
Suppressing warnings is not recommended but might be needed in some cases, like when working with external libraries that do not implement the same code standards as our project.
In addition to this, we can control the severity of all checks using additional compiler arguments:
- -Xep:EmptyMethodCheck: turns on EmptyMethodCheck with the severity level from its BugPattern annotation
- -Xep:EmptyMethodCheck: OFF turns off EmptyMethodCheck check
- -Xep:EmptyMethodCheck: WARN turns on EmptyMethodCheck check as a warning
- -Xep:EmptyMethodCheck: ERROR turns on EmptyMethodCheck check as an error
We also have some blanket severity-changing flags that are global for all checks:
- -XepAllErrorsAsWarnings
- -XepAllSuggestionsAsWarnings
- -XepAllDisabledChecksAsWarnings
- -XepDisableAllChecks
- -XepDisableAllWarnings
- -XepDisableWarningsInGeneratedCode
We can also combine our custom compiler flags with the global ones:
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne -XepDisableAllChecks -Xep:EmptyMethodCheck:ERROR</arg>
</compilerArgs>
By configuring our compiler as above, we’ll disable all checks except the custom check we created.
5. Refactoring Code
A feature that separates our plugin from the rest of the static code analysis programs is the possibility of patching the codebase. Aside from identifying errors during our standard compilation phase, Error Prone can also provide suggested replacements. As we saw in subpoint 3, when Error Prone found the DeadException, it also suggested a fix for it:
Did you mean 'throw new IllegalArgumentException();'?
In this context, Error Prone recommends resolving this problem by adding the throw keyword. We can also use Error Prone to modify the source code with the suggested replacements. This is useful when first adding Error Prone enforcement to our existing codebase. To activate this, we need to add two compiler flags to our compiler invocation:
- –XepPatchChecks: followed by the checks that we want to patch. If a check does not suggest fixes, then it won’t do anything.
- -XepPatchLocation: the location where to generate the patch file containing the fixes
So, we can rewrite our compiler configuration like this:
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne -XepPatchChecks:DeadException,EmptyMethodCheck -XepPatchLocation:IN_PLACE</arg>
</compilerArgs>
We’ll tell the compiler to fix the DeadException and our custom EmptyMethodCheck. We set the location to IN_PLACE, meaning that it will apply the changes in our source code.
Now, if we run the maven clean verify command on a buggy class:
public class BuggyClass {
public static void main(String[] args) {
if (args.length == 0 || args[0] != null) {
new IllegalArgumentException();
}
}
public void emptyMethod() {
}
}
It will refactor the class:
public class BuggyClass {
public static void main(String[] args) {
if (args.length == 0 || args[0] != null) {
throw new IllegalArgumentException();
}
}
}
6. Conclusion
In summary, Error Prone is a versatile tool that combines effective error identification with customizable configurations. It empowers developers to enforce coding standards seamlessly and facilitates efficient code refactoring through automated suggested replacements. Overall, Error Prone is a valuable asset for enhancing code quality and streamlining the development process.
As always, the full code presented in this tutorial is available over on GitHub.