Introduction to Error Prone
Error Prone is a static analysis tool for Java that catches common programming mistakes at compile-time. It extends the Java compiler with additional error checking and can automatically fix some issues.
Maven Configuration
<properties>
<errorprone.version>2.21.1</errorprone.version>
<errorprone.core.version>2.21.1</errorprone.core.version>
</properties>
<dependencies>
<!-- Error Prone Annotations -->
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<version>${errorprone.version}</version>
</dependency>
<!-- Error Prone Core (for custom checkers) -->
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.core.version}</version>
<scope>provided</scope>
</dependency>
<!-- Error Prone Test Helpers -->
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_test_helpers</artifactId>
<version>${errorprone.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<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>${errorprone.core.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Core Error Prone Rules
Nullness Annotations and Checks
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.CompileTimeConstant;
public class NullnessRules {
// ErrorProne will warn if @Nullable is not used appropriately
public @interface Nullable {}
public @interface NonNull {}
// Example 1: Nullable return value
@Nullable
public String findUserEmail(String userId) {
// ErrorProne will warn if this might return null without @Nullable
if (userId == null) {
return null;
}
return userRepository.findEmail(userId);
}
// Example 2: NonNull parameter
public void sendEmail(@NonNull String email, @NonNull String subject) {
// ErrorProne ensures callers don't pass null
if (email == null) {
throw new IllegalArgumentException("Email cannot be null");
}
emailService.send(email, subject);
}
// Example 3: Compile-time constant checking
public void logMessage(@CompileTimeConstant String message) {
// ErrorProne ensures this is called only with compile-time constants
logger.info(message);
}
// Usage examples that ErrorProne will catch:
public void problematicCode() {
// This would trigger ErrorProne warning:
// sendEmail(null, "Test"); // Null passed to non-null parameter
// This would trigger ErrorProne warning:
// String email = findUserEmail("123");
// email.toUpperCase(); // Possible null pointer dereference
// This is OK:
logMessage("This is a constant message");
// This would trigger ErrorProne warning:
// String dynamicMessage = "Dynamic: " + System.currentTimeMillis();
// logMessage(dynamicMessage); // Not a compile-time constant
}
}
Return Value Checks
public class ReturnValueRules {
// Methods annotated with @CheckReturnValue must use their return value
@CheckReturnValue
public String createUserName(String firstName, String lastName) {
return firstName + " " + lastName;
}
// Methods that can safely ignore return value
@CanIgnoreReturnValue
public StringBuilder appendLog(StringBuilder builder, String message) {
return builder.append("LOG: ").append(message);
}
// Immutable objects - ErrorProne will warn about mutation attempts
@Immutable
public static final class UserId {
private final String value;
public UserId(String value) {
this.value = value;
}
public String getValue() {
return value;
}
// ErrorProne will warn if we add a setter
// public void setValue(String value) { this.value = value; }
}
public void demonstrateReturnValueChecks() {
// ErrorProne will warn: Return value ignored
// createUserName("John", "Doe");
// Correct usage:
String userName = createUserName("John", "Doe");
System.out.println(userName);
// This is OK because of @CanIgnoreReturnValue
StringBuilder builder = new StringBuilder();
appendLog(builder, "Starting process");
// Also OK to use return value
builder = appendLog(builder, "Process completed");
}
}
Custom Error Prone Rules
Custom Bug Checker Implementation
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Matchers.*;
@BugPattern(
name = "LoggingStringConcatenation",
summary = "Avoid string concatenation in log statements",
explanation = "String concatenation in log statements can impact performance " +
"even when the log level is disabled. Use parameterized logging instead.",
severity = WARNING
)
public class LoggingStringConcatenationChecker extends BugChecker
implements BugChecker.MethodInvocationTreeMatcher {
private static final Matcher<ExpressionTree> LOG_METHOD =
anyOf(
instanceMethod().onDescendantOf("org.slf4j.Logger").namedAnyOf("debug", "info", "warn", "error"),
staticMethod().onClass("java.util.logging.Logger").namedAnyOf("fine", "finer", "finest", "info", "warning", "severe")
);
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!LOG_METHOD.matches(tree, state)) {
return Description.NO_MATCH;
}
// Check if first argument contains string concatenation
if (tree.getArguments().isEmpty()) {
return Description.NO_MATCH;
}
ExpressionTree firstArg = tree.getArguments().get(0);
if (containsStringConcatenation(firstArg)) {
return describeMatch(tree, createSuggestedFix(tree, state));
}
return Description.NO_MATCH;
}
private boolean containsStringConcatenation(ExpressionTree tree) {
String source = tree.toString();
// Simple heuristic: look for + operator in string context
return source.contains("+") &&
(source.contains("\"") || source.contains("'"));
}
private SuggestedFix createSuggestedFix(MethodInvocationTree tree, VisitorState state) {
// This is a simplified fix - real implementation would be more sophisticated
return SuggestedFix.builder()
.replace(tree, "/* TODO: Replace string concatenation with parameterized logging */")
.build();
}
}
Repository Pattern Checker
@BugPattern(
name = "RepositoryMethodNaming",
summary = "Repository method names should follow Spring Data conventions",
severity = WARNING
)
public class RepositoryMethodNamingChecker extends BugChecker
implements BugChecker.MethodTreeMatcher {
private static final Pattern SPRING_DATA_PATTERN = Pattern.compile(
"^(find|read|get|query|search|count|exists|delete|remove)" +
"(By|And|Or|OrderBy)[A-Z].*$"
);
private static final Matcher<Tree> REPOSITORY_INTERFACE =
isSubtypeOf("org.springframework.data.repository.Repository");
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!REPOSITORY_INTERFACE.matches(tree, state)) {
return Description.NO_MATCH;
}
String methodName = tree.getName().toString();
// Skip default methods and methods from super interfaces
if (tree.getModifiers().getFlags().contains(Modifier.DEFAULT) ||
tree.getModifiers().getFlags().contains(Modifier.STATIC)) {
return Description.NO_MATCH;
}
if (!SPRING_DATA_PATTERN.matcher(methodName).matches()) {
return buildDescription(tree)
.setMessage(String.format(
"Repository method '%s' doesn't follow Spring Data naming conventions. " +
"Expected pattern: (find|read|get|query|search|count|exists|delete|remove)(By|And|Or|OrderBy)...",
methodName
))
.addFix(SuggestedFix.builder()
.replace(tree.getName(), "/* TODO: Rename method following Spring Data conventions */")
.build())
.build();
}
return Description.NO_MATCH;
}
}
Resource Management Checker
@BugPattern(
name = "AutoCloseableResource",
summary = "AutoCloseable resources should be used in try-with-resources",
severity = WARNING
)
public class AutoCloseableResourceChecker extends BugChecker
implements BugChecker.VariableTreeMatcher {
private static final Matcher<Tree> AUTOCLOSEABLE_TYPE =
isSubtypeOf("java.lang.AutoCloseable");
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
if (!AUTOCLOSEABLE_TYPE.matches(tree, state)) {
return Description.NO_MATCH;
}
// Check if this is a local variable
Tree parent = state.getPath().getParentPath().getLeaf();
if (!(parent instanceof BlockTree)) {
return Description.NO_MATCH;
}
// Look for try-with-resources in the following statements
BlockTree block = (BlockTree) parent;
List<? extends StatementTree> statements = block.getStatements();
int variableIndex = statements.indexOf(tree);
if (variableIndex == -1 || variableIndex >= statements.size() - 1) {
return Description.NO_MATCH;
}
StatementTree nextStatement = statements.get(variableIndex + 1);
if (!isInTryWithResources(tree, nextStatement)) {
return buildDescription(tree)
.setMessage(String.format(
"AutoCloseable resource '%s' should be used in try-with-resources statement",
tree.getName()
))
.addFix(createTryWithResourcesFix(tree, block, variableIndex, state))
.build();
}
return Description.NO_MATCH;
}
private boolean isInTryWithResources(VariableTree variable, StatementTree statement) {
if (!(statement instanceof TryTree)) {
return false;
}
TryTree tryTree = (TryTree) statement;
return tryTree.getResources().stream()
.anyMatch(resource -> resource.toString().contains(variable.getName().toString()));
}
private SuggestedFix createTryWithResourcesFix(VariableTree variable, BlockTree block,
int variableIndex, VisitorState state) {
// Complex fix that would restructure the code
return SuggestedFix.builder()
.replace(block, "/* TODO: Convert to try-with-resources */")
.build();
}
}
Common Error Prone Patterns
Collection and Stream Rules
public class CollectionStreamRules {
// ErrorProne will detect common collection/stream mistakes
public void commonMistakes(List<String> items) {
// Bug: Collection.removeAll with singleton list
List<String> toRemove = Arrays.asList("bad-item");
items.removeAll(toRemove); // ErrorProne: prefer removeIf
// Better:
items.removeIf("bad-item"::equals);
// Bug: String.equals on enum
enum Status { ACTIVE, INACTIVE }
Status status = Status.ACTIVE;
if (status.equals("ACTIVE")) { // ErrorProne: comparing enum with string
// ...
}
// Better:
if (status == Status.ACTIVE) {
// ...
}
}
// ErrorProne: Boxed primitive used in collection
public void boxedPrimitiveIssue() {
Map<String, Integer> counts = new HashMap<>();
// This might trigger auto-boxing warnings
counts.put("key", new Integer(42)); // ErrorProne: unnecessary boxing
// Better:
counts.put("key", 42);
}
// ErrorProne: String comparison with ==
public void stringComparison(String input) {
if (input == "expected") { // ErrorProne: string comparison with ==
// ...
}
// Better:
if ("expected".equals(input)) {
// ...
}
}
}
Concurrency Rules
public class ConcurrencyRules {
private final Object lock = new Object();
private volatile boolean running = true;
// ErrorProne: synchronized method on non-final class
public synchronized void process() { // Warning: synchronized method can be broken by subclass
// ...
}
// Better: use private final lock object
public void processSafely() {
synchronized (lock) {
// ...
}
}
// ErrorProne: volatile array
private volatile int[] data = new int[10]; // Warning: volatile array doesn't make elements volatile
// Better: use AtomicIntegerArray or other thread-safe collection
private final AtomicIntegerArray safeData = new AtomicIntegerArray(10);
// ErrorProne: Wait without loop
public void waitForCondition() throws InterruptedException {
synchronized (lock) {
if (!running) {
lock.wait(); // Warning: wait() should be in a loop
}
}
}
// Better: wait in loop
public void waitForConditionProperly() throws InterruptedException {
synchronized (lock) {
while (!running) {
lock.wait();
}
}
}
}
Integration with Build Tools
Maven Configuration with Custom Rules
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-Xplugin:ErrorProne</arg>
<!-- Enable specific ErrorProne checks -->
<arg>-Xep:CollectionIncompatibleType:ERROR</arg>
<arg>-Xep:FutureReturnValueIgnored:WARN</arg>
<arg>-Xep:JdkObsolete:WARN</arg>
<arg>-Xep:MissingOverride:ERROR</arg>
<arg>-Xep:MutableConstantField:WARN</arg>
<arg>-Xep:NullAway:ERROR</arg>
<!-- Disable specific checks -->
<arg>-Xep:WildcardImport:OFF</arg>
<!-- Custom error checkers -->
<arg>-XepPatchChecks:LoggingStringConcatenation,RepositoryMethodNaming</arg>
<arg>-XepPatchLocation:IN_PLACE</arg>
</compilerArgs>
</configuration>
<dependencies>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</dependency>
</dependencies>
</plugin>
<!-- ErrorProne Maven Plugin for additional control -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-errorprone-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<compilerArgs>
<arg>-Xep:StringSplitter:WARN</arg>
<arg>-Xep:ClassCanBeStatic:WARN</arg>
<arg>-Xep:DefaultCharset:ERROR</arg>
</compilerArgs>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Gradle Configuration
plugins {
id 'net.ltgt.errorprone' version '3.0.1'
}
dependencies {
errorprone 'com.google.errorprone:error_prone_core:2.21.1'
errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1'
}
tasks.withType(JavaCompile) {
options.compilerArgs += [
'-Xplugin:ErrorProne',
'-Xep:CollectionIncompatibleType:ERROR',
'-Xep:FutureReturnValueIgnored:WARN',
'-Xep:MissingOverride:ERROR',
'-Xep:NullAway:ERROR',
'-Xep:WildcardImport:OFF',
'-XepExcludedPaths:.*/generated/.*'
]
}
errorprone {
disabledChecks = [
'AlmostJavadoc', // Too noisy
'ClassCanBeStatic' // We prefer inner classes for organization
]
}
Custom Rule Configuration
Error Prone Configuration File
# .errorprone.yml errorProne: checks: # Enable with custom configuration CollectionIncompatibleType: severity: ERROR FutureReturnValueIgnored: severity: WARN MissingOverride: severity: ERROR # Custom checks LoggingStringConcatenation: severity: WARN RepositoryMethodNaming: severity: ERROR # Disable checks AlmostJavadoc: disabled: true ClassCanBeStatic: disabled: true excludedPatterns: - ".*/generated/.*" - ".*/test/.*" general: trackUnusedSuppressions: true suggestSuppressions: false
Suppressing Warnings
public class SuppressionExamples {
@SuppressWarnings("FutureReturnValueIgnored")
public void ignoreFutureResult() {
// We're intentionally ignoring this future
executor.submit(() -> doBackgroundWork());
}
@SuppressWarnings("NullAway")
public String potentialNullReturn() {
// We've validated this won't be null in production
return systemPropertyThatMightBeNull();
}
// Custom suppression for our custom checkers
@SuppressWarnings("LoggingStringConcatenation")
public void performanceCriticalLogging() {
// This is in a performance-critical path where we've measured the impact
logger.info("Processing item " + itemId + " at " + System.currentTimeMillis());
}
// ErrorProne-specific suppression
@SuppressWarnings("Immutable")
public static class MutableButSafe {
private String value;
// This class is used in a single-threaded context
public void setValue(String value) {
this.value = value;
}
}
}
Testing Custom Rules
Testing Error Prone Checkers
public class LoggingStringConcatenationCheckerTest {
private final CompilationTestHelper compilationHelper =
CompilationTestHelper.newInstance(LoggingStringConcatenationChecker.class, getClass());
@Test
public void testStringConcatenationInLog() {
compilationHelper
.addSourceLines(
"Test.java",
"import org.slf4j.Logger;",
"import org.slf4j.LoggerFactory;",
"public class Test {",
" private static final Logger logger = LoggerFactory.getLogger(Test.class);",
" public void badMethod(String user) {",
" // BUG: Diagnostic contains: Avoid string concatenation in log statements",
" logger.info(\"User logged in: \" + user);",
" }",
" public void goodMethod(String user) {",
" logger.info(\"User logged in: {}\", user);",
" }",
"}")
.doTest();
}
@Test
public void testNoWarningForParameterizedLogging() {
compilationHelper
.addSourceLines(
"Test.java",
"import org.slf4j.Logger;",
"import org.slf4j.LoggerFactory;",
"public class Test {",
" private static final Logger logger = LoggerFactory.getLogger(Test.class);",
" public void goodMethod(String user, int count) {",
" logger.info(\"User {} processed {} items\", user, count);",
" }",
"}")
.doTest();
}
}
public class RepositoryMethodNamingCheckerTest {
private final CompilationTestHelper compilationHelper =
CompilationTestHelper.newInstance(RepositoryMethodNamingChecker.class, getClass());
@Test
public void testInvalidRepositoryMethodName() {
compilationHelper
.addSourceLines(
"UserRepository.java",
"import org.springframework.data.repository.Repository;",
"public interface UserRepository extends Repository<User, Long> {",
" // BUG: Diagnostic contains: doesn't follow Spring Data naming conventions",
" User fetchUserById(Long id);",
" // Valid method name",
" User findById(Long id);",
"}")
.doTest();
}
}
Integration with CI/CD
GitHub Actions Configuration
name: Error Prone Check on: [push, pull_request] jobs: errorprone: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Run Error Prone run: mvn compile -Perrorprone - name: Upload Error Prone Report uses: actions/upload-artifact@v3 if: always() with: name: errorprone-report path: target/errorprone-report/
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Error Prone Analysis') {
steps {
sh 'mvn errorprone:check'
}
post {
always {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target/site/errorprone',
reportFiles: 'index.html',
reportName: 'Error Prone Report'
])
}
}
}
}
}
Best Practices and Patterns
Effective Error Prone Usage
public class ErrorProneBestPractices {
// 1. Use @Nullable and @NonNull consistently
public @Nullable String findData(@NonNull String key) {
Objects.requireNonNull(key, "key cannot be null");
return cache.get(key);
}
// 2. Prefer immutable data structures
@Immutable
public static final class Configuration {
private final String host;
private final int port;
public Configuration(String host, int port) {
this.host = host;
this.port = port;
}
// No setters - immutable
}
// 3. Use try-with-resources for AutoCloseable
public void readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line);
}
}
}
// 4. Avoid string concatenation in performance-critical paths
public void efficientLogging(String userId, int count) {
// Good - parameterized logging
logger.debug("User {} processed {} items", userId, count);
// Avoid - string concatenation
// logger.debug("User " + userId + " processed " + count + " items");
}
// 5. Use @CheckReturnValue for methods with important return values
@CheckReturnValue
public ValidationResult validateInput(String input) {
return new ValidationResult(isValid(input));
}
// 6. Suppress warnings only when necessary and document why
@SuppressWarnings("FutureReturnValueIgnored")
public void fireAndForgetTask() {
// We intentionally ignore the future because this is a fire-and-forget operation
// and we handle errors through the task's own error handling
executor.submit(this::backgroundTask);
}
}
Conclusion
Error Prone provides powerful static analysis for Java with:
- Built-in Bug Detection - Hundreds of common bug patterns
- Custom Rule Creation - Extensible framework for domain-specific rules
- Build Integration - Seamless Maven and Gradle integration
- Automatic Fixes - Suggested fixes for many issues
- CI/CD Support - Integration with modern development workflows
By implementing custom rules tailored to your codebase and following Error Prone's recommendations, you can catch bugs early, maintain code quality, and enforce consistent coding standards across your organization.