Skip to content

Nullables are not passed correctly in a generic context in Jspecify mode #1320

@Marinell0

Description

@Marinell0

We are moving to jspecify as our null annotation method and we are discovering some issues.

We added these flags to our project:

-XepOpt:NullAway:JSpecifyMode=true
-XDaddTypeAnnotationsToSymbol=true

To start using the jspecify mode, as per specification.

The problem we are having is that the @Nullable type is not passed to another generic type from a class or method.

False positive example

import java.util.List;
import java.util.function.Function;

import org.jspecify.annotations.Nullable;

public interface NullAwayExample<T> {
    default void doSomething(List<@Nullable T> input) {
        doSomething(input, Function.identity());
    }

    <U> void doSomething(List<@Nullable U> input, Function<@Nullable U, @Nullable T> mapper);
}

This gives me a warning:

[NullAway] Cannot pass parameter of type Function<U, U>, as formal parameter has type Function<@org.jspecify.annotations.Nullable U, @org.jspecify.annotations.Nullable T>, which has mismatched type parameter nullability

The Function.identity() has the signature:

    /**
     * Returns a function that always returns its input argument.
     *
     * @param <T> the type of the input and output objects to the function
     * @return a function that always returns its input argument
     */
    static <T> Function<T, T> identity() {
        return t -> t;
    }

So the problem that I'm seeing is that the <T> in the Function<T, T> or any function that receives a generic from outside the method loses the @Nullable annotation on it's definition.

In this case, the warning is a false positive as the generic type <T> will be returned in both cases, and as I'm giving it a @Nullable Object, it should return a @Nullable Object of the same type, as the lambda t -> t implies.

False negative example

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;

import org.jspecify.annotations.Nullable;

@SuppressWarnings("SystemOut")
public final class NullAwayExample {
    private NullAwayExample() {}

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        List<Callable<@Nullable Throwable>> tasks = new ArrayList<>();

        tasks.add(() -> null);

        try (var exec = Executors.newFixedThreadPool(1)) {
            @Nullable Throwable result = exec.invokeAny(tasks);

            String message = result.getMessage();
            System.out.println(message);
        }
    }
}

This compiles and NullAway doesn't warn that result might be null. This happens for the same reason as the last example:

<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

invokeAny has the generic method type <T> declared, but the annotation from @Nullable Throwable is lost when Collection<Callable<@Nullable Throwable>> is passed.

This gives a false negative, as we can accept any result from invokeAny (or any function with a generic typing) and it loses the @Nullable annotation.

Expected behaviour

The generic type from methods need to retain the @Nullable typing when given a @Nullable Object, otherwise NullAway might give false positives or even worse, false negatives.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions