-
-
Notifications
You must be signed in to change notification settings - Fork 64
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
Is Arbitrary<@Nullable T> correct/intended under JSpecify semantics? #575
Comments
@lcahmu Thanks for writing up the details of this problem. The nullability annotations were initially introduced to improve type inference on the Kotlin side. So I've never really thought deeply about what the JSpecify (or JSR305 or jetbrains.* or Checker) annotations want to express. :-/ That said, the goal should be to make it work with both CheckerFramework and Kotlin. This gets more complicated with Kotlin 2.0, which no longer supports JSR 305. That's why jqwik 1.9.0 will get rid of the remaining JSR305 annotation. In fact |
Thanks @jlink! I was able to try the snapshot, but unfortunately I do still see the same errors. |
Thanks. I'll try the suggested approach then. Hopefully in the days to come. |
Sadly,
|
@jibidus Is there a workaround for your situation? Like disabling CheckerFramework errors through comments or annotations? |
Do you mean the Kotlin compiler? It's definitely legal Java on the class/interface declaration. I'll see if I can reproduce the error. |
What is the exact error message? |
% git diff
diff --git a/api/src/main/java/net/jqwik/api/Arbitrary.java b/api/src/main/java/net/jqwik/api/Arbitrary.java
index f76f8c7b4..baa91c28e 100644
--- a/api/src/main/java/net/jqwik/api/Arbitrary.java
+++ b/api/src/main/java/net/jqwik/api/Arbitrary.java
@@ -20,7 +20,7 @@ import static org.apiguardian.api.API.Status.*;
*/
@API(status = STABLE, since = "1.0")
@CheckReturnValue
-public interface Arbitrary<@Nullable T> {
+public interface Arbitrary<T extends @Nullable Object> {
@API(status = INTERNAL)
abstract class ArbitraryFacade {
|
Like vlsi, I wasn't able to reproduce a compiler error. And the good news is that Kotlin does seem to understand There are a couple of possible workarounds for Checker. It is possible to disable checks in a limited scope with |
IntelliJ IDEA produces warnings for |
Well, I tried a different thing: public interface Arbitrary...
default Arbitrary<T extends @Nullable Object> injectNull(double nullProbability) {
return ArbitraryFacade.implementation.injectNull(Arbitrary.this, nullProbability);
} When going with public interface Arbitrary<T extends @Nullable Object> {...} How would the signature of
var strings: Arbitrary<String> = String.any()
var nullableStrings: Arbitrary<String?> = String.any().injectNull(0.1) Leaving out the |
Interestingly, the warning appears here: public interface Arbitrary<@Nullable T> { ... } but not here: default Arbitrary<@Nullable T> injectNull(double nullProbability) { ... } |
@jlink , the difference is as follows: // Here `T` declares a **type variable**, so `@Nullable T` stands for "annotated type variable"
public interface Arbitrary<@Nullable T> {
// Here `T` stands for a type, so `@Nullable T` stands for an "annotated type"
default Arbitrary<@Nullable T> injectNull(double nullProbability) { ... }
} Does it make sense?
// It means "it is allowable to implement Arbitrary when T is nullable and non-nullable"
public interface Arbitrary<T extends @Nullable Object> {
// No matter which was T at the declaration time, injectNull always returns a null-producing Arbitrary
default Arbitrary<@Nullable T> injectNull(double nullProbability)
}
Could you clarify why would you want to have the following? val nullableStrings: Arbitrary<String> = String.any().injectNull(0.1) |
Actually, I don't want it. It should be prevented by the time system - or at least get a warning as it is now. If I understand @lcahmu correctly, default Arbitrary<@Nullable T> injectNull(double nullProbability) does not work in the presence of CheckerFramework, but I might have misunderstood. |
@jlink , lcahmu mentions interface declarations, so they suggest patches like #575 (comment) |
By the way, it looks like |
Just as @vlsi says. I believe it's generally anywhere type variables are declared, so:
but also things like:
The |
So I misunderstood. Good. Simplifies the solution. |
Looks good on a first pass! Checker is happy in my project at least, everything is compiling -- though obviously that doesn't necessarily generalize. I am getting some runtime failures, but right now I'm suspecting that's just from other changes between 1.7 and 1.8 that I haven't corrected for yet, probably not anything to do with these annotation updates. (JQwik isn't looking at these annotations when trying to match types, right?) I'll continue working through that and report back in the unlikely event that I find something, but for now... works on my machine! |
Okay, I did run into a case where I can't tell whether or not it's an expected change from 1.7 to 1.8. Consider a scenario with a wrapper class and a provider for it, and a property that holds only for a subset of values. The following works on all of 1.7.4, 1.8.5, and 1.9.0-SNAPSHOT (3db8e5f):
Changing the - public record Wrapper<T>(T value) { }
+ public record Wrapper<T extends @Nullable Object>(T value) { }
@Property
- public <T> void property(@ForAll("reduced") Wrapper<T> wrapper) { }
+ public <T extends @Nullable Object> void property(@ForAll("reduced") Wrapper<T> wrapper) { }
@Provide("reduced")
- public <T> Arbitrary<Wrapper<T>> provider(@ForAll Wrapper<T> wrapper) {
+ public <T extends @Nullable Object> Arbitrary<Wrapper<T>> provider(@ForAll Wrapper<T> wrapper) {
return Arbitraries.just(wrapper);
}
It does seem to be the annotation itself having some effect here. The error does not occur with public class GenericTest {
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+ public @interface Dummy { }
+
- public record Wrapper<T>(T value) { }
+ public record Wrapper<T extends @Dummy Object>(T value) { }
@Property
- public <T> void property(@ForAll("reduced") Wrapper<T> wrapper) { }
+ public <T extends @Dummy Object> void property(@ForAll("reduced") Wrapper<T> wrapper) { }
@Provide("reduced")
- public <T> Arbitrary<Wrapper<T>> provider(@ForAll Wrapper<T> wrapper) {
+ public <T extends @Dummy Object> Arbitrary<Wrapper<T>> provider(@ForAll Wrapper<T> wrapper) {
return Arbitraries.just(wrapper);
}
|
@lcahmu Thanks for the example. In general Using |
Thanks @jlink that did seem to move things forward a bit. I've run into another possible issue Edit: I found a workaround while shrinking the issue to a simpler example to include with the issue. That has allowed me to continue testing. |
Even with the current changes I get unexpected (for me) compiler warnings. E.g.: fun test() {
val v1: Arbitrary<String?> = Arbitraries.just("1")
val v2: Arbitrary<String?> = Arbitraries.just(null)
// Compiler warning in IntelliJ for both assignments:
// Type mismatch.
// Required: Arbitrary<String?>
// Found: Arbitrary<String>
} This happens despite public class Arbitraries...
public static <T extends @Nullable Object> Arbitrary<T> just(T value) {
return ArbitrariesFacade.implementation.just(value);
} Am I missing something or is IntelliJ's tooling just not working correctly with the |
Can now confirm that, at least in my project, everything seems to be working as expected now. Thanks so much for looking into these issues! Not sure what's going on there with Kotlin though. I'm not sure if I'm missing some compiler settings somewhere (Kotlin isn't my day-to-day) but I'm not seeing those warnings. Instead, I'm seeing something else just as confusing/concerning though: I'm not seeing a warning where I would expect one: I do see those same warnings if I change it back to |
The compiler option you need is: |
I tend to close this issue - and maybe open another one to tackle the confusing Kotlin type warnings. |
I guess IDEA's Kotlin plugin does not handle |
Closing as fixed. @lcahmu Feel free to reopen if anything is missing. |
Testing Problem
I assume that the intent of
interface Arbitrary<@Nullable T>
is to say thatT
could be a type that allows nulls, but aT
that is@NonNull
(whether explicitly or implicitly) is also acceptable. I assume this because classes likeIntegerArbitrary
ultimately extendArbitrary<Integer>
, whereInteger
is implicitly@NonNull
. Also becauseArbitrary.injectNull(..)
explicitly adds@Nullable
to the response type, which is redundant ifT
is already@Nullable
, so there appears to be some expectation that it might not be.I see that it's the JSpecify flavor of
@Nullable
that's being used here. I haven't used JSpecify myself so I could easily be misreading it, but some of its project discussions, such as this one and its documentation on "Other TYPE_USE annotation targets", seem to be saying that<@Nullable T>
doesn't mean anything to JSpecify.If it doesn't mean anything, then this seems harmless at first glance. I stumbled across this minor discrepancy in a project that's trying to use both JQwik and CheckerFramework. This combination worked just fine until I tried to update from JQwik 1.7 to 1.8, which is when the
@Nullable
annotations were introduced. Now, obviously, CheckerFramework is not JSpecify. It has it's own@Nullable
annotations. But their semantics are close enough that ChekerFramework also accepts JSpecify's annotations in addition to its own. Close, but not exact: the relevant difference between the two is that while<@Nullable T>
doesn't mean anything to JSpecify, it does mean something to CheckerFramework. Specifically, it means thatT
must allow nulls, which means that it will not accept the following:CheckerFramework doesn't like this because
Arbitraries.integers()
returns anIntegerArbitrary
, which, as noted above, ultimately extendsArbitrary<@NonNull Integer>
. And since@NonNull Integer
does not allow null, that violates the<@Nullable T>
constraint that requires it to, so Checker rejects this assignment for having incompatible types. But since that very line appears more than once in the JQwik user guide, I have to assume that's intended to work.Arbitrary
was the first place I noticed this, but methods ofArbitraries
,Combinators
, and some other classes extending fromArbitrary
also have similar@Nullable T
s in their type parameters.Suggested Solution
The same JSpecify discussion linked above, and also its user guide, suggest that
Arbitrary<T extends @Nullable Object>
would correctly express the semantics that I assume were intended. It's also compatible with CheckerFramework, resolving the error that's currently produced by the one-line example above. I've been able to confirm this with CheckerFramework stubs. Stubbing also provides a workaround, even if a somewhat inconvenient one.Discussion
Although it's CheckerFramework that motivates me here personally, compatibility has not been promised or even suggested anywhere, so I understand if it's not convincing. I would instead focus on the (lack of) meaning under JSpecify's own semantics, and treat any benefit toward CheckerFramework users as purely coincidental.
I also saw that nullability annotations were originally added for Kotlin compatibility. I have no idea how Kotlin would treat
<T extends @Nullable Object>
. If Kotlin doesn't read it in a way that's consistent with JSpecify's semantics and it came down to having to make a choice on which to support, I'd absolutely understand prioritizing Kotlin.The text was updated successfully, but these errors were encountered: