Nullables are not passed correctly in a generic context in Jspecify mode
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.
Hi @Marinell0 thanks for the report. At least some of these issues are due to missing JDK models (#950), which we are actively working on and hope to have some solid progress soon. The false positive in your first example goes away if I write my own identity function:
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NullMarked;
import java.util.List;
import java.util.function.Function;
@NullMarked
class Scratch {
static <T extends @Nullable Object> Function<T, T> identity() {
return t -> t;
}
public interface NullAwayExample<T> {
default void doSomething(List<@Nullable T> input) {
doSomething(input, Scratch.identity());
}
<U> void doSomething(List<@Nullable U> input, Function<@Nullable U, @Nullable T> mapper);
}
}
I'll investigate the false negative in your second example soon, but I also suspect missing library models to be the issue.
Perfect, that's how I fixed my Function.identity() issue too, but it is good to have reassurance =)
Thanks for the quick response, I'll follow #950 in the meantime. As this issue is possibly because of it, you may close this as duplicate if you want.
Thanks!