Skip to content
Merged
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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,56 @@ All configuration parameters can be set either in the plugin `<configuration>` b
| `nullAwaySeverity` | `nullability.nullAwaySeverity` | `error` | Severity for the NullAway check: `error`, `warn`, or `off` |
| `requireExplicitNullMarkingSeverity` | `nullability.requireExplicitNullMarkingSeverity` | `error` | Severity for the `RequireExplicitNullMarking` check: `error`, `warn`, or `off` |
| `addTypeAnnotationsToSymbol` | `nullability.addTypeAnnotationsToSymbol` | `true` | Add `-XDaddTypeAnnotationsToSymbol=true` to javac when JSpecify mode is on. Set to `false` if your JDK does not support this flag (e.g., Oracle JDK) |
| `nullAwayOptions` | `nullability.nullAwayOptions.<name>` | | Additional NullAway options, passed as `-XepOpt:NullAway:<name>=<value>` (see [Setting arbitrary NullAway options](#setting-arbitrary-nullaway-options)) |
| `skip` | `nullability.skip` | `false` | Skip the plugin |

Since NullAway 0.12.11, any annotation with the simple name `@Contract` is automatically recognized regardless of package (e.g. `org.springframework.lang.Contract`, `org.assertj.core.internal.annotation.Contract`). The `customContractAnnotations` parameter is only needed when:

- Using a contract annotation whose simple name is not `Contract`
- Using an older NullAway version (pre-0.12.11) that requires explicit registration

### Setting arbitrary NullAway options

NullAway has many [options](https://github.com/uber/NullAway/wiki/Configuration) that this plugin does not expose as dedicated parameters. Any of them can be set with `nullAwayOptions`, using the option name without the `-XepOpt:NullAway:` prefix as the element name:

```xml
<plugin>
<groupId>am.ik.maven</groupId>
<artifactId>nullability-maven-plugin</artifactId>
<version>0.4.3</version>
<extensions>true</extensions>
<configuration>
<nullAwayOptions>
<KnownInitializers>com.example.api.SomeClass.init</KnownInitializers>
<TreatGeneratedAsUnannotated>true</TreatGeneratedAsUnannotated>
</nullAwayOptions>
</configuration>
<executions>
<execution>
<goals>
<goal>configure</goal>
</goals>
</execution>
</executions>
</plugin>
```

The options above are appended to the ErrorProne argument as `-XepOpt:NullAway:KnownInitializers=com.example.api.SomeClass.init -XepOpt:NullAway:TreatGeneratedAsUnannotated=true`, for both main and test compilation.

The same options can be set as Maven properties by prefixing the option name with `nullability.nullAwayOptions.`:

```xml
<properties>
<nullability.nullAwayOptions.KnownInitializers>com.example.api.SomeClass.init</nullability.nullAwayOptions.KnownInitializers>
</properties>
```

An entry in the plugin `<configuration>` wins over the property with the same option name, and both win over the option that the plugin derives from the other parameters (for example `<CustomContractAnnotations>` overrides `customContractAnnotations`).

Option names and values must not contain whitespace: all options are appended to a single `-Xplugin:ErrorProne` argument. The build fails with an explicit message if they do.

Note that adding `-XepOpt:NullAway:...` to the `<compilerArgs>` of `maven-compiler-plugin` does not work: javac rejects it as an invalid flag unless it is part of the `-Xplugin:ErrorProne` argument.

### `generate-package-info` goal configuration

The `generate-package-info` goal accepts the following additional parameters:
Expand Down
51 changes: 51 additions & 0 deletions src/it/errorprone-arg-element-name/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>errorprone-arg-element-name</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<compilerArgs>
<!-- compilerArgs is a plain list, so any element name is accepted -->
<compilerArg>-Xplugin:ErrorProne -Xep:MissingOverride:ERROR</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>am.ik.maven</groupId>
<artifactId>nullability-maven-plugin</artifactId>
<version>@project.version@</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>configure</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example;

import org.jspecify.annotations.NullMarked;

@NullMarked
public class Greeter {

private final String name;

public Greeter(String name) {
this.name = name;
}

public String greet() {
return "Hello, " + this.name + "!";
}

}
4 changes: 4 additions & 0 deletions src/it/errorprone-arg-element-name/verify.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def buildLog = new File(basedir, "build.log").text
assert buildLog.contains("[nullability] Configuring ErrorProne") : "Plugin should log configuration message"
// A duplicated -Xplugin:ErrorProne argument makes javac fail with "plug-in not found: ErrorProne"
assert buildLog.contains("BUILD SUCCESS") : "Build should succeed with an ErrorProne argument declared as <compilerArg>"
50 changes: 50 additions & 0 deletions src/it/nullaway-options/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>nullaway-options</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
<plugin>
<groupId>am.ik.maven</groupId>
<artifactId>nullability-maven-plugin</artifactId>
<version>@project.version@</version>
<extensions>true</extensions>
<configuration>
<nullAwayOptions>
<KnownInitializers>com.example.Service.init</KnownInitializers>
</nullAwayOptions>
</configuration>
<executions>
<execution>
<goals>
<goal>configure</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
20 changes: 20 additions & 0 deletions src/it/nullaway-options/src/main/java/com/example/Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example;

import org.jspecify.annotations.NullMarked;

@NullMarked
public class Service {

private String greeting;

// Without -XepOpt:NullAway:KnownInitializers, NullAway reports that the field
// 'greeting' is not initialized.
public void init() {
this.greeting = "Hello";
}

public String greet(String name) {
return this.greeting + ", " + name + "!";
}

}
3 changes: 3 additions & 0 deletions src/it/nullaway-options/verify.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def buildLog = new File(basedir, "build.log").text
assert buildLog.contains("[nullability] Configuring ErrorProne") : "Plugin should log configuration message"
assert buildLog.contains("BUILD SUCCESS") : "KnownInitializers passed via nullAwayOptions should make the build succeed"
13 changes: 11 additions & 2 deletions src/main/java/am/ik/maven/nullability/CompilerConfigurer.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,17 @@ static String extractOptionPrefix(String option) {
return option;
}

/**
* Finds an existing compiler argument starting with the given prefix. Every child
* element is inspected because {@code compilerArgs} is a plain list: the
* {@code maven-compiler-plugin} accepts any element name (commonly {@code <arg>}, but
* {@code <compilerArg>} is used as well) for its items.
* @param compilerArgs the {@code compilerArgs} element
* @param prefix the argument prefix to look for
* @return the matching element, or {@code null} if there is none
*/
private static Xpp3Dom findArgByPrefix(Xpp3Dom compilerArgs, String prefix) {
for (Xpp3Dom child : compilerArgs.getChildren("arg")) {
for (Xpp3Dom child : compilerArgs.getChildren()) {
if (child.getValue() != null && child.getValue().startsWith(prefix)) {
return child;
}
Expand Down Expand Up @@ -283,7 +292,7 @@ private static Xpp3Dom getOrCreateChild(Xpp3Dom parent, String name) {
}

private static void addArgIfAbsent(Xpp3Dom compilerArgs, String value) {
for (Xpp3Dom child : compilerArgs.getChildren("arg")) {
for (Xpp3Dom child : compilerArgs.getChildren()) {
if (value.equals(child.getValue())) {
return;
}
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/am/ik/maven/nullability/ConfigureMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package am.ik.maven.nullability;

import java.util.Map;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
Expand Down Expand Up @@ -114,6 +116,18 @@ public ConfigureMojo() {
@Parameter(property = "nullability.requireExplicitNullMarkingSeverity", defaultValue = "error")
private String requireExplicitNullMarkingSeverity;

/**
* Additional NullAway options keyed by option name. Each entry is passed to
* ErrorProne as {@code -XepOpt:NullAway:<name>=<value>}, so that any NullAway option
* can be set without configuring the {@code maven-compiler-plugin} by hand. An entry
* overrides the option of the same name derived from the other parameters. Option
* names and values must not contain whitespace.
*
* @since 0.5.0
*/
@Parameter
private Map<String, String> nullAwayOptions;

/**
* Whether to skip the plugin execution.
*/
Expand Down
50 changes: 37 additions & 13 deletions src/main/java/am/ik/maven/nullability/NullAwayArgsBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@
package am.ik.maven.nullability;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Builds the {@code -Xplugin:ErrorProne} argument string for main or test compilation.
*/
public final class NullAwayArgsBuilder {

/**
* Prefix of a NullAway option passed to ErrorProne.
*/
static final String NULLAWAY_OPTION_PREFIX = "-XepOpt:NullAway:";

private NullAwayArgsBuilder() {
}

Expand Down Expand Up @@ -53,19 +60,8 @@ public static String build(boolean forTests, NullabilityConfiguration config) {
*/
static List<String> buildNullAwayOptions(boolean forTests, NullabilityConfiguration config) {
List<String> options = new ArrayList<>();
options.add("-XepOpt:NullAway:OnlyNullMarked=true");
options.add("-XepOpt:NullAway:CheckContracts=true");
if (config.jspecifyMode()) {
options.add("-XepOpt:NullAway:JSpecifyMode=true");
}

if (config.customContractAnnotations() != null && !config.customContractAnnotations().isEmpty()) {
options.add("-XepOpt:NullAway:CustomContractAnnotations=" + config.customContractAnnotations());
}

if (forTests) {
options.add("-XepOpt:NullAway:HandleTestAssertionLibraries=true");
}
buildNullAwayOptionMap(forTests, config)
.forEach((name, value) -> options.add(NULLAWAY_OPTION_PREFIX + name + "=" + value));

options.add("-Xep:NullAway:" + config.nullAwaySeverity().name());

Expand All @@ -81,6 +77,34 @@ static List<String> buildNullAwayOptions(boolean forTests, NullabilityConfigurat
return options;
}

/**
* Builds the {@code -XepOpt:NullAway:*} options keyed by option name. The options
* configured via {@link NullabilityConfiguration#nullAwayOptions()} are applied last
* so that they override the ones derived from the other parameters.
* @param forTests whether this is for test compilation
* @param config the nullability configuration
* @return the NullAway options keyed by option name, in emission order
*/
private static Map<String, String> buildNullAwayOptionMap(boolean forTests, NullabilityConfiguration config) {
Map<String, String> options = new LinkedHashMap<>();
options.put("OnlyNullMarked", "true");
options.put("CheckContracts", "true");
if (config.jspecifyMode()) {
options.put("JSpecifyMode", "true");
}

if (config.customContractAnnotations() != null && !config.customContractAnnotations().isEmpty()) {
options.put("CustomContractAnnotations", config.customContractAnnotations());
}

if (forTests) {
options.put("HandleTestAssertionLibraries", "true");
}

options.putAll(config.nullAwayOptions());
return options;
}

static String buildExcludedPaths(boolean forTests, NullabilityConfiguration config) {
List<String> patterns = new ArrayList<>();
if (!forTests) {
Expand Down
Loading