Open sebthom opened 4 months ago
Thanks for the clear report.
ecj handles different kinds of "implicit" annotations (i.e., not visible in the immediate source code):
@NonNullByDefault
Unfortunately, we haven't specified how exactly those should interact in the given example.
What we know:
Map.get()
should be seen as @Nullable V get(Object)
from external annotationshashmap
has type HashMap<@NonNull Object, @NonNull Object>
from applying @NonNullByDefault
The exact contract of hasmap.get()
depends on the order / precedence of evaluation:
@NonNull Object get(@NonNull Object)
Map.get()
to HashMap.get()
.
HashMap<K,V>
to be @Nullable V get(Object)
, in which case instantiating V
as @NonNull Object
would create a conflict with the "explicit" @Nullable
. For this particular case we implemented a "merge" such that the explicit annotation will win.The first things I can say:
For now you have several options:
HashMap.get()
, too.Map
wherever possible like you do in the last part of your exampleVery enlightening insights. Thanks for your prompt and thorough response!
I thought a bit more about it.
If I am replicating the example from above with directly applying a @Nullable
annotation to code the result is different.
package test;
import org.eclipse.jdt.annotation.*;
public class Test2 {
interface Map<K, V> {
@Nullable V get(Object key);
}
static abstract class AbstractMap<K, V> implements Map<K, V> {
@Override
public V get(final Object key) {
return (V) null;
}
}
static class HashMap<K, V> extends AbstractMap<K, V> { }
@NonNullByDefault
public static void main(final String[] args) {
final HashMap<String, Object> hashmap = new HashMap<>();
if (hashmap.get("") == null) { // NO error: Redundant null check: comparing '@NonNull Object' against null
}
final AbstractMap<String, Object> abstractmap = new HashMap<>();
if (abstractmap.get("") == null) { // NO error: Redundant null check: comparing '@NonNull Object' against null
}
final Map<String, Object> map = new HashMap<>();
if (map.get("") == null) {
}
}
}
Isn't the idea that applying a nullable/nonnull annotation in code has the same effect as applying it via an EEA file?
I thought a bit more about it. [...]
Thanks.
By tweaking this test a bit further I got to the bottom of the problem: Put Map and friends in separate files and compile separately, to force that compilation of the main method will use the binary (.class) version of them. Then you'll see the problems even with the explicit annotation. :(
Turns out the following combination doesn't work:
Remove any of these factors and the error will go away.
If that could be fixed, perhaps the case of external annotations would automatically work, too.
FWIW inheritance of annotations seems to be problematic in general across multiple null analysis tools. JSpecify has decided not support it and I agree with Stephan that I prefer the explicit annotation approach.
I wonder how bad it would be to remove that feature (inheritance of null annotations)? @sebthom do you rely on inheritance of null annotations?
@agentgt I was, but I now changed the EEA generator/updater of https://github.com/vegardit/no-npe to do the inheritance analysis and propagate inherited null annotations to the generated EEA files.
Given:
An EEA file for Map containing:
"Inherit null annotations" compiler option enabled
A test class with
@NonNullByDefault
Then in the following example the compiler correctly determines that
Map#get
can return null values but incorrectly determines thatHashMap#get
orAbstractMap#get
will always return non-null values.I am using Eclipse 2023-03 with these null analysis settings: