-
Notifications
You must be signed in to change notification settings - Fork 322
Description
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.