eclipse-jdt / eclipse.jdt.core

Eclipse Public License 2.0
158 stars 126 forks source link

EEAs applied incorrectly when using "Inherit null annotations" with NonNullByDefault #2512

Open sebthom opened 4 months ago

sebthom commented 4 months ago

Given:

  1. An EEA file for Map containing:

    class java/util/Map
     <K:Ljava/lang/Object;V:Ljava/lang/Object;>
    
    get
     (Ljava/lang/Object;)TV;
     (Ljava/lang/Object;)T0V;
  2. "Inherit null annotations" compiler option enabled

  3. A test class with @NonNullByDefault

Then in the following example the compiler correctly determines that Map#get can return null values but incorrectly determines that HashMap#get or AbstractMap#get will always return non-null values.

package test;

import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;

public class Test {

    @NonNullByDefault
    public static void main(final String[] args) {

        final HashMap<Object, Object> hashmap = new HashMap<>();
        if (hashmap.get("") == null) { // compiler warning: Redundant null check: comparing '@NonNull Object' against null
        }

        final AbstractMap<Object, Object> abstractmap = new HashMap<>();
        if (abstractmap.get("") == null) { // compiler warning: Redundant null check: comparing '@NonNull Object' against null
        }

        final Map<Object, Object> map = new HashMap<>();
        if (map.get("") == null) { // no warning
        }
    }
}

I am using Eclipse 2023-03 with these null analysis settings:

image

stephan-herrmann commented 4 months ago

Thanks for the clear report.

ecj handles different kinds of "implicit" annotations (i.e., not visible in the immediate source code):

Unfortunately, we haven't specified how exactly those should interact in the given example.

What we know:

The exact contract of hasmap.get() depends on the order / precedence of evaluation:

The first things I can say:

For now you have several options:

sebthom commented 4 months ago

Very enlightening insights. Thanks for your prompt and thorough response!

sebthom commented 4 months ago

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?

stephan-herrmann commented 4 months ago

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.

agentgt commented 4 months ago

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?

sebthom commented 4 months ago

@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.