hamcrest / JavaHamcrest

Java (and original) version of Hamcrest

Home Page:http://hamcrest.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Matchers.everyItem() has incorrect covariance in return type in hamcrest 2.2

Tikani opened this issue · comments

Environment: Java 11, hamcrest 2.2
Due to strange signature of everyItem():
public static <U> org.hamcrest.Matcher<java.lang.Iterable<? extends U>> everyItem(org.hamcrest.Matcher<U> itemMatcher)
it can't be combined with other matchers in allOf() methods: allOf(iterableWithSize(5), everyItem(displayed())) results in
Error:(109,28) java: no suitable method found for allOf(org.hamcrest.Matcher<java.lang.Iterable<java.lang.Object>>,org.hamcrest.Matcher<java.lang.Iterable<? extends org.openqa.selenium.WebElement>>)
Seemingly, Java's generalized target-type inference do its job wrong in this case. With hamcrest-all 1.3 this works fine because of invariant return type of everyItem():
<U> Matcher<java.lang.Iterable<U>> everyItem(Matcher<U> itemMatcher).

Any news here? Is the project dead?

Hi @Tikani, the project is still alive. I'm afraid the last few months have been quite overwhelming for me personally, so I haven't had much time to look at this. I should be able to make some time this week, though.

Hmm... looking closer at this issue: the change was introduced in issue #40, with the justification that it followed the Guidelines for Wildcard Use.

Looking at the example you provided, it appears the compiler doesn't have enough information to infer the correct type for the first argument to everyItem. If you help the compiler, it should work. e.g. change this:

allOf(iterableWithSize(5), everyItem(displayed()))

to this:

allOf(IsIterableWithSize.<WebElement>iterableWithSize(5), everyItem(displayed())).

This looks uglier, though it can be improved by using helper utility methods with the correct type information.

@tumbarumba i think who claimed this change as complying with the Java wildcard usage guidelines made a mistake. Wildcards should only be used as method's generic parameters enrichers (following "producer extends, consumer super" semantics) to provide more flexible API. "Wildcarded" return type should be avoided in general (especially, return types with bounds). Guidelines for Wildcard Use states that too, after Wildcard Guidelines: block.

Yeah, reading that page more carefully, it explicitly says:

These guidelines do not apply to a method's return type. Using a wildcard as a return type should be avoided because it forces programmers using the code to deal with wildcards.

I tried reverting #40 in my local workspace. Annoyingly, the problem with the failing type inference in allOf persisted. That is, I changed Every.everyItem back to this:

public class Every<T> extends TypeSafeDiagnosingMatcher<Iterable<T>> {
...
    public static <U> Matcher<Iterable<U>> everyItem(final Matcher<U> itemMatcher) {
        return new Every<>(itemMatcher);
    }
}

Even with that change, I still get the same error for this test:

@Test public void
everyItemMatchesWhenUsedWithOtherCompoundMatchers() {
    Matcher<Iterable<String>> matcher = allOf(iterableWithSize(3), everyItem(containsString("ca")));
    assertMatches(matcher, asList("cake", "cat", "carve"));
}

I'm scratching my head at this one at the moment. It appears everyItem is working as expected. Rather, it is iterableWithSize that is causing problems with the type inference. I can't see that IterableWithSize has changed since 1.3, though.

If anyone could help diagnose what's happening in this instance, I'd be very interested.

@tumbarumba methods, that have generics in return types only, behave very bad with implicit type inference (iterableWithSize is an example). They are constrained only by the destination (target) type, i.e. in a very weak fashion and if target type represents not a single type, but a (sub-)set via extends/super wildcard, here we go with explicit generic declaration crutches to satisfy the capricious compiler. I think only that can be done is your first piece of advice - uglify code with helpers. :(

@tumbarumba Would it work with the following signature?

public static <U> Matcher<Iterable<U>> everyItem(final Matcher<? super U> itemMatcher)

If I understand Guidelines for Wildcard Use correctly, wildcards shouldn't be used in return types, thus keeping Matcher<Iterable<U>> as return type, but regarding itemMatcher parameter, matchers for superclasses of U should still be allowed to assert items of type U.

@tumbarumba I tested in my project the signature I mentioned in my last comment, and it works (using Java 8).
Since this issue is breaking backwards compatibility, it prevents me from updating Hamcrest from 1.3 to neither 2.1 nor 2.2. A new release with this new signature (or at least reverting changes made in #40) would be very appreciated. 😛

Does this solution break #40

Hey, any news from this? I tried to update junit4 source code to hamcrest 2.2 and found this exact issue.

Hi @tumbarumba,

We've run into related issues in the Spring Framework as well.

See spring-projects/spring-framework#28660 (comment) for details.

Specifically, in Spring's testing support we wish to add a new RequestMatcher factory method that accepts an argument with an effective type of Matcher<Iterable<String>> (tentatively slated as Matcher<Iterable<? super String>>).

The problem is that the following methods in org.hamcrest.CoreMatchers have incompatible generics declarations.

  • <T> Matcher<Iterable<? super T>> hasItem(Matcher<? super T>)
  • <U> Matcher<Iterable<? extends U>> everyItem(Matcher<U>)

Consequently, it is impossible for us to create a new RequestMatcher factory method in Spring that is compatible with both hasItem(...) and everyItem(...).

As a side note, most of the methods in org.hamcrest.Matchers that return a Matcher<Iterable<...>> in fact return Matcher<Iterable<? extends T>> instead of Matcher<Iterable<? super T>>.

In summary, it would be great if Hamcrest could implement a consistent solution for the following issues.

  • #40 (which is still open even though #41 has been merged)
  • #289 (this issue)

@sbrannen imo, a correct way of dealing with variance, if you want to give flexibility to API - when an argument is contravariant and a return type is covariant. It will be great if public API is reworked this way.