diff --git a/api/src/main/java/net/jqwik/api/providers/TypeUsage.java b/api/src/main/java/net/jqwik/api/providers/TypeUsage.java index b11d29850..a34662e30 100644 --- a/api/src/main/java/net/jqwik/api/providers/TypeUsage.java +++ b/api/src/main/java/net/jqwik/api/providers/TypeUsage.java @@ -274,4 +274,10 @@ static TypeUsage forType(Type type) { @API(status = EXPERIMENTAL, since = "1.6.0") TypeUsage withMetaInfo(String key, Object value); + + /** + * Check for type equality, ie do not consider annotations. + */ + @API(status = EXPERIMENTAL, since = "1.9.0") + boolean hasSameTypeAs(TypeUsage other); } diff --git a/engine/src/main/java/net/jqwik/engine/support/types/TypeUsageImpl.java b/engine/src/main/java/net/jqwik/engine/support/types/TypeUsageImpl.java index f01023101..bbe064766 100644 --- a/engine/src/main/java/net/jqwik/engine/support/types/TypeUsageImpl.java +++ b/engine/src/main/java/net/jqwik/engine/support/types/TypeUsageImpl.java @@ -511,8 +511,8 @@ private boolean allTypeArgumentsCanBeAssigned( return false; } - // Unbound type variables can only be assigned to themselves. - if (targetTypeArgument.isTypeVariable() && !providedTypeArgument.equals(targetTypeArgument)) { + // Unbound type variables can only be assigned to themselves or type-equal type usages. + if (targetTypeArgument.isTypeVariable() && !providedTypeArgument.hasSameTypeAs(targetTypeArgument)) { return false; } @@ -622,6 +622,57 @@ private TypeUsageImpl cloneWith(Consumer updater) { } } + @Override + public boolean hasSameTypeAs(TypeUsage otherUsage) { + if (this == otherUsage) { + return true; + } + + if (!(otherUsage instanceof TypeUsageImpl)) { + return false; + } + + TypeUsageImpl other = (TypeUsageImpl) otherUsage; + + // The rest is mostly a copy of the equals method but without annotation checking + // and with comparing component types using hasSameTypeAs + + if (!other.getRawType().equals(getRawType())) + return false; + if (!(haveSameTypes(other.typeArguments, typeArguments))) + return false; + if (other.isWildcard() != isWildcard()) { + return false; + } + if (other.isTypeVariable() != isTypeVariable()) { + return false; + } + if (other.isWildcard() && isWildcard()) { + if (!(haveSameTypes(other.lowerBounds, lowerBounds))) + return false; + if (!(haveSameTypes(other.upperBounds, upperBounds))) + return false; + } + if (other.isTypeVariable() && isTypeVariable()) { + if (!other.typeVariable.equals(typeVariable)) + return false; + return haveSameTypes(other.upperBounds, upperBounds); + } + return other.isNullable() == isNullable(); + } + + private static boolean haveSameTypes(List left, List right) { + if (left.size() != right.size()) { + return false; + } + for (int i = 0; i < left.size(); i++) { + if (!left.get(i).hasSameTypeAs(right.get(i))) { + return false; + } + } + return true; + } + @Override public boolean equals(Object obj) { if (this == obj) diff --git a/engine/src/test/java/net/jqwik/api/providers/TypeUsageTests.java b/engine/src/test/java/net/jqwik/api/providers/TypeUsageTests.java index 1d13a237f..b49e99d23 100644 --- a/engine/src/test/java/net/jqwik/api/providers/TypeUsageTests.java +++ b/engine/src/test/java/net/jqwik/api/providers/TypeUsageTests.java @@ -1205,6 +1205,82 @@ public void test( } } + @Group + @Label("hasSameType(TypeUsage)") + class HasSameType { + + @Example + void parameterizedType() throws NoSuchMethodException { + + class LocalClass { + public void listWithAnnotation(List<@From String> list) {} + } + + Method method = LocalClass.class.getMethod("listWithAnnotation", List.class); + MethodParameter parameter = JqwikReflectionSupport.getMethodParameters(method, LocalClass.class).get(0); + TypeUsage listWithAnnotation = TypeUsageImpl.forParameter(parameter); + + TypeUsage listOfString = TypeUsage.of(List.class, TypeUsage.of(String.class)); + assertThat(listOfString.hasSameTypeAs(listOfString)).isTrue(); + assertThat(listOfString.hasSameTypeAs(listWithAnnotation)).isTrue(); + + TypeUsage listOfInt = TypeUsage.of(List.class, TypeUsage.of(Integer.class)); + assertThat(listOfString.hasSameTypeAs(listOfInt)).isFalse(); + + } + + @Example + void parameterizedTypeWithWildcard() throws NoSuchMethodException { + + class LocalClass { + public void listWithVariable(List list) {} + + public void listWithAnnotatedVariable(List<@From T> list) {} + + public void listWithOtherVariable(List list) {} + } + + Method method = LocalClass.class.getMethod("listWithVariable", List.class); + MethodParameter parameter = JqwikReflectionSupport.getMethodParameters(method, LocalClass.class).get(0); + TypeUsage listWithVariable = TypeUsageImpl.forParameter(parameter); + + method = LocalClass.class.getMethod("listWithAnnotatedVariable", List.class); + parameter = JqwikReflectionSupport.getMethodParameters(method, LocalClass.class).get(0); + TypeUsage listWithAnnotatedVariable = TypeUsageImpl.forParameter(parameter); + + method = LocalClass.class.getMethod("listWithOtherVariable", List.class); + parameter = JqwikReflectionSupport.getMethodParameters(method, LocalClass.class).get(0); + TypeUsage listWithOtherVariable = TypeUsageImpl.forParameter(parameter); + + assertThat(listWithVariable.hasSameTypeAs(listWithVariable)).isTrue(); + assertThat(listWithVariable.hasSameTypeAs(listWithAnnotatedVariable)).isTrue(); + assertThat(listWithVariable.hasSameTypeAs(listWithOtherVariable)).isFalse(); + } + + @Example + void parameterizedNestedType() throws NoSuchMethodException { + + class LocalClass { + public void listWithAnnotation(List> list) {} + } + + Method method = LocalClass.class.getMethod("listWithAnnotation", List.class); + MethodParameter parameter = JqwikReflectionSupport.getMethodParameters(method, LocalClass.class).get(0); + TypeUsage listWithAnnotation = TypeUsageImpl.forParameter(parameter); + + TypeUsage listOfListOfString = TypeUsage.of( + List.class, + TypeUsage.of( + List.class, + TypeUsage.of(String.class) + ) + ); + assertThat(listOfListOfString.hasSameTypeAs(listOfListOfString)).isTrue(); + assertThat(listOfListOfString.hasSameTypeAs(listWithAnnotation)).isTrue(); + } + + } + @Group class TypeUsageEnhancers {