Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions docs/errorprone_nullaway_integration_guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Guide for Integrating ErrorProne and NullAway
This guide outlines how to integrate ErrorProne and NullAway into your Java project. You can choose to implement basic ErrorProne analysis or extend it with NullAway for strict JSpecify nullability validation.

## Definitions
* **ErrorProne**: A Java compiler plugin created by Google that hooks into the compilation process to identify common bug patterns at compile-time.
* **NullAway**: A fast, low-overhead null-pointer checker designed as an ErrorProne plugin. It eliminates NullPointerExceptions (NPEs) by checking annotations on method signatures and variable declarations.
* **JSpecify**: A standard set of annotations (like `@NullMarked` and `@Nullable`) that define the nullness contracts for Java types. NullAway has a strict JSpecifyMode that aligns its rules with the JSpecify spec.

---

## 1. Basic ErrorProne Setup
ErrorProne hooks into the compilation process to identify bug patterns. This is configured directly inside the `maven-compiler-plugin`.

### Maven Configuration
Add ErrorProne to the `<annotationProcessorPaths>` of the `maven-compiler-plugin`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version> <!-- Or omit if managed by parent POM -->
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
```

### Gradle Configuration
```groovy
plugins {
id("net.ltgt.errorprone") version "${errorprone.plugin.version}"
}
dependencies {
errorprone("com.google.errorprone:error_prone_core:${errorprone.version}")
}
```

---

## 2. Adding NullAway
NullAway runs as an annotation processor plug-in inside ErrorProne. To configure it:
1. Add NullAway to the compiler's annotation processor paths.
2. Pass flags enabling NullAway and setting packages to scan.

### [Optional] Understanding NullAway JSpecify Mode
By default, NullAway checks nullability using legacy checking rules. Passing `-XepOpt:NullAway:JSpecifyMode=true` enables **JSpecify Mode**, which aligns NullAway's static analysis with the JSpecify 1.0 specifications:
* **Generics Nullability**: Enables checking annotations inside generic parameters (e.g. `List<@Nullable String>`).
* **Subclass Compatibility**: Enforces strict Liskov Substitution Principle compliance, ensuring subclasses do not accept fewer nulls or return more nulls than their parent class methods.
* **Array element checks**: Validates nullability contracts on array references and array type arguments.
* **Bytecode Symbol Dependency**: Checking generics and type-use nullability requires the compiler to preserve these type-use annotations in compiled bytecode symbols. Consequently, JSpecify Mode requires building on JDK 22+ or using OpenJDK 17/21 with the `-XDaddTypeAnnotationsToSymbol=true` compiler argument.

### Maven NullAway Setup
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version> <!-- Or omit if managed by parent POM -->
<configuration>
<fork>true</fork>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<!-- Enforces NullAway JSpecify violations -->
<arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=<your.package.prefix> -XepOpt:NullAway:JSpecifyMode=true</arg>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the maven-compiler-plugin configuration, passing multiple space-separated compiler arguments within a single <arg> tag will cause Maven to pass them as a single quoted argument to javac. This results in a compilation error (e.g., invalid flag). Each compiler argument must be defined in its own <arg> element.

Suggested change
<arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=<your.package.prefix> -XepOpt:NullAway:JSpecifyMode=true</arg>
<arg>-Xplugin:ErrorProne</arg>
<arg>-Xep:NullAway:ERROR</arg>
<arg>-XepOpt:NullAway:AnnotatedPackages=<your.package.prefix></arg>
<arg>-XepOpt:NullAway:JSpecifyMode=true</arg>

<arg>-XDshould-stop.ifError=FLOW</arg>
<arg>-XDaddTypeAnnotationsToSymbol=true</arg>

<!-- JDK 16+ exports (only when running on Java 16+) -->
<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>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>${nullaway.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
```

### Gradle NullAway Setup
```groovy
dependencies {
errorprone("com.uber.nullaway:nullaway:${nullaway.version}")
}
```
Configure the compiler plugin to run ErrorProne as a compiler plugin and NullAway as an annotation processor.
* **Classpath Note**: Ensure `error_prone_api` is on the classpath to prevent runtime failures.
* **Recommendation for Build Severity**: If your project has many existing violations, default to `WARN` initially. Upgrade to `ERROR` to break the build only after those issues are resolved.

---

## 3. Running On-Demand with Maven Profiles (Optional)
Because running deep nullness checks on every compile slows down build times, we encapsulate these checks into a Maven Profile to run them on-demand.

### Profile A: Strict Check (`nullaway`)
This profile enforces NullAway checks on annotated packages and breaks the build on any violation:
```xml
<profiles>
<profile>
<id>nullaway</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version> <!-- Or omit if managed by parent POM -->
<configuration>
<!-- Insert compiler configuration from Section 2 here -->
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
```

---

## 4. Verification & Testing

### Understanding NullAway Configuration Flags
* **`-Xep:NullAway:ERROR`** (Recommended Setting: `ERROR` / `WARN`): Set to `ERROR` to break the build on any nullability contract violation and try to resolve the flagged reports. If too many to be resolved, set to `WARN` to print warnings without failing the compilation.
* **`AnnotatedPackages`** (Recommended Setting: `<your.package.prefix>`): Required. A comma-separated list of package prefixes that NullAway scans. Anything outside these package prefixes will be ignored.
* **`JSpecifyMode`** (Recommended Setting: `true`): Required for JSpecify. Enforces strict JSpecify rules, including type-use checks, array type-argument checks, and subclass compatibility constraints.

### How to Verify Success
Run the compilation command:
```shell
mvn clean compile
```
*(or `mvn clean compile -Pnullaway` if using the profile)*

* **Success**: You see `BUILD SUCCESS`. This guarantees your code has no nullability violations within the annotated packages.
* **To verify the checks are actively running**:
1. Add an intentional bug method (introduce a temporary violation to one of your Java files).
2. Run the build with `mvn clean compile -X` and search the output logs for `-Xplugin:ErrorProne` or `NullAway`. If Maven is passing those arguments to the compiler, the checker is active.
* **Failure**: The compiler fails with clear error messages pointing to code lines. Ex:
`[ERROR] /path/to/File.java:[45,12] error: [NullAway] passing @Nullable parameter to a @NonNull method`

---

## 5. Supported JDK Versions & Compiler Requirements
When `JSpecifyMode` is enabled, NullAway requires the compiler to support reading type-use annotations from bytecode symbols:
* **JDK 22 or higher**: Fully supported natively.
* **JDK 17 and 21**: You must pass `-XDaddTypeAnnotationsToSymbol=true` in `compilerArgs`.
* **Important**: This flag is only supported by OpenJDK builds (e.g. Temurin, Zulu) as of release 21.0.8 / 17.0.19. It is not supported by Oracle JDK.
* **NullAway Version**: Use **`0.11.0` or higher** on JDK 21+ to avoid compiler crashes due to a known `NullPointerException` inside the analyzer's generics checks (e.g., `castToNonNull failed!` in `GenericsChecks`).
107 changes: 107 additions & 0 deletions docs/jspecify_automation_migration_playbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Automation & Migration Playbook
Onboarding a large codebase to strict JSpecify null safety requires a systematic approach. This document details the automation workflow and best practices.

---

## 1. Generated vs. Handwritten Code Handling
Before starting the migration, identify which parts of the codebase are generated vs. handwritten:
* **Handwritten Modules (GAX, Auth, parent modules)**: Apply the 3-step automation workflow below directly to the source tree.
* **Generated Modules**: Do not edit generated classes manually! Instead, update the code generator engine (`gapic-generator-java`) to output JSpecify annotations (`@NullMarked` on classes/packages and `@Nullable` on generic/nullable return methods) during its code generation phase.

---

## 2. The 3-Step Automation Workflow
Use these three parts in order to automate the mechanical tasks of JSpecify migration.

### Part 1: Bulk `@NullMarked` Annotation (Automated Script)
Use a python script to search Java source directories and inject `@NullMarked` at the package or class level:
* **Package-level**: Add to `package-info.java` files.
* **Class-level**: Prepend `@NullMarked` to top-level classes, interfaces, or enums.

*Example class-level injection pattern (Python)*:
```python
package_match = re.search(r"^(package\s+[\w\.]+;)", content, re.MULTILINE)
if package_match:
package_line = package_match.group(1)
content = content.replace(package_line, package_line + "\nimport org.jspecify.annotations.NullMarked;")
# Add @NullMarked before the class/interface/enum
content = re.sub(r"\n(public\s+)?(class|interface|enum)\s+", r"\n@NullMarked\n\2 ", content)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regular expression replacement string r"\n@NullMarked\n\2 " discards group 1 (public\s+)?. This will strip the public modifier from any public class, interface, or enum (e.g., converting public class MyClass to @NullMarked\nclass MyClass). Update the replacement pattern to include \1 to preserve the modifier.

Suggested change
content = re.sub(r"\n(public\s+)?(class|interface|enum)\s+", r"\n@NullMarked\n\2 ", content)
content = re.sub(r"\n(public\s+)?(class|interface|enum)\s+", r"\n@NullMarked\n\1\2 ", content)

```

---

### Part 2: Legacy Javax Migration (Automated Script)
Translate legacy annotations (e.g. `javax.annotation.Nullable`) to JSpecify `@Nullable`. If annotations are in declaration positions, programmatically reposition them to type-use positions:
```python
# Reposition from declaration-use to type-use position if necessary
content = re.sub(r"@Nullable\s+public\s+(\w+)", r"public @Nullable \1", content)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regular expression r"@Nullable\s+public\s+(\w+)" replaced with r"public @Nullable \1" will produce invalid Java code if other modifiers (such as static or final) are present. For example, @Nullable public static String would become public @Nullable static String, which is a compilation error because @Nullable is a type-use annotation and cannot annotate the static modifier. Additionally, this pattern misses other visibility modifiers like private or protected.

Using a more robust regex that matches any modifiers and places @Nullable immediately before the type name resolves this issue.

Suggested change
content = re.sub(r"@Nullable\s+public\s+(\w+)", r"public @Nullable \1", content)
content = re.sub(r"@Nullable\s+((?:public|protected|private|static|final)\s+)*(\w+)", r"\1@Nullable \2", content)

# Replace imports
content = content.replace("import javax.annotation.Nullable;", "import org.jspecify.annotations.Nullable;")
```

---

### Part 3: ErrorProne Auto-Patching (`nullaway-patch`)
For the remaining manual nullness checks, configure ErrorProne's built-in **Auto-Patching tool** to scan your project, trace assignments, and write suggested `@Nullable` annotations directly to your source files.

#### Declare Profile B: Auto-Patching (`nullaway-patch`)
Add this profile to your `pom.xml`:
```xml
<profile>
<id>nullaway-patch</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version> <!-- Or omit if managed by parent POM -->
<configuration>
<fork>true</fork>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-XDshould-stop.ifError=FLOW</arg>
<arg>-XDaddTypeAnnotationsToSymbol=true</arg>

<!-- Configures ErrorProne patch checkers to write modifications IN_PLACE using JSpecify annotations -->
<arg>-Xplugin:ErrorProne -XepDisableAllChecks -Xep:FieldMissingNullable:ERROR -Xep:ParameterMissingNullable:ERROR -Xep:ReturnMissingNullable:ERROR -Xep:EqualsMissingNullable:ERROR -XepPatchChecks:FieldMissingNullable,ParameterMissingNullable,ReturnMissingNullable,EqualsMissingNullable -XepPatchLocation:IN_PLACE -XepOpt:Nullness:DefaultNullnessAnnotation=org.jspecify.annotations.Nullable</arg>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the issue in the integration guide, passing multiple space-separated compiler arguments within a single <arg> tag will cause Maven to pass them as a single quoted argument to javac, leading to compilation failures. Each argument must be split into its own <arg> element.

Suggested change
<arg>-Xplugin:ErrorProne -XepDisableAllChecks -Xep:FieldMissingNullable:ERROR -Xep:ParameterMissingNullable:ERROR -Xep:ReturnMissingNullable:ERROR -Xep:EqualsMissingNullable:ERROR -XepPatchChecks:FieldMissingNullable,ParameterMissingNullable,ReturnMissingNullable,EqualsMissingNullable -XepPatchLocation:IN_PLACE -XepOpt:Nullness:DefaultNullnessAnnotation=org.jspecify.annotations.Nullable</arg>
<arg>-Xplugin:ErrorProne</arg>
<arg>-XepDisableAllChecks</arg>
<arg>-Xep:FieldMissingNullable:ERROR</arg>
<arg>-Xep:ParameterMissingNullable:ERROR</arg>
<arg>-Xep:ReturnMissingNullable:ERROR</arg>
<arg>-Xep:EqualsMissingNullable:ERROR</arg>
<arg>-XepPatchChecks:FieldMissingNullable,ParameterMissingNullable,ReturnMissingNullable,EqualsMissingNullable</arg>
<arg>-XepPatchLocation:IN_PLACE</arg>
<arg>-XepOpt:Nullness:DefaultNullnessAnnotation=org.jspecify.annotations.Nullable</arg>


<!-- JDK 16+ exports (only when running on Java 16+) -->
<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>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>${nullaway.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</profile>
```
**Command to execute:**
```shell
mvn clean compile -Pnullaway-patch
```
*(Verify changes using `git diff` after compilation)*

---

## 3. Reference
https://buganizer.corp.google.com/issues/341380807
Loading