javaeclipsenullstatic-analysisexternal-annotations

Eclipse null analysis has @NonNull on generic parameter


I am having trouble with this code...

import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
...
StdSerializer<Object> stdSerializer;

stdSerializer = ToStringSerializer.instance;

Note: The package is annotated with @NonNullByDefault in package-info.java.

On the last line of the code above, ToStringSerializer.instance has the following error...

Null type safety (type annotations): The expression of type 'ToStringSerializer' needs unchecked conversion to conform to 'StdSerializer<@NonNull Object>', corresponding supertype is 'StdSerializer'

The ToStringSerializer.eea file has...

class com/fasterxml/jackson/databind/ser/std/ToStringSerializer

instance
 Lcom/fasterxml/jackson/databind/ser/std/ToStringSerializer;
 L1com/fasterxml/jackson/databind/ser/std/ToStringSerializer;
...

Why does Eclipse 2021-03 (4.19.0 build 20210312-0638) say ToStringSerializer.instance needs to be ...<@NonNull...>? How does Eclipse determine that Object must be @NonNull? How do I fix this?

I discovered this problem on Eclipse 2020-12 and it also happens on Eclipse 2021-03.


Solution

  • why does eclipse say ToStringSerializer.instance needs to be ...<@NonNull...>?

    Because you said so. Specifically, you said all types are to be presumed as inherently @NonNull, unless they are explicitly marked as @Nullable. That's what @NonNullByDefault does. That means: Everywhere a type appears that has no nullity-indicating annotation slapped on it, are to be presumed to have a @NonNull annotation.

    The problem is twofold.

    1. Nullity is a type tag. It is a separate dimension. Generics are invariant (that means: a List<Number> cannot be used when a List<Object> is required; in contrast to just a Number, which can be used when an Object is required. This is because that's how the math works out: Otherwise you could assign a List<Number> to a variable of type List<Object>, then add some non-number to it, but that means you just stuffed a non-number in your list of numbers, whooooops. Combine these two facts and it gets a bit hairy. Let's opt into covariance, which you can do with generics (List<? extends Number> is a subtype of List<? extends Object>. This works because you can't add anything to a List<? extends Object>, whereas you can add anything to a List<Object>.

    Here is a little ASCII art picture. @NN means 'nonnull', and @Nul means nullable:

    
    List<? extends @Nul Integer> ➞ List<? extends @Nul Number> ➞ List<? extends @Nul Object>
                ↑                             ↑                        ↑
    List<? extends @NN Integer> ➞ List<? extends @NN Number> ➞ List<? extends @NN Object>
    

    In this image, if you have one of these 6 items, then you can pass that item as argument to a method whose argument can be 'reached' with the arrows.

    As you can see, you cannot unfold this picture into a single line: It has two dimensions.

    It gets even more complicated if you pile in the fact that generics types, themselves, can have their togs modified.

    For example, imagine you have this type:

    Map<@NonNull String, @NonNull String> map;
    

    Makes sense, right? It's a map that maps strings to strings. It cannot contain null keys. Any key is mapped to a guaranteed non-null string.

    The definition of the Map interface is: public interface Map<K, V>, where K and V represent the key and value types. This, in this example, V is bound to @NonNull String.

    Let's look at the get method of Map:

    public V get(K key);
    

    NB: It's actually 'Object key', but the reason for that is irrelevant to this explanation, and this is easier to follow.

    Soo.. get returns a @NN String if invoked on an expression of type Map<@NN String, @NN String>, right?

    Wrong!

    It can return null, because the get method's implementation does that if the provided key is not in the map!

    So, the get method definition needs to be marked down with a type tag modifier. It needs to say: "This method returns V, but modified so that it is @Nul. If it was of unknown nullity state, well, it's known now: It can definitely be null, calling code should check. If it was @NN, that is immaterial; the type returned by this method should be @Nul String. If it was already @Nul, great, no need for modifications.

    This gets us to multiple significant realizations about using annotations to track nullity:

    In this specific case? It's probably a simple matter, where Jackson's ToStringSerializer has no nullity annotations at all, so eclipse presumes all things are nullable, and thus ToStringSerializer.instance's type is @Nullable StdSerializer<@Nullable Object>. In other words: The field may be null. If it is not null, it is definitely an StdSerializer. What it can serialize, though, is presumably any object, and even null. Whereas your variable type is (due to the @ByDefault effect) a 'actual StdSerializer and not a null reference. Furthermore, said serializer can serialize actual objects. It is a compiler error to even attempt to ask it to serialize a value that could potentially hold null'.

    Those two notions are type-wise incompatible. The fix is presumably one of these:

    1. Make your variable type StdSerializer<@Nullable Object>.
    2. Reconfigure eclipse to just silently ignore any null issues stemming from 'legacy' nullity info (as in, missing nullity info), and double check that eclipse realizes that it has no idea about the nullity of the types present in the signatures of the jackson library.
    3. Use that external annotation system to properly 'externally annotate' jackson.
    4. Find somebody who has done that work and published it on the web. Then download that and use it.
    5. Disable the nullity checker system entirely, or reconfigure it to apply only to interactions within the project and not with any external-to-the-project APIs you are using.