Skip to content

Add nullability annotations and compile-time checks #4557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 65 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
7468059
Create pre-compiled script plugin for NullAway and Error Prone
marcphilipp May 19, 2025
5238fec
Set up OSGi verification to ignore missing JSpecify
marcphilipp May 20, 2025
61b9978
Enable NullAway in junit-platform-commons
marcphilipp May 19, 2025
37224bb
Fix `ModularUserGuideTests`
marcphilipp May 20, 2025
d8b4286
Annotate nullability in `org.junit.platform.commons.logging` package
marcphilipp May 20, 2025
1bb8ab9
Annotate nullability in `org.junit.platform.commons.annotation` package
marcphilipp May 19, 2025
9094796
Annotate nullability in `org.junit.platform.commons.function` package
marcphilipp May 19, 2025
efa8be1
Annotate nullability in `org.junit.platform.commons.support.scanning`
marcphilipp May 20, 2025
116d904
Annotate nullability in `org.junit.platform.commons.support.conversion`
marcphilipp May 20, 2025
71530d7
Annotate nullability in `org.junit.platform.commons` package
marcphilipp May 19, 2025
b4926e1
Add `Try.getNonNull{OrThrow}` method to ease null handling
marcphilipp May 19, 2025
2202f3a
Annotate nullability in `org.junit.platform.commons.support` package
marcphilipp May 19, 2025
92f4ee1
Annotate nullability in `org.junit.platform.commons.util` package
marcphilipp May 20, 2025
d917a2c
Enable NullAway in junit-platform-engine
marcphilipp May 20, 2025
ed8b8ef
Annotate nullability in `org.junit.platform.engine.support.config`
marcphilipp May 20, 2025
383d577
Annotate nullability in `org.junit.platform.engine.support.descriptor`
marcphilipp May 20, 2025
0b128e4
Annotate nullability in `org.junit.platform.engine.support.discovery`
marcphilipp May 20, 2025
ee11a6c
Annotate nullability in `org.junit.platform.engine.support.hierarchical`
marcphilipp May 20, 2025
410793c
Annotate nullability in `org.junit.platform.engine.support.store`
marcphilipp May 20, 2025
ccbd996
Annotate nullability in `org.junit.platform.engine`
marcphilipp May 20, 2025
56f5e2d
Annotate nullability in `org.junit.platform.engine.discovery`
marcphilipp May 20, 2025
dcf835f
Annotate nullability in `org.junit.platform.engine.reporting`
marcphilipp May 20, 2025
f494ee0
Enable and fail on all compile warnings in test fixtures
marcphilipp May 20, 2025
eb83ac4
Annotate nullability in `org.junit.platform.fakes`
marcphilipp May 20, 2025
2dc7270
Treat root package and all subpackages as `@NullMarked`
marcphilipp May 20, 2025
493cc94
Enable NullAway in junit-platform-suite-api
marcphilipp May 20, 2025
9897085
Enable NullAway in junit-platform-suite-commons
marcphilipp May 20, 2025
fe2035f
Enable NullAway in junit-platform-suite-engine
marcphilipp May 20, 2025
7802b36
Enable NullAway in junit-vintage-engine
marcphilipp May 20, 2025
f12b036
Let NullAway handle assertions in test code
marcphilipp May 20, 2025
2cc1b8d
Enable NullAway in junit-platform-testkit
marcphilipp May 20, 2025
e5210e3
Use open-test-reporting snapshots
marcphilipp May 20, 2025
9f43b05
Enable NullAway in junit-platform-reporting
marcphilipp May 20, 2025
9257604
Enable NullAway in junit-platform-jfr
marcphilipp May 20, 2025
66a38d5
Enable NullAway in junit-platform-console
marcphilipp May 20, 2025
c170e31
Add ArchUnit check for `@NullMarked` on all packages
marcphilipp May 22, 2025
f95e26d
Annotate packages to support downstream classpath compilation
marcphilipp May 22, 2025
9ebd170
Move `@NullMarked` annotations to packages
marcphilipp May 22, 2025
c13a556
Enable NullAway in junit-platform-launcher
marcphilipp May 22, 2025
2f880cf
Enable NullAway in junit-jupiter-api
marcphilipp May 22, 2025
c98f595
Annotate nullability in `org.junit.jupiter.api.condition`
marcphilipp May 22, 2025
6949509
Annotate nullability in `org.junit.jupiter.api.extension.support`
marcphilipp May 22, 2025
3cb766e
Annotate nullability in `org.junit.jupiter.api.extension`
marcphilipp May 22, 2025
b37450d
Annotate nullability in `org.junit.jupiter.api.function`
marcphilipp May 22, 2025
1fb5c7f
Annotate nullability in `org.junit.jupiter.api.io`
marcphilipp May 22, 2025
54b6522
Annotate nullability in `org.junit.jupiter.api.parallel`
marcphilipp May 22, 2025
b3ca703
Annotate nullability in `org.junit.jupiter.api`
marcphilipp May 22, 2025
0b7b463
Check `org.junit.jupiter.api..`
marcphilipp May 22, 2025
169e5d7
Enable NullAway in junit-jupiter-engine
marcphilipp May 22, 2025
6b2a64a
Annotate nullability in `org.junit.jupiter.engine.config`
marcphilipp May 22, 2025
179b3e9
Annotate nullability in `org.junit.jupiter.engine`
marcphilipp May 22, 2025
63377d7
Annotate nullability in `org.junit.jupiter.engine.descriptor`
marcphilipp May 22, 2025
b6386c7
Check `org.junit.jupiter.engine..`
marcphilipp May 22, 2025
ddd17a9
Annotate nullability in remaining `org.junit.jupiter.engine..` packages
marcphilipp May 22, 2025
f38fb2d
Enable NullAway in junit-jupiter-migrationsupport
marcphilipp May 22, 2025
dc3ae6e
Enable NullAway in junit-jupiter-params
marcphilipp May 22, 2025
5587851
Add JSpecify to all module descriptors
marcphilipp May 22, 2025
2cccb29
Enable NullAway in tests
marcphilipp May 23, 2025
62797b0
Enable NullAway in documentation
marcphilipp May 23, 2025
c0bb36a
Annotate nullability in `org.junit.platform.commons.test`
marcphilipp May 23, 2025
5da1c40
Treat compiler warnings as errors for all source sets
marcphilipp May 23, 2025
aa121ee
Fix compilation of JMH benchmarks
marcphilipp May 23, 2025
02c8d81
Move `@Nullable` to return type for methods
marcphilipp May 23, 2025
d80c226
Fix IDEA-detected nullability problems in main source sets
marcphilipp May 23, 2025
6c11b2b
Add to release notes
marcphilipp May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ See [`ExtensionContext`](junit-jupiter-api/src/main/java/org/junit/jupiter/api/e
[`ParameterContext`](junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ParameterContext.java) for example Javadoc.


### Nullability

This project uses JSpecify's annotation to indicate nullability. In general, the approach
is as follows:

- The Gradle build is set up to treat all code as being `@NullMarked`
- The descriptor of each module is annotated with `@NullMarked` for IDEs such as IntelliJ
IDEA to treat code correctly.
Comment on lines +129 to +130
Copy link
Contributor

@scordio scordio Jun 7, 2025

Choose a reason for hiding this comment

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

@marcphilipp IIUC, this eventually didn't happen due to uber/NullAway#1083, and you instead went for annotating the packages, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, thank you! Updated in df24efb

- Fields, parameters, return types etc. may be annotated with `@Nullable`
- A package can be excluded (temporarily) using `@NullUnmarked`

### Tests

#### Naming
Expand Down
1 change: 1 addition & 0 deletions documentation/documentation.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ plugins {
alias(libs.plugins.plantuml)
id("junitbuild.build-parameters")
id("junitbuild.java-multi-release-test-sources")
id("junitbuild.java-nullability-conventions")
id("junitbuild.kotlin-library-conventions")
id("junitbuild.testing-conventions")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ repository on GitHub.
[[release-notes-6.0.0-M1-overall-new-features-and-improvements]]
==== New Features and Improvements

* ❓
* All JUnit modules now use https://jspecify.dev/[JSpecify]'s nullability annotations to
indicate which method parameters, return types, etc. can be `null`.


[[release-notes-6.0.0-M1-junit-platform]]
Expand Down
8 changes: 6 additions & 2 deletions documentation/src/main/java/example/util/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@

package example.util;

import static java.util.Objects.requireNonNull;

import org.jspecify.annotations.Nullable;

public class StringUtils {

public static boolean isPalindrome(String candidate) {
int length = candidate.length();
public static boolean isPalindrome(@Nullable String candidate) {
int length = requireNonNull(candidate).length();
for (int i = 0; i < length / 2; i++) {
if (candidate.charAt(i) != candidate.charAt(length - (i + 1))) {
return false;
Expand Down
4 changes: 4 additions & 0 deletions documentation/src/test/java/example/ClassTemplateDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.List;
import java.util.stream.Stream;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.ClassTemplate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ClassTemplateInvocationContext;
Expand All @@ -37,6 +38,9 @@ class ClassTemplateDemo {
// tag::custom_line_break[]
= unmodifiableList(Arrays.asList("apple", "banana", "lemon"));

//end::user_guide[]
@Nullable
//tag::user_guide[]
private String fruit;

@Test
Expand Down
2 changes: 1 addition & 1 deletion documentation/src/test/java/example/DynamicTestsDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class DynamicTestsDemo {
@TestFactory
// end::user_guide[]
@Tag("exclude")
DynamicTest dummy() { return null; }
DynamicTest dummy() { return dynamicTest("dummy", () -> {}); }
// tag::user_guide[]
List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
Expand Down
4 changes: 4 additions & 0 deletions documentation/src/test/java/example/FirstCustomEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.UncheckedIOException;
import java.net.ServerSocket;

import org.jspecify.annotations.Nullable;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
Expand All @@ -32,6 +33,9 @@
*/
public class FirstCustomEngine implements TestEngine {

//end::user_guide[]
@Nullable
//tag::user_guide[]
public ServerSocket socket;

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ void testWithExplicitArgumentConversion(

// end::explicit_conversion_example[]
static
@SuppressWarnings({ "NullableProblems", "NullAway" })
// tag::explicit_conversion_example_ToStringArgumentConverter[]
public class ToStringArgumentConverter extends SimpleArgumentConverter {

Expand All @@ -474,6 +475,7 @@ protected Object convert(Object source, Class<?> targetType) {
// end::explicit_conversion_example_ToStringArgumentConverter[]

static
@SuppressWarnings({ "NullableProblems", "NullAway", "ConstantValue" })
// tag::explicit_conversion_example_TypedArgumentConverter[]
public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {

Expand Down
4 changes: 4 additions & 0 deletions documentation/src/test/java/example/SecondCustomEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.io.UncheckedIOException;
import java.net.ServerSocket;

import org.jspecify.annotations.Nullable;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
Expand All @@ -32,6 +33,9 @@
*/
public class SecondCustomEngine implements TestEngine {

//end::user_guide[]
@Nullable
//tag::user_guide[]
public ServerSocket socket;

@Override
Expand Down
4 changes: 2 additions & 2 deletions documentation/src/test/java/example/TestingAStackDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
Expand All @@ -39,6 +37,8 @@ void isInstantiatedWithNew() {
@DisplayName("when new")
class WhenNew {

Stack<Object> stack;

@BeforeEach
void createNewStack() {
stack = new Stack<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.callbacks;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.defaultmethods;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.exception;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon
return HttpServer.class.equals(parameterContext.getParameter().getType());
}

//end::user_guide[]
@SuppressWarnings({ "DataFlowIssue", "NullAway" })
//tag::user_guide[]
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {

ExtensionContext rootContext = extensionContext.getRoot();
ExtensionContext.Store store = rootContext.getStore(Namespace.GLOBAL);
String key = HttpServerResource.class.getName();
Class<HttpServerResource> key = HttpServerResource.class;
HttpServerResource resource = store.getOrComputeIfAbsent(key, __ -> {
try {
HttpServerResource serverResource = new HttpServerResource(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package example.extensions;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand All @@ -19,6 +20,9 @@ class RandomNumberDemo {
// Use static randomNumber0 field anywhere in the test class,
// including @BeforeAll or @AfterEach lifecycle methods.
@Random
// end::user_guide[]
@Nullable
// tag::user_guide[]
private static Integer randomNumber0;

// Use randomNumber1 field in test methods and @BeforeEach
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.lang.reflect.Field;
import java.util.function.Predicate;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
Expand Down Expand Up @@ -69,7 +70,7 @@ public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) {
return this.random.nextInt();
}

private void injectFields(Class<?> testClass, Object testInstance,
private void injectFields(Class<?> testClass, @Nullable Object testInstance,
Predicate<Field> predicate) {

predicate = predicate.and(field -> isInteger(field.getType()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.extensions;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.interceptor;

import org.jspecify.annotations.NullMarked;
5 changes: 5 additions & 0 deletions documentation/src/test/java/example/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.nio.file.Path;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
Expand All @@ -20,6 +21,9 @@
//tag::user_guide[]
class DocumentationDemo {

//end::user_guide[]
@Nullable
//tag::user_guide[]
static Path lookUpDocsDir() {
// return path to docs dir
// end::user_guide[]
Expand All @@ -40,13 +44,13 @@ void generateDocumentation() {
class DocumentationExtension implements AfterEachCallback {

@SuppressWarnings("unused")
private final Path path;
private final @Nullable Path path;

private DocumentationExtension(Path path) {
private DocumentationExtension(@Nullable Path path) {
this.path = path;
}

static DocumentationExtension forPath(Path path) {
static DocumentationExtension forPath(@Nullable Path path) {
return new DocumentationExtension(path);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.registration;

import org.jspecify.annotations.NullMarked;
3 changes: 3 additions & 0 deletions documentation/src/test/java/example/session/HttpTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon
return HttpServer.class.equals(parameterContext.getParameter().getType());
}

//end::user_guide[]
@SuppressWarnings({ "DataFlowIssue", "NullAway" })
//tag::user_guide[]
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return extensionContext
Expand Down
5 changes: 5 additions & 0 deletions documentation/src/test/java/example/session/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.session;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

class SharedResourceDemo {

@SuppressWarnings({ "DataFlowIssue", "NullAway" })
//tag::user_guide[]
@Test
void runBothCustomEnginesTest() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.sharedresources;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.testinterface;

import org.jspecify.annotations.NullMarked;
5 changes: 5 additions & 0 deletions documentation/src/test/java/example/testkit/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.testkit;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@ public class TimingExtension implements BeforeTestExecutionCallback, AfterTestEx
private static final String START_TIME = "start time";

@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
public void beforeTestExecution(ExtensionContext context) {
getStore(context).put(START_TIME, System.currentTimeMillis());
}

//end::user_guide[]
@SuppressWarnings({ "DataFlowIssue", "NullAway" })
//tag::user_guide[]
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
public void afterTestExecution(ExtensionContext context) {
Method testMethod = context.getRequiredTestMethod();
long startTime = getStore(context).remove(START_TIME, long.class);
long duration = System.currentTimeMillis() - startTime;
Expand Down
5 changes: 5 additions & 0 deletions documentation/src/test/java/example/timing/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example.timing;

import org.jspecify.annotations.NullMarked;
5 changes: 5 additions & 0 deletions documentation/src/test/java/extensions/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package extensions;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.nio.file.Path;
import java.util.List;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand Down Expand Up @@ -53,6 +54,9 @@ static void beforeInvocation(TextFile textFile, @TempDir Path tempDir) throws Ex
textFile.path = Files.writeString(filePath, textFile.content);
}

//end::user_guide[]
@SuppressWarnings({ "DataFlowIssue", "NullAway" })
//tag::user_guide[]
@AfterParameterizedClassInvocation
static void afterInvocation(TextFile textFile) throws Exception {
var actualContent = Files.readString(textFile.path); // <3>
Expand All @@ -61,6 +65,9 @@ static void afterInvocation(TextFile textFile) throws Exception {
// File will be deleted automatically by @TempDir support
}

//end::user_guide[]
@SuppressWarnings({ "DataFlowIssue", "NullAway" })
//tag::user_guide[]
@Test
void test() {
assertTrue(Files.exists(textFile.path)); // <2>
Expand All @@ -75,6 +82,9 @@ static class TextFile {

final String fileName;
final String content;
// end::example[]
@Nullable
// tag::example[]
Path path;

TextFile(String fileName, String content) {
Expand Down
5 changes: 5 additions & 0 deletions documentation/src/test/java21/example/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@NullMarked
package example;

import org.jspecify.annotations.NullMarked;
Loading
Loading