-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Remove explicit nullable annotations if they depend on the generic type argument #337
Comments
Mind the JavaDoc on the method:
The function is nonnull, not the result of it. |
I did not mean the mapping function, I'm talking specifically about the Example: public @NonNull String foo(@NonNull String bar) { // note that in CF @NonNull is assumed, just here for clarity sake
return cache.get(bar, String::toUpperCase);
} Since I hope this makes my question a bit more clear, sorry if I use the wrong terminology, I'm always open to learn. (currently, because of the public @NonNull String foo(@NonNull String bar) { // note that in CF @NonNull is assumed, just here for clarity sake
return Objects.requireNonNull(cache.get(bar, String::toUpperCase), "Unexpected null result from cache");
} |
Yes, if I originally meant it for documentation as a hint to the reader, but static analysis tools have gotten better. I think dropping the annotation would imply non-null by default. |
@DavyLandman: Actually I learned something now. Thanks for the more detailed explanation!
The nullability might be part of the return type of the mapping function in CF, but there is no way to propagate this. Implementations of the same method signature can legally return always There is no contract on the method that CF can use to determine how the returned value relates to the mapper function. |
Indeed, similar to that, maybe we can do the same as the CF jdk annotations for map? default V computeIfAbsent(K key, Function<? super K, ? extends @Nullable V> mappingFunction) { I have to admit, I would have to dive deep into the CF docs to understand how the
I understand this tradeoff, I think the example from the jdk annotations might be a good middle ground. |
Thanks. I think that's a reasonable solution. I use ErrorProne + NullAway instead, so there will likely be other minor mistakes in the future. |
Great, I imagine might be more cases like this throughout the big caffeine api? I hadn't heard about nullaway, looks like a good alternative to checker framework. CF takes some investment and getting used too, but after the initial pain, keeping it updated is not that hard. |
I wouldn't be surprised. This is a little special because its a computation, so we just need to fix each of the I tried CF once but it was too invasive for me, but I'm sure it's matured a lot since then. ErrorProne + NullAway are a bit easier to add progressively, though I suspect they are not as powerful. |
I forgot to link here earlier, but I am waiting to see how the effort in google/guava#2960 (comment) pans out. |
Reviewing this anew, and I think maybe this case should use @PolyNull.
If so, then I would change this one method signature to, @PolyNull
V get(@NonNull K key, @NonNull Function<? super K, ? extends @PolyNull V> mappingFunction); What do you think @DavyLandman? |
The following should be enough: // `K extends Object` means K is non-null (provided default checkerframework configuration is used)
public interface Cache<K extends Object, V> {
V get(/* @NonNull <== not needed */K key, @NonNull Function<? super K, ? extends V> mappingFunction); |
I haven't used checker for a long time, but it used to default to null (not nonnull). Is that no longer the case? That was one reason that I decided not to use it as it became very verbose with that coding style. |
PS. I'm surprised you have lots of |
Here's how we declare |
For my usages, the goal wasn't actually about static analysis but for clearer documentation. The jsr305 were used that way to be more explicit for those scanning the JavaDoc that the return value is nullable. When migrating in #242, it was requested that these are more explicit to better conform to the checker framework's analysis. That made sense, but became verbose. Would using |
The defaults are described here: https://checkerframework.org/manual/#climb-to-top They suggest non-null by default, however, things get a bit complicated with generics.
Exactly.
I see. However, tools like IntelliJ IDEA recognize the annotations and they might produce false warnings in case the annotations are not at their best :)
I guess so. For instance, even if you write PS. I've added a gist on checkerframework behavior, however, it is more for machine verification rather than for "documenting nullability in code" |
Right, modern Java generally assumes non-null by default. The IDE nullness checkers often did too, or at least Eclipse's as one of the early ones. I am positive checker went against that in early versions, as I recall that in their docs in 2014 when first trying it out here. If that's changed or by using default qualifier then that is great. I'll clean up the annotations in the v3 branch and ping you for a quick review. |
@vlsi Why not include the generic bounds or use |
Generic bounds are tricky, and trying to make every bound nullable or non-nullable does not really work for cases like I recently annotated Apache Calcite codebase (see apache/calcite#2268), and it does pass the verification. I learned that the Checker Framework's recommended approach to generics works OK, it is readable, and it is more-or-less consistent. |
@vlsi I spent a few hours trying to resolve checker framework warnings, but I don't think it's worthwhile. It complains about types being nullable despite non-null if conditions asserting that to no longer be true. By checking only the types and not logic flow, hundreds of pointless warnings are produced. These have to be suppressed, which makes the code harder to read and negates any value in catching errors. This may be a bad fit because as a data structure I'll try to port over any public api interface changes to try and make that better conform. That's your and @DavyLandman intent anyway, I just got too ambitious by trying to pass its rules too. |
@ben-manes I agree, especially for map like structures, checker-framework is really hard to get right. One of the problems is caused by arrays. I think at some points maps where hard-coded in their internals. So I think providing the right annotations on the interface is a good enough trade-off. If you want, you could take a look at the CF annotations for |
Additional wildcards are used throughput the APIs to more flexibly accept parameters. For example this allows a wider range of method references to be used as load functions. The generic types now match the Checker Framework's rules [1]. This should improve usage of the cache apis in projects that run the checker. This project does not and its implementation classes are not compliant for the nullness checker. [1] https://checkerframework.org/manual/#generics-instantiation
Additional wildcards are used throughput the APIs to more flexibly accept parameters. For example this allows a wider range of method references to be used as load functions. The generic types now match the Checker Framework's rules [1]. This should improve usage of the cache apis in projects that run the checker. This project does not and its implementation classes are not compliant for the nullness checker. [1] https://checkerframework.org/manual/#generics-instantiation
Additional wildcards are used throughput the APIs to more flexibly accept parameters. For example this allows a wider range of method references to be used as load functions. The generic types now match the Checker Framework's rules [1]. This should improve usage of the cache apis in projects that run the checker. This project does not and its implementation classes are not compliant for the nullness checker. [1] https://checkerframework.org/manual/#generics-instantiation
Additional wildcards are used throughput the APIs to more flexibly accept parameters. For example this allows a wider range of method references to be used as load functions. The generic types now match the Checker Framework's rules [1]. This should improve usage of the cache apis in projects that run the checker. This project does not and its implementation classes are not compliant for the nullness checker. [1] https://checkerframework.org/manual/#generics-instantiation
I see that |
Guava will throw an There isn't a way in the annotations to handle that I don't plan to spend much time on 2.x's annotations, but I am open to any refinements for 3.x. |
I see, thanks for the explanation. Would it be possible to use |
My limited understanding is it won't, because it requires checking a method argument. In v3 we have, @PolyNull V get(K key, Function<? super K, ? extends @PolyNull V> mappingFunction); This is evaluated as if @NonNull V get(K key, Function<? super K, ? extends @NonNull V> mappingFunction);
@Nullable V get(K key, Function<? super K, ? extends @Nullable V> mappingFunction); but that requires pairing with an argument, which |
Makes sense. And I guess the other option of removing the |
It wouldn't accurate, because you are allowed to return null from For example let's say that I wanted to cache users by their email for login. If the user mistypes their email then it will be a cache miss that returns null. In Guava this would throw an exception as if an error, whereas in |
Got it. It's just unfortunate I guess. Another solution might be to define a NonNullable getOrthrow method, but that might be an overkill. |
I think CheckerFramework also uses PolyNull: default @PolyNull V computeIfAbsent(K key,
Function<? super K, ? extends @PolyNull V> mappingFunction) { |
@DavyLandman : Like Ben pointed out earlier, I think that requires the type parameter be "paired with an argument, which LoadingCache.get(key) lacks" |
@Avinm ah, my bad. I would think that the type came from the class level type bound. But I might be missing something about the LoadingCache signature. Ah wait, is it that the closures is passed in at an earlier point? Maybe that should influence the result of the V type. So the whole loading cache result type is PolyNull. But I admit, I cannot oversee the implications of that. |
The LoadingCache has class level K and V type params: The nullability here depends on the CacheLoader supplied while building the LoadingCache:
Now its usage would cause a (slightly) ugly warning:
Could Polynull annotation somehow be used here? |
minor correction, the type signatures in 3.x were updated for checker framework, LoadingCache<K extends Object, V extends Object> extends Cache<K, V> and the @DefaultQualifier(value = NonNull.class, locations = TypeUseLocation.FIELD)
@DefaultQualifier(value = NonNull.class, locations = TypeUseLocation.PARAMETER)
@DefaultQualifier(value = NonNull.class, locations = TypeUseLocation.RETURN)
package com.github.benmanes.caffeine.cache; @Avinm is on 2.x, but I think we should try to focus on 3.x and maybe backport. |
@vlsi The latest errorprone release claims that it is unnecessary to use
Since we don't compile using CheckerFramework's javac, should this be stripped out? |
Well, in theory, |
I think I may have suggested the Error Prone message, and it has a mistake in it. That said, |
hey @cpovirk, just a reminder in case you forgot about this over the weekend. No worries otherwise, I know everyone is busy and if this is only about verbosity then we discuss whenever time allows. |
So much for Monday... :) The main points are:
Here's a demo to show that the two produce identical bytecode:
Thus, there's no benefit to writing At that point, the question becomes: So how does the Checker Framework distinguish between First: When the Checker Framework is getting information about an API from source code, rather than bytecode, then it can distinguish between But that doesn't explain everything: The Checker Framework does advise using The key point here is that the Checker Framework modifies what bytecode javac writes. I don't want to claim that I can give a fully precise description of what you have to do to make it do that, but I can get closer than I did in the Error Prone message: If you run the Checker Framework's nullness checker, then the Checker Framework will make javac write nullness annotations to the resulting bytecode. That's technically enough to answer the original question, but I think it would be best for me to step back and talk about the Checker Framework's writing of nullness annotations more broadly: Why is it useful to write the annotations? After all, if the Checker Framework knows that unannotated types are non-nullable, then it doesn't need to write annotations to tell its future self that :) I know of two reasons:
So, if you were to build your Caffeine release with a javac invocation that runs the Checker Framework, then Until then, you want |
|
Hi Ben,
I was pleasantly surprised by CheckerFramework annotations on the caffeine code, since we also switched a while back, it makes life easier. 👍
A pattern I saw and I wanted to check your opinion on is for example
Cache::get
:Now reading the documentation, if the mapping function never returns null, neither should the get function, right?
If that is so, than I think the
@Nullable
should be removed from the return type. Since CF will just propagate the nullability of theV
type parameter. If V is@Nullable
, then the result will be, but if you provide a mapping function that has a@NonNull
return type, it's a bit strange to still have to handle the null case.I hope this question makes sense?
The text was updated successfully, but these errors were encountered: