Skip to content

Commit c901fcc

Browse files
committed
Add support for checking nullability of test code
Closes gh-11
1 parent 960ee80 commit c901fcc

File tree

6 files changed

+128
-23
lines changed

6 files changed

+128
-23
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A Gradle plugin for verifying a Spring project's nullability at compile time. Gr
88

99
The plugin can then be applied to a project in the usual manner, as shown in the following example:
1010

11-
```
11+
```groovy
1212
plugins {
1313
id "io.spring.nullability" version "<<version>>"
1414
}
@@ -23,9 +23,31 @@ It will verify the nullability of all `JavaCompile` tasks whose name matches `co
2323
A `nullability` extension is added by the plugin.
2424
The extension can be used to customize the versions of the dependencies that are used during verification.
2525

26-
```
26+
```groovy
2727
nullability {
2828
errorProneVersion = <<custom-version>>
2929
nullAwayVersion = <<custom-version>>
3030
}
3131
```
32+
33+
## Types of Nullability Checking
34+
35+
The plugin supports two types of nullability checking, `main` and `tests`.
36+
The former is applied by default to tasks whose name matches `compile(\\d+)?Java`.
37+
The latter is opt-in.
38+
39+
Checking using `tests` differs from `main`. It adds support for AssertJ's custom `Contract` annotation and enables NullAway's `HandleTestAssertionLibraries` option.
40+
41+
## Configuring Nullability Checking
42+
43+
The plugin adds a `nullability` extension to the `options` of all `JavaCompile` tasks.
44+
The extension provides a single property, `checking`, that can be used to configure the type of null checking that is performed.
45+
It defaults to `main` for tasks whose name matches `compile(\\d+)?Java`.
46+
For all other `JavaCompile` tasks it defaults to `disabled`.
47+
48+
The following enables `tests` nullability checking for `compileTestJava`:
49+
50+
```groovy
51+
tasks.named("compileTestJava") {
52+
options.nullability.checking = "tests"
53+
}

src/main/java/io/spring/gradle/nullability/NullabilityOptions.java

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@
1616

1717
package io.spring.gradle.nullability;
1818

19+
import java.util.ArrayList;
1920
import java.util.Collections;
2021
import java.util.LinkedHashMap;
22+
import java.util.List;
23+
import java.util.Locale;
2124
import java.util.Map;
2225

2326
import javax.inject.Inject;
2427

2528
import net.ltgt.gradle.errorprone.CheckSeverity;
2629
import net.ltgt.gradle.errorprone.ErrorProneOptions;
2730
import org.gradle.api.provider.Property;
31+
import org.gradle.api.provider.Provider;
2832
import org.gradle.api.tasks.compile.JavaCompile;
2933

3034
/**
@@ -41,41 +45,61 @@ public abstract class NullabilityOptions {
4145
*/
4246
@Inject
4347
public NullabilityOptions(ErrorProneOptions errorProne) {
44-
errorProne.getEnabled().set(getChecking().map((checking) -> checking != Checking.DISABLED));
45-
errorProne.getDisableAllChecks().set(getChecking().map((checking) -> checking != Checking.DISABLED));
46-
errorProne.getCheckOptions().putAll(getChecking().map((checking) -> {
47-
Map<String, String> options = new LinkedHashMap<>();
48-
if (checking == Checking.MAIN) {
49-
options.put("NullAway:OnlyNullMarked", "true");
50-
options.put("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract");
51-
options.put("NullAway:JSpecifyMode", "true");
52-
}
53-
return options;
54-
}));
55-
errorProne.getChecks().putAll(getChecking().map((checking) -> {
56-
if (checking == Checking.MAIN) {
57-
return Map.of("NullAway", CheckSeverity.ERROR);
58-
}
48+
Provider<Checking> checkingAsEnum = getChecking()
49+
.map((string) -> Checking.valueOf(string.toUpperCase(Locale.ROOT)));
50+
errorProne.getEnabled().set(checkingAsEnum.map((checking) -> checking != Checking.DISABLED));
51+
errorProne.getDisableAllChecks().set(checkingAsEnum.map((checking) -> checking != Checking.DISABLED));
52+
errorProne.getCheckOptions().putAll(checkingAsEnum.map(this::checkOptions));
53+
errorProne.getChecks().putAll(checkingAsEnum.map(this::checks));
54+
}
55+
56+
private Map<String, String> checkOptions(Checking checking) {
57+
if (checking == Checking.DISABLED) {
5958
return Collections.emptyMap();
60-
}));
59+
}
60+
Map<String, String> options = new LinkedHashMap<>();
61+
options.put("NullAway:OnlyNullMarked", "true");
62+
List<String> customContractAnnotations = new ArrayList<>();
63+
customContractAnnotations.add("org.springframework.lang.Contract");
64+
if (checking == Checking.TESTS) {
65+
customContractAnnotations.add("org.assertj.core.internal.annotation.Contract");
66+
}
67+
options.put("NullAway:CustomContractAnnotations", String.join(",", customContractAnnotations));
68+
options.put("NullAway:JSpecifyMode", "true");
69+
if (checking == Checking.TESTS) {
70+
options.put("NullAway:HandleTestAssertionLibraries", "true");
71+
}
72+
return options;
73+
}
74+
75+
private Map<String, CheckSeverity> checks(Checking checking) {
76+
if (checking != Checking.DISABLED) {
77+
return Map.of("NullAway", CheckSeverity.ERROR);
78+
}
79+
return Collections.emptyMap();
6180
}
6281

6382
/**
6483
* Returns the type of checking to perform.
6584
* @return the type of checking
6685
*/
67-
public abstract Property<Checking> getChecking();
86+
public abstract Property<String> getChecking();
6887

6988
/**
70-
* The type of checking to perform for the {@link JavaCompile} task.
89+
* The type of null checking to perform for the {@link JavaCompile} task.
7190
*/
72-
public enum Checking {
91+
enum Checking {
7392

7493
/**
7594
* Main code nullability checking is performed.
7695
*/
7796
MAIN,
7897

98+
/**
99+
* Test code nullability checking is performed.
100+
*/
101+
TESTS,
102+
79103
/**
80104
* Nullability checking is disabled.
81105
*/

src/main/java/io/spring/gradle/nullability/NullabilityPlugin.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ private void configureJavaCompilation(Project project) {
6464
.getByType(ErrorProneOptions.class);
6565
NullabilityOptions nullabilityOptions = ((ExtensionAware) javaCompile.getOptions()).getExtensions()
6666
.create("nullability", NullabilityOptions.class, errorProneOptions);
67-
nullabilityOptions.getChecking().set(compilesMainSources(javaCompile) ? Checking.MAIN : Checking.DISABLED);
67+
nullabilityOptions.getChecking()
68+
.set(compilesMainSources(javaCompile) ? Checking.MAIN.name() : Checking.DISABLED.name());
6869
});
6970
}
7071

src/test/java/io/spring/gradle/nullability/NullabilityPluginIntegrationTests.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,20 @@ void configuresErrorProneOnCompileJava() {
7474
}
7575

7676
@Test
77-
void disablesErrorProneOnCompileTestJava() {
77+
void disablesErrorProneOnCompileTestJavaByDefault() {
7878
BuildResult result = this.gradleBuild.build("checkCompileTestJava");
7979
assertThat(result.getOutput()).contains("-XepCompilingTestOnlyCode");
8080
}
8181

82+
@Test
83+
void configuresErrorProneOnCompileTestJavaWhenEnabled() {
84+
BuildResult result = this.gradleBuild.build("checkCompileTestJava");
85+
assertThat(result.getOutput()).contains(
86+
"-XepDisableAllChecks -XepCompilingTestOnlyCode -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true "
87+
+ "-XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract,org.assertj.core.internal.annotation.Contract "
88+
+ "-XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:HandleTestAssertionLibraries=true");
89+
}
90+
8291
@Test
8392
void compileFailsForNullabilityViolationInMainCode() throws IOException {
8493
writeSource("main");
@@ -93,6 +102,13 @@ void compileSucceedsForNullabilityViolationInTestCode() {
93102
assertThat(result.task(":compileTestJava").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
94103
}
95104

105+
@Test
106+
void compileFailsForNullabilityViolationInTestCodeWhenCheckingIsEnabled() throws IOException {
107+
writeSource("test");
108+
BuildResult result = this.gradleBuild.prepareRunner("compileTestJava").buildAndFail();
109+
assertThat(result.getOutput()).contains("[NullAway] assigning @Nullable expression to @NonNull field");
110+
}
111+
96112
private void writeSource(String sourceSetName) {
97113
Path projectDir = this.gradleBuild.getProjectDir().toPath();
98114
Path pkg = projectDir.resolve("src/%s/java/com/example".formatted(sourceSetName));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
id "io.spring.nullability"
3+
id "java"
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
testCompileOnly("org.jspecify:jspecify:1.0.0")
12+
}
13+
14+
tasks.named("compileTestJava") {
15+
options.nullability.checking = "tests"
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import io.spring.gradle.nullability.NullabilityOptions.Checking
2+
3+
plugins {
4+
id "io.spring.nullability"
5+
id "java"
6+
}
7+
8+
repositories {
9+
mavenCentral()
10+
}
11+
12+
dependencies {
13+
compileOnly("org.jspecify:jspecify:1.0.0")
14+
15+
testCompileOnly("org.jspecify:jspecify:1.0.0")
16+
}
17+
18+
tasks.register("checkCompileTestJava") {
19+
doFirst {
20+
println compileTestJava.options.extensions.errorprone
21+
}
22+
}
23+
24+
tasks.named("compileTestJava") {
25+
options.nullability.checking = "tests"
26+
}

0 commit comments

Comments
 (0)