Some Eclipse Foundation services are deprecated, or will be soon. Please ensure you've read this important communication.

Bug 488495

Summary: [null] Wrong warning about unchecked conversion. Constraints lost after using streams.
Product: [Eclipse Project] JDT Reporter: Marc-André Laperle <malaperle>
Component: CoreAssignee: Till Brychcy <register.eclipse>
Status: VERIFIED FIXED QA Contact: Stephan Herrmann <stephan.herrmann>
Severity: normal    
Priority: P3 CC: alexmonthy, jarthana, manoj.palat, matthew.khouzam, patrick.tasse, register.eclipse, sasikanth.bharadwaj, stephan.herrmann
Version: 4.6Flags: sasikanth.bharadwaj: review+
jarthana: review+
Target Milestone: 4.6 RC2   
Hardware: PC   
OS: Linux   
See Also: https://git.eclipse.org/r/69082
https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/commit/?id=78439d31c46454ce758b8068b8dcf447748481a2
Whiteboard:
Bug Depends on:    
Bug Blocks: 493151    
Attachments:
Description Flags
Sample project
none
Project showing the lost annotation to Collectors.toList() none

Description Marc-André Laperle CLA 2016-02-25 15:14:25 EST
Created attachment 259931 [details]
Sample project

Using Eclipse 4.6-I20160223-0800

After using a few stream methods, it seems that the null constraints get lost. For example, in this code:

@NonNullByDefault
public class Main {

    public static void main(String[] args, Set<String> set) {
        Optional<@Nullable String> kam = set.stream()
                .map(trace -> getString(String.class))
                .findFirst(); // Null type safety (type annotations): The expression of type 'Optional<String>' needs unchecked conversion to conform to 'Optional<@Nullable String>'
        System.out.println(kam);
    }

    public @Nullable static <T extends CharSequence> T getString(Class<T> moduleClass) {
        return null;
    }
}

After the map, it should be a stream of @Nullable String but instead it's just String (can be verified with hover). Because of that, findFirst returns an Optional<String> instead of Optional<@Nullable String>

I attached a sample project for convenience.
Comment 1 Alexandre Montplaisir CLA 2016-03-10 23:11:52 EST
I have noticed another similar problem with recent JDT, and I think it's the same bug.

We often use the pattern:

  List<Something> list = somethings.stream()
                           ...
                           ...
                           .collect(Collectors.toList());

and it was annoying to have to null check the resulting 'list' all the time, so we've added external annotations to Collectors.toList() :

>  public static <T> Collector<T, ?, List<T>> toList()

becomes

>  public static <T> Collector<T, ?, @NonNull List<T>> toList()

so that the list returned by .collect(Collectors.toList()) would be a "@NonNull List<T>". T remains unannotated of course.

This used to work, but since about a week or two (using I-Builds) the returned List is no longer @NonNull, and we get warnings. It seems the @NonNull is lost when going through the .collect() call.
Comment 2 Stephan Herrmann CLA 2016-03-12 18:24:10 EST
(In reply to Marc-Andre Laperle from comment #0)

Thanks, I can reproduce.

The obvious workaround is to change the invocation of getString() like so:

      .map(trace -> Main.<@Nullable String>getString(String.class))

This does not seem to be a regression, previous versions report the same unchecked conversion.


Versions in [4.5M5,4.6M3] additionally report:
----------
2. ERROR in /tmp/Main.java (at line 10)
        .map(trace -> Main.getString(String.class))
                                     ^^^^^^^^^^^^
Null type safety (type annotations): The expression of type 'Class<String>' needs unchecked conversion to conform to '@NonNull Class<@NonNull String>'
----------

Explanation: the bound of <T> is implicitly @NonNull (via @NNBD), and only since 4.6M4 do we recognize String.class as having type '@NonNull Class<@NonNull String>'



(In reply to Alexandre Montplaisir from comment #1)

Would you like to extend this to a reproducing example?
Comment 3 Alexandre Montplaisir CLA 2016-03-14 16:58:16 EDT
(In reply to Stephan Herrmann from comment #2)
> (In reply to Marc-Andre Laperle from comment #0)
> 
> Thanks, I can reproduce.
> 
> The obvious workaround is to change the invocation of getString() like so:
> 
>       .map(trace -> Main.<@Nullable String>getString(String.class))

I imported Marc-André's project and tried doing this, but I get an error at the '<@Nullable String>' part:

Null constraint mismatch: The type '@Nullable String' is not a valid substitute for the type parameter 'T extends @NonNull CharSequence'

This is with the latest Integration build (I20160314-0800).

> (In reply to Alexandre Montplaisir from comment #1)
> Would you like to extend this to a reproducing example?

Sure, I will prepare a test project shortly.
Comment 4 Alexandre Montplaisir CLA 2016-03-14 18:13:13 EDT
Created attachment 260289 [details]
Project showing the lost annotation to Collectors.toList()

I have attached a project that exposes the problem I was mentioning in Comment 1.

It contains an external annotation to Collectors.toList(), to return a
  Collector<T, ?, @NonNull List<T>>
instead of
  Collector<T, ?, List<T>>

so that calling .collect(Collectors.toList()) always returns a @NonNull List<T>. (I'm guessing we should not annotate .collect() directly, since a clumsy Collector could return null.)

With the latest integration build (as well as M4 and M5) and the attached project, the signatures reported by the tooltips of toList() and collect(), respectively, are:

> Collector<@NonNull String, ?, @NonNull List<@NonNull String>> java.util.stream.Collectors.toList()
> List<@NonNull String> java.util.stream.Stream.collect(Collector<? super @NonNull String, ?, List<@NonNull String>> collector)

We see in the second one that the List is not @NonNull anymore.

I was pretty sure it was working previously, and in fact I had to go back to the 4.6 M3 build to get it working correctly:

> Collector<@NonNull String, ?, @NonNull List<@NonNull String>> java.util.stream.Collectors.toList()
> @NonNull List<@NonNull String> java.util.stream.Stream.collect(Collector<? super @NonNull String, ?, @NonNull List<@NonNull String>> collector)

So it was broken for longer than I thought :)
Comment 5 Stephan Herrmann CLA 2016-03-14 18:27:03 EDT
Thanks, I'll look into it during M7.
Comment 6 Eclipse Genie CLA 2016-03-22 17:34:58 EDT
New Gerrit change created: https://git.eclipse.org/r/69082
Comment 7 Till Brychcy CLA 2016-03-22 17:36:23 EDT
The example with …collect(toList()) is not the same problem as the other one.

Below is a distilled test case:

package test;

import org.eclipse.jdt.annotation.NonNullByDefault;

interface Collector<A, R> {
}

interface Stream {
    <A1, R1> R1 collect(Collector<A1, R1> collector);
}

interface List<E> {
}

@NonNullByDefault
public class Test {
    public static <T> Collector<?, List<T>> toList() {
        return new Collector<Object, List<T>>(){};
    }

    public static List<String> myMethod(Stream stream) {
        List<String> list = stream.collect(toList());
        return list;
    }
}

ANALYSIS:
This got broken by commit a8001f94dedfcf0ea807d46ff48ed3d0c7a3c2ef for bug 483146 which added the if-condition
	if (nullHints != 0 && TypeBinding.equalsEquals(boundI.left, boundJ.left)) {
			boundI.nullHints |= nullHints;
			boundJ.nullHints |= nullHints;
	}
but in this case, boundI.relation == ReductionResult.SAME and boundI.right is the same as boundJ.left:
boundI: Dependency List<T#2>#4 = R1#1
boundJ: TypeBound  R1#1 = test.List<java.lang.Object>

PATCH (via gerrit)
changes the if-condition to
	if (nullHints != 0) {
			if (TypeBinding.equalsEquals(boundI.left, boundJ.left)
				|| (boundI.relation == ReductionResult.SAME	&& TypeBinding.equalsEquals(boundI.right, boundJ.left))
				|| (boundJ.relation == ReductionResult.SAME	&& TypeBinding.equalsEquals(boundI.left, boundJ.right))) {
				boundI.nullHints |= nullHints;
				boundJ.nullHints |= nullHints;
			}
	}
Comment 8 Till Brychcy CLA 2016-03-22 17:47:28 EDT
I don't have a patch for the original problem reported in this bug, but I have made a distilled test case without lambdas (for easier debugging, so no nested inference)

package test2;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

@NonNullByDefault
public class TestWithoutLambda {
	public @Nullable static <@NonNull A> A getString(Class<A> moduleClass) {
		return null;
	}

	public static <X> X r(X x) {
		return x;
	}

	public static void f() {
		@NonNull String s=r(getString(String.class));
	}
}

for which 
----------
1. WARNING in test2\TestWithoutLambda.java (at line 18)
	@NonNull String s=r(getString(String.class));
	                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
Null type safety (type annotations): The expression of type 'String' needs unchecked conversion to conform to '@NonNull String'
----------

is reported instead of the expected
----------
1. ERROR in test2\TestWithoutLambda.java (at line 18)
	@NonNull String s=r(getString(String.class));
	                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
Null type mismatch (type annotations): required '@NonNull String' but this expression has type '@Nullable String'
----------

The problem is, that the @NonNull annotation on A is combined with the @Nullable of in the return type, but the @Nullable should instead supersede it.
Comment 9 Stephan Herrmann CLA 2016-04-24 17:06:51 EDT
Bulk move: too late for 4.6 M7.

Very likely has to be moved out of 4.6 entirely.
Comment 10 Till Brychcy CLA 2016-05-05 15:18:50 EDT
@Stephan, maybe the patch i made for the example with Collectors.toList() could still go into 4.6 RC1 ? This is a regression w.r.t. to Mars and the patch is really small (see comment #7)
Comment 11 Stephan Herrmann CLA 2016-05-06 11:10:15 EDT
(In reply to Till Brychcy from comment #10)
> @Stephan, maybe the patch i made for the example with Collectors.toList()
> could still go into 4.6 RC1 ? This is a regression w.r.t. to Mars and the
> patch is really small (see comment #7)

In order to focus on this part (and keep the context to your investigation) I created bug 493151 for the original problem.
Comment 12 Stephan Herrmann CLA 2016-05-06 11:41:30 EDT
I agree to the new logic when compared to the current version, because #left and #right of a SAME bound can be swapped at will. There may be more involved but that can be handled later (perhaps via bug 493151).

I've rebased the change.
Comment 13 Stephan Herrmann CLA 2016-05-06 16:47:59 EDT
Good for RC1 from my p.o.v.

@Sasi, I'd like to request your +1 for RC1. As Till mentioned this is a regression wrt Mars.
Comment 14 Stephan Herrmann CLA 2016-05-09 16:49:16 EDT
(In reply to Stephan Herrmann from comment #13)
> Good for RC1 from my p.o.v.
> 
> @Sasi, I'd like to request your +1 for RC1. As Till mentioned this is a
> regression wrt Mars.

Have we run out of time for RC1?

What do you suggest, move to RC2 or to 4.7?
Comment 15 Stephan Herrmann CLA 2016-05-11 10:36:38 EDT
One more attempt for RC2 to get sufficient reviews for this regression fix.

Please see that we "highjacked" this ticket for the problem in comment 7 and its solution.
Comment 16 Sasikanth Bharadwaj CLA 2016-05-12 02:00:03 EDT
(In reply to Stephan Herrmann from comment #14)
> (In reply to Stephan Herrmann from comment #13)
> > Good for RC1 from my p.o.v.
> > 
> > @Sasi, I'd like to request your +1 for RC1. As Till mentioned this is a
> > regression wrt Mars.
> 
> Have we run out of time for RC1?
> 
> What do you suggest, move to RC2 or to 4.7?

Sorry, never saw this, probably got lost among other emails

+1, looks good to me. Just one (probably naive) observation. If left and right of SAME bounds can be swapped, for completeness sake, we should also include right right comparison in the check?
Comment 17 Stephan Herrmann CLA 2016-05-12 05:08:33 EDT
(In reply to Sasikanth Bharadwaj from comment #16)
> +1, looks good to me.

Thanks.

> Just one (probably naive) observation. If left and
> right of SAME bounds can be swapped, for completeness sake, we should also
> include right right comparison in the check?

Maybe Till has more on this, but: TypeBound is not fully symmetric in that #left is always an InferenceVariable, whereas #right is an arbitrary TypeBinding. Also see that the left-left check is from bug 483146 and Till only proposed to add the left-right & right-left checks.

Anyway, I think it's good to do a minimal fix for the known regression now, and continue our search for a *complete* solution for 4.7 / 4.6.1 - see bug 493151 comment 2.
Comment 18 Till Brychcy CLA 2016-05-12 07:34:38 EDT
(In reply to Stephan Herrmann from comment #17)
> Maybe Till has more on this, but: TypeBound is not fully symmetric in that
> #left is always an InferenceVariable, whereas #right is an arbitrary
> TypeBinding. Also see that the left-left check is from bug 483146 and Till
> only proposed to add the left-right & right-left checks.
> 
> Anyway, I think it's good to do a minimal fix for the known regression now,
> and continue our search for a *complete* solution for 4.7 / 4.6.1 - see bug
> 493151 comment 2.

I agree 100% and have nothing to add.
Comment 19 Jay Arthanareeswaran CLA 2016-05-16 07:28:26 EDT
Patch looks good to me.
Comment 21 Stephan Herrmann CLA 2016-05-16 11:11:17 EDT
(In reply to Eclipse Genie from comment #20)
> Gerrit change https://git.eclipse.org/r/69082 was merged to [master].
> Commit:
> http://git.eclipse.org/c/jdt/eclipse.jdt.core.git/commit/?id=78439d31c46454ce758b8068b8dcf447748481a2
> 

Thanks to all. I rebased and released the change for 4.6 RC2.
Comment 22 Manoj N Palat CLA 2016-05-25 07:15:47 EDT
Verified for Eclipse Neon RC2 Build id: I20160519-1730