Skip to content
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

WICKET-7145: added @Nonnull to parameters #1093

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

jstuyts
Copy link
Contributor

@jstuyts jstuyts commented Jan 31, 2025

Added @Nonnull to parameters that are checked for null. Also included some method overloads/variants.

In addition found some bugs in null checks.

See https://issues.apache.org/jira/browse/WICKET-7145 for details.

Copy link
Contributor

@mattrpav mattrpav left a comment

Choose a reason for hiding this comment

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

I'm about 1/2 through the review a couple of comments:

  1. If there are null check fixes, it would be good if they were in a separate commit so those changes could potentially be back-ported.

  2. Please see the code style notes around import static and end-of-file new lines.

@@ -47,6 +45,10 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
Copy link
Contributor

Choose a reason for hiding this comment

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

static imports usually come before non-static?

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the Wicket code style call for newline or no newline at end of file?

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Code style EOF new line?

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Code style EOF new line?

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Code style EOF new line?

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Code style EOF new line?

import javax.annotation.Nonnull;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Copy link
Contributor

Choose a reason for hiding this comment

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

static imports first?

<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
Copy link
Contributor

Choose a reason for hiding this comment

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

Good time to fix the wicket-native-websocket-core/pom.xml formatting?

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Code style EOF new line

public FilenameWithVersionResourceCachingStrategy(String versionPrefix,
IResourceVersion resourceVersion)
public FilenameWithVersionResourceCachingStrategy(@Nonnull String versionPrefix,
@Nonnull IResourceVersion resourceVersion)
Copy link
Contributor

Choose a reason for hiding this comment

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

Double check for unintended whitespace

<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
Copy link
Contributor

Choose a reason for hiding this comment

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

Good time to fix pom formatting?

@jstuyts
Copy link
Contributor Author

jstuyts commented Feb 3, 2025

I'm about 1/2 through the review a couple of comments:

Thanks, I'll fix everything once you're done.

  1. If there are null check fixes, it would be good if they were in a separate commit so those changes could potentially be back-ported.

There is only 1 in production code: OriginResourceIsolationPolicy (search for Checks.notNull). The other 2 are in test code: BaseWicketTester and WicketTesterTest (search for assertNull). If you want, I can create a separate issue and PRs for backporting those.

  1. Please see the code style notes around import static and end-of-file new lines.

I'll check the notes and will make sure all changed files comply with them.

@theigl
Copy link
Contributor

theigl commented Feb 3, 2025

Is there a reason you went with jakarta-annotations and not with JSpecify?

JSpecify is intended to be the defacto standard and Spring Framework recently migrated all their annotations to it. I recently annotated our main project with it but I went the other way and used JSpecify's recommended strategy of applying @NullMarked on the package level und marking @Nullable parameters.

@mattrpav
Copy link
Contributor

mattrpav commented Feb 3, 2025

There is only 1 in production code: OriginResourceIsolationPolicy (search for Checks.notNull). The other 2 are in test code: BaseWicketTester and WicketTesterTest (search for assertNull). If you want, I can create a separate issue and PRs for backporting those.

That would be great, please do. I think there will be users on Wicket 9.x for a while since it is the javax.* based release.

@mattrpav
Copy link
Contributor

mattrpav commented Feb 3, 2025

JSpecify is intended to be the defacto standard and Spring Framework recently migrated all their annotations to it.

If JSpecify's approach becomes the standard, it will be in an updated release of the jakarta.annotation API. I do not see any reason to bring in a non-standard dependency when the standardized Jakarta-based solution is sufficient.

@theigl
Copy link
Contributor

theigl commented Feb 3, 2025

JSpecify is intended to be the defacto standard and Spring Framework recently migrated all their annotations to it.

If JSpecify's approach becomes the standard, it will be in an updated release of the jakarta.annotation API. I do not see any reason to bring in a non-standard dependency when the standardized Jakarta-based solution is sufficient.

The whole nullability discussion has been going on for years. There are findbugs annotations, checker framework annotations, jetbrains, nullaway, javax/jakarta and many more. All of them have their advantages and disadvantages. JSpecify is an attempt (by Google, Facebook, Uber, etc) to standardize these annotations and define their exact semantics.

The jakarta.annotation package is simply not enough. There is no way using only these annotations to define the default nullability of a module/package/class. What does it mean now when a parameter is not annotated? Does it mean it is nullable? Or is its nullability unspecified? JSpecify solves these ambiguities.

Having said that, annotating with jakarta.annotation is probably still better than nothing, though I would have preferred annotating with @Nullable instead.

This is not a veto from me, just my opinion.

@jstuyts
Copy link
Contributor Author

jstuyts commented Feb 3, 2025

Is there a reason you went with jakarta-annotations and not with JSpecify?

JSpecify is intended to be the defacto standard and Spring Framework recently migrated all their annotations to it. I recently annotated our main project with it but I went the other way and used JSpecify's recommended strategy of applying @NullMarked on the package level und marking @Nullable parameters.

I have been a Kotlin developer for years, so I have no knowledge of recent annotation libraries. I chose the Jakarta annotations because the library was already in the BOM. But the annotations for nullability had not been used anywhere yet.

As for the other concerns:

  • Something that is not annotated will indeed have an ambiguous meaning: it is nullable, or it has not been checked for non-nullability yet. Specifying at some higher level that all of the API is nullable if marked (or the other way around) won't help here: you still need to add an annotation to each variable or method that is nullable (or not) before it matches how the code behaves. Until that has been done, the other variables and methods are ambiguous too.
  • About using @Nonnull: in Java the default is that a reference is nullable, so for me it is more logical to state that you cannot assume the standard Java semantics.

@mattrpav
Copy link
Contributor

mattrpav commented Feb 3, 2025

The whole nullability discussion has been going on for years. There are findbugs annotations, checker framework annotations, jetbrains, nullaway, javax/jakarta and many more. All of them have their advantages and disadvantages. JSpecify is an attempt (by Google, Facebook, Uber, etc) to standardize these annotations and define their exact semantics.

There is also a huge graveyard of dead projects from large organizations that take passes at solving for short comings.

I agree, that the spec could be improved. I agree the the Jakarta Annotation Team should take this up-- if a spec doesn't meet the needs of users, it won't be viable.

The jakarta.annotation package is simply not enough. There is no way using only these annotations to define the default nullability of a module/package/class. What does it mean now when a parameter is not annotated? Does it mean it is nullable? Or is its nullability unspecified? JSpecify solves these ambiguities.

If something isn't marked, it should be presumed to be 'nullable', since that is the language default.

IMO, less noise is better. Nulls are a feature, not a bug. Null values don't use memory and do not require object allocations that need to be garbage collected. Annotating everything and throwing Optionals everywhere creates non-zero side effects and performance impacts.

Related Jakarta Annotation update requested:
ref: jakartaee/common-annotations-api#145

@theigl
Copy link
Contributor

theigl commented Feb 3, 2025

If something isn't marked, it should be presumed to be 'nullable', since that is the language default.
IMO, less noise is better.

Marking with @Nullable produces significantly less noise in my experience. While it is true that at the language level almost everything is nullable, in a typical project, the majority of values are never null. I recently annotated hundreds of thousands of LOC in my main project and I would have needed at least 3-5x the amount of annotations if I went with @Nonnull.
But marking as @Nullable only makes sense together with a @Nullmarked or @ParametersAreNonnullByDefault annotation. Otherwise it doesn't provide much value. So if we stick with jakarta.annotation I think that only the current @Nonnull approach makes sense.

@jstuyts: Are you consuming Wicket APIs from Kotlin? If so, does adding these @Nonnulls already help with null-safety in Kotlin?

@jstuyts
Copy link
Contributor Author

jstuyts commented Feb 4, 2025

@jstuyts: Are you consuming Wicket APIs from Kotlin? If so, does adding these @Nonnulls already help with null-safety in Kotlin?

@theigl Yes, I do. I just tested, and no, they unfortunately don't. Of the many options, I chose the one that is not supported by Kotlin (Jakarta annotations are not listed on this page and not in the code linked to): https://kotlinlang.org/docs/java-interop.html#nullability-annotations

I switched to JSpecify, and although that does result in warnings in Kotlin, it won't allow me to specify @NonNull everywhere where the Jakarta annotation used to be. The compiler trips over non-simple names: fully-qualified names and nested types. For example: @NonNull WebResponse.CacheScope scope results in a compile error. I had to remove annotations because of this.

So it seems to me that JSpecify is not in a usable state yet. Is there a preference for one of the other supported annotations? I don't see a good candidate as most of them are part of a library that is not specifically meant to strengthen contracts, is vendor-specific or is (as good as) obsolete.

@theigl
Copy link
Contributor

theigl commented Feb 4, 2025

So it seems to me that JSpecify is not in a usable state yet. Is there a preference for one of the other supported annotations? I don't see a good candidate as most of them are part of a library that is not specifically meant to strengthen contracts, is vendor-specific or is (as good as) obsolete.

JSpecify is perfectly usable already but it has a couple of non-intuitive annotation usages. Your example is one of them. See https://jspecify.dev/docs/user-guide/#some-subtler-details.

You have to either use static imports or write it like this:

WebResponse.@NonNull CacheScope scope

Another example is with arrays. I always have to think twice where to put the annotation depending on if the array itself is nullable or its elements are.

@jstuyts
Copy link
Contributor Author

jstuyts commented Feb 4, 2025

That would be great, please do. I think there will be users on Wicket 9.x for a while since it is the javax.* based release.

I only found the issues in 9.x. In 8.x the methods are used correctly: https://issues.apache.org/jira/browse/WICKET-7147

@jstuyts
Copy link
Contributor Author

jstuyts commented Feb 4, 2025

JSpecify is perfectly usable already but it has a couple of non-intuitive annotation usages. Your example is one of them. See https://jspecify.dev/docs/user-guide/#some-subtler-details.

Got it. I have created a branch using JSpecify: https://github.com/jstuyts/wicket/tree/add-non-null-to-parameters-jspecify

The advantages are:

  • It seems like the Java ecosystem is moving to JSpecify as the semantics are more precise and there is more tooling support.
  • The Kotlin compiler understands the annotations of JSpecify.

Let me know if you decide you would like to use JSpecify instead of the Jakarta annotations. I will close this PR and create a new one.

Another remark about which annotations to use. I think the annotations should be added incrementally, and both annotations (nullable and not nullable) must be added. Once everything is annotated (which can take quite some time), the decision to switch to package-level (or some other level) annotations can be made. It is then also easy to determine which of the 2 annotations results in the least amount of code. The most occurring annotation can be changed to higher-level annotations.

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

Successfully merging this pull request may close these issues.

3 participants