Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Tikani opened this issue Mar 21, 2020 · 12 comments
Open

Comments

@Tikani
Copy link

Tikani commented Mar 21, 2020

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

@Tikani
Copy link
Author

Tikani commented Apr 24, 2020

Any news here? Is the project dead?

@tumbarumba
Copy link
Member

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.

@tumbarumba
Copy link
Member

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.

@Tikani
Copy link
Author

Tikani commented Apr 28, 2020

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

@tumbarumba
Copy link
Member

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.

@Tikani
Copy link
Author

Tikani commented May 2, 2020

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

@fbastien
Copy link

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

@fbastien
Copy link

fbastien commented Oct 6, 2020

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

@nhojpatrick
Copy link
Member

Does this solution break #40

@juliocbcotta
Copy link

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

@sbrannen
Copy link

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.

@Tikani
Copy link
Author

Tikani commented Jun 24, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants