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

Conversation

marcphilipp
Copy link
Member

@marcphilipp marcphilipp commented May 20, 2025

Overview

This PR uses JSpecify's annotations to annotate packages as @NullMarked which implies fields, parameters, return types etc. in those package are not nullable by default. If null is allowed, the @Nullable annotation is used to indicate that.

At compile-time Error Prone with the NullAway plugin is used to verify consistency of the annotations causing the build to fail, for example, if null is passed as a parameter where it isn't allowed.


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@marcphilipp marcphilipp self-assigned this May 20, 2025
@sormuras
Copy link
Member

This PR uses JSpecify's annotations to annotate packages as @NullMarked which implies fields, parameters, return types etc. in those package are not nullable by default. If null is allowed, the @Nullable annotation is used to indicate that.

Would it make sense to annotate modules instead of packages with @NullMarked instead?

@marcphilipp marcphilipp force-pushed the marc/4550-nullability-checks branch from f61aa87 to f8f1e3d Compare May 20, 2025 09:45
@marcphilipp
Copy link
Member Author

Would it make sense to annotate modules instead of packages with @NullMarked instead?

Yes, I think that's a good idea! I went package by package but when a module is done moving the annotation to the module declaration makes sense. 👍

I'll give that a shot.

@marcphilipp
Copy link
Member Author

Unfortunately, NullAway does not support that:

@marcphilipp marcphilipp force-pushed the marc/4550-nullability-checks branch from f8f1e3d to e2cd369 Compare May 20, 2025 10:28
@marcphilipp
Copy link
Member Author

I think a found a good "interim" solution:

  • For NullAway, the config is now in the build setup because it doesn't yet support module descriptors
  • For IntelliJ, the config is now in the module descriptors

requires static transitive org.apiguardian.api;
requires static transitive org.jspecify;
Copy link
Member Author

Choose a reason for hiding this comment

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

We should discuss whether this is the right "scope". It requires downstream projects to have the JSpecify jar on their module path (should they be compiled using the module path).

Copy link
Member Author

Choose a reason for hiding this comment

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

requires static would make it optional, but we'd have to suppress warnings like these.

Copy link
Member

Choose a reason for hiding this comment

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

Mh, yeah. It ships with a compiled module descriptor, though: org.jspecify

@marcphilipp marcphilipp force-pushed the marc/4550-nullability-checks branch 2 times, most recently from e950738 to 914277e Compare May 20, 2025 11:28

tasks.withType<JavaCompile>().configureEach {
options.errorprone {
disableAllChecks = true
Copy link
Member Author

Choose a reason for hiding this comment

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

We can later (after this PR) enable additional checks provided by Error Prone but I didn't want to mix those changes into this already large set of changes.

@marcphilipp
Copy link
Member Author

I'll put annotations back on the packages after the discussion in uber/NullAway#1083 (comment).

@marcphilipp marcphilipp force-pushed the marc/4550-nullability-checks branch from f3fca05 to 11adeb2 Compare May 22, 2025 06:24
Comment on lines 83 to 102
@ArchTest
void packagesShouldBeNullMarked(JavaClasses classes) {
var exclusions = Stream.of( //
"..shadow..", //
"org.junit.jupiter.api..", //
"org.junit.jupiter.engine..", //
"org.junit.jupiter.migrationsupport..", //
"org.junit.jupiter.params..", //
"org.junit.platform.launcher.." //
).map(PackageMatcher::of).toList();

var subpackages = Stream.of("org.junit.platform", "org.junit.jupiter", "org.junit.vintage") //
.map(classes::getPackage) //
.flatMap(rootPackage -> rootPackage.getSubpackagesInTree().stream()) //
.filter(pkg -> exclusions.stream().noneMatch(it -> it.matches(pkg.getName()))) //
.filter(pkg -> !pkg.getClasses().isEmpty()) //
.toList();
assertThat(subpackages).isNotEmpty();

var violations = subpackages.stream() //
.filter(pkg -> !pkg.isAnnotatedWith(NullMarked.class)) //
.map(JavaPackage::getName) //
.sorted();
assertThat(violations).describedAs("The following packages are missing the @NullMarked annotation").isEmpty();
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This check will help us to avoid forgetting to add the annotation on new packages.

@marcphilipp marcphilipp force-pushed the marc/4550-nullability-checks branch 6 times, most recently from cd8776e to 29ede48 Compare May 22, 2025 15:37
@marcphilipp marcphilipp force-pushed the marc/4550-nullability-checks branch from 29ede48 to 834e903 Compare May 23, 2025 13:38
@marcphilipp marcphilipp force-pushed the marc/4550-nullability-checks branch from 834e903 to aa121ee Compare May 23, 2025 13:55
@marcphilipp marcphilipp marked this pull request as ready for review May 23, 2025 14:49
@marcphilipp marcphilipp merged commit 261c2f5 into main May 23, 2025
13 checks passed
@marcphilipp marcphilipp deleted the marc/4550-nullability-checks branch May 23, 2025 15:06
@@ -565,7 +566,7 @@ static boolean isWideningConversion(Class<?> sourceType, Class<?> targetType) {
* @return the corresponding wrapper type or {@code null} if the
* supplied type is {@code null} or not a primitive type
*/
public static Class<?> getWrapperType(Class<?> type) {
public static @Nullable Class<?> getWrapperType(Class<?> type) {
Copy link
Contributor

@scordio scordio May 24, 2025

Choose a reason for hiding this comment

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

@marcphilipp given that the Javadoc mentions:

* @return the corresponding wrapper type or {@code null} if the
* supplied type is {@code null} or not a primitive type

should the parameter also have @Nullable?

public static @Nullable Class<?> getWrapperType(@Nullable Class<?> type)

(I spotted it during the rebase of #4219)

After a quick look, I think there are no unit tests for getWrapperType() in ReflectionUtilsTests.

Happy to submit a PR to address both topics, if you agree 🙂

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! On main, there are no usages that potentially pass null, are there? Do you need that for #4219? If so, adding @Nullable along with tests sounds good to me. If not, I think we should rather change the Javadoc.

Copy link
Contributor

Choose a reason for hiding this comment

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

From my branch:

if (type == null) { // FIXME parameter of ReflectionUtils.getWrapperType should be @Nullable
return null;
}
Class<?> wrapperType = ReflectionUtils.getWrapperType(type);

triggered this, which is somehow related to the following on main:

I try to figure out why the warning doesn't happen on main 🤔

Comment on lines +129 to +130
- The descriptor of each module is annotated with `@NullMarked` for IDEs such as IntelliJ
IDEA to treat code correctly.
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add JSpecify nullability annotations to Java APIs
3 participants