Browse Source

Erase MethodParameter generic type signature when unresolvable.

Make sure to use the raw type or java.lang.Object for unresolvable generic method parameters while retaining the type variable in the method signature.

See: #3374
pull/3386/head
Christoph Strobl 2 months ago
parent
commit
c2a135a7aa
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 213
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java
  2. 25
      src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java
  3. 259
      src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java
  4. 101
      src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java

213
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java

@ -16,32 +16,23 @@ @@ -16,32 +16,23 @@
package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.lang.model.element.Modifier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.MethodSpec;
@ -340,210 +331,6 @@ class AotRepositoryCreator { @@ -340,210 +331,6 @@ class AotRepositoryCreator {
generationMetadata.addRepositoryMethod(method, contributor);
}
/**
* Value object to determine whether generics in a given {@link Method} can be resolved. Resolvable generics are e.g.
* declared on the method level (unbounded type variables, type variables using class boundaries). Considers
* collections and map types.
* <p>
* Considers resolvable:
* <ul>
* <li>Unbounded method-level type parameters {@code <T> T foo(Class<T>)}</li>
* <li>Bounded method-level type parameters that resolve to a class
* {@code <T extends Serializable> T foo(Class<T>)}</li>
* <li>Simple references to interface variables {@code T foo(), List<T> foo()}</li>
* <li>Unbounded wildcards {@code User foo(GeoJson<?>)}</li>
* </ul>
* Considers non-resolvable:
* <ul>
* <li>Parametrized bounds referring to known variables on method-level type parameters
* {@code <P extends T> T foo(Class<T>), List<? super T> foo()}</li>
* <li>Generally unresolvable generics</li>
* </ul>
* </p>
*
* @author Mark Paluch
*/
record ResolvableGenerics(Method method, Class<?> implClass, Set<Type> resolvableTypeVariables,
Set<Type> unwantedMethodVariables) {
/**
* Create a new {@code ResolvableGenerics} object for the given {@link Method}.
*
* @param method
* @return
*/
public static ResolvableGenerics of(Method method, Class<?> implClass) {
return new ResolvableGenerics(method, implClass, getResolvableTypeVariables(method),
getUnwantedMethodVariables(method));
}
private static Set<Type> getResolvableTypeVariables(Method method) {
Set<Type> simpleTypeVariables = new HashSet<>();
for (TypeVariable<Method> typeParameter : method.getTypeParameters()) {
if (isClassBounded(typeParameter.getBounds())) {
simpleTypeVariables.add(typeParameter);
}
}
return simpleTypeVariables;
}
private static Set<Type> getUnwantedMethodVariables(Method method) {
Set<Type> unwanted = new HashSet<>();
for (TypeVariable<Method> typeParameter : method.getTypeParameters()) {
if (!isClassBounded(typeParameter.getBounds())) {
unwanted.add(typeParameter);
}
}
return unwanted;
}
/**
* Check whether the {@link Method} has unresolvable generics when being considered in the context of the
* implementation class.
*
* @return
*/
public boolean hasUnresolvableGenerics() {
ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, implClass);
if (isUnresolvable(resolvableType)) {
return true;
}
for (int i = 0; i < method.getParameterCount(); i++) {
if (isUnresolvable(ResolvableType.forMethodParameter(method, i, implClass))) {
return true;
}
}
return false;
}
private boolean isUnresolvable(TypeInformation<?> typeInformation) {
return isUnresolvable(typeInformation.toResolvableType());
}
private boolean isUnresolvable(ResolvableType resolvableType) {
if (isResolvable(resolvableType)) {
return false;
}
if (isUnwanted(resolvableType)) {
return true;
}
if (resolvableType.isAssignableFrom(Class.class)) {
return isUnresolvable(resolvableType.getGeneric(0));
}
TypeInformation<?> typeInformation = TypeInformation.of(resolvableType);
if (typeInformation.isMap() || typeInformation.isCollectionLike()) {
for (ResolvableType type : resolvableType.getGenerics()) {
if (isUnresolvable(type)) {
return true;
}
}
return false;
}
if (typeInformation.getActualType() != null && typeInformation.getActualType() != typeInformation) {
return isUnresolvable(typeInformation.getRequiredActualType());
}
return resolvableType.hasUnresolvableGenerics();
}
private boolean isResolvable(Type[] types) {
for (Type type : types) {
if (resolvableTypeVariables.contains(type)) {
continue;
}
if (isClass(type)) {
continue;
}
return false;
}
return true;
}
private boolean isResolvable(ResolvableType resolvableType) {
return testGenericType(resolvableType, it -> {
if (resolvableTypeVariables.contains(it)) {
return true;
}
if (it instanceof WildcardType wt) {
return isClassBounded(wt.getLowerBounds()) && isClassBounded(wt.getUpperBounds());
}
return false;
});
}
private boolean isUnwanted(ResolvableType resolvableType) {
return testGenericType(resolvableType, o -> {
if (o instanceof WildcardType wt) {
return !isResolvable(wt.getLowerBounds()) || !isResolvable(wt.getUpperBounds());
}
return unwantedMethodVariables.contains(o);
});
}
private static boolean testGenericType(ResolvableType resolvableType, Predicate<Type> predicate) {
if (predicate.test(resolvableType.getType())) {
return true;
}
ResolvableType[] generics = resolvableType.getGenerics();
for (ResolvableType generic : generics) {
if (testGenericType(generic, predicate)) {
return true;
}
}
return false;
}
private static boolean isClassBounded(Type[] bounds) {
for (Type bound : bounds) {
if (isClass(bound)) {
continue;
}
return false;
}
return true;
}
private static boolean isClass(Type type) {
return type instanceof Class;
}
}
/**
* Factory interface to conditionally create {@link MethodContributor} instances. An implementation may decide whether
* to return a {@link MethodContributor} or {@literal null}, if no method (code or metadata) should be contributed.

25
src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java

@ -26,7 +26,6 @@ import java.util.Map.Entry; @@ -26,7 +26,6 @@ import java.util.Map.Entry;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
@ -36,6 +35,7 @@ import org.springframework.data.repository.core.RepositoryInformation; @@ -36,6 +35,7 @@ import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.TypeName;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@ -79,14 +79,17 @@ class MethodMetadata { @@ -79,14 +79,17 @@ class MethodMetadata {
ResolvableType repositoryInterface, Map<String, ParameterSpec> methodArguments,
Map<String, MethodParameter> methodParameters) {
Class<?> repositoryInterfaceType = repositoryInterface.toClass();
for (Parameter parameter : method.getParameters()) {
MethodParameter methodParameter = MethodParameter.forParameter(parameter).withContainingClass(repositoryInterface.resolve());
MethodParameter methodParameter = MethodParameter.forParameter(parameter)
.withContainingClass(repositoryInterfaceType);
methodParameter.initParameterNameDiscovery(nameDiscoverer);
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter);
TypeName parameterType = TypeNames.resolvedTypeName(resolvableParameterType);
TypeName parameterType = parameterTypeName(methodParameter, repositoryInterfaceType);
Assert.notNull(methodParameter.getParameterName(), "MethodParameter.getParameterName() must not be null");
ParameterSpec parameterSpec = ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build();
if (methodArguments.containsKey(parameterSpec.name())) {
@ -98,6 +101,20 @@ class MethodMetadata { @@ -98,6 +101,20 @@ class MethodMetadata {
}
}
@SuppressWarnings("NullAway")
static TypeName parameterTypeName(MethodParameter methodParameter, Class<?> repositoryInterface) {
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter);
if (ClassUtils.isPrimitiveOrWrapper(resolvableParameterType.toClass())) {
return TypeNames.className(resolvableParameterType);
}
ResolvableGenerics resolvableGenerics = ResolvableGenerics.of(methodParameter.getMethod(), repositoryInterface);
return resolvableGenerics.isFullyResolvableParameter(methodParameter.getParameter())
? TypeNames.resolvedTypeName(resolvableParameterType)
: TypeNames.className(resolvableParameterType);
}
ResolvableType getReturnType() {
return returnType;
}

259
src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java

@ -0,0 +1,259 @@ @@ -0,0 +1,259 @@
/*
* Copyright 2025-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.util.TypeInformation;
/**
* Value object to determine whether generics in a given {@link Method} can be resolved. Resolvable generics are e.g.
* declared on the method level (unbounded type variables, type variables using class boundaries). Considers collections
* and map types.
* <p>
* Considers resolvable:
* <ul>
* <li>Unbounded method-level type parameters {@code <T> T foo(Class<T>)}</li>
* <li>Bounded method-level type parameters that resolve to a class
* {@code <T extends Serializable> T foo(Class<T>)}</li>
* <li>Simple references to interface variables {@code T foo(), List<T> foo()}</li>
* <li>Unbounded wildcards {@code User foo(GeoJson<?>)}</li>
* </ul>
* Considers non-resolvable:
* <ul>
* <li>Parametrized bounds referring to known variables on method-level type parameters
* {@code <P extends T> T foo(Class<T>), List<? super T> foo()}</li>
* <li>Generally unresolvable generics</li>
* </ul>
* </p>
*
* @author Mark Paluch
*/
record ResolvableGenerics(Method method, Class<?> implClass, Set<Type> resolvableTypeVariables,
Set<Type> unwantedMethodVariables) {
/**
* Create a new {@code ResolvableGenerics} object for the given {@link Method}.
*
* @param method
* @return
*/
public static ResolvableGenerics of(Method method, Class<?> implClass) {
return new ResolvableGenerics(method, implClass, getResolvableTypeVariables(method),
getUnwantedMethodVariables(method));
}
/**
* Checks if a given {@link Parameter} can be resolved fully. Unresolvable or unwanted type signatures should fall
* back to using raw types.
*
* @param parameter
* @return
*/
public boolean isFullyResolvableParameter(Parameter parameter) {
MethodParameter methodParameter = MethodParameter.forParameter(parameter).withContainingClass(implClass);
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
return testGenericType(resolvableType, o -> {
if (o instanceof WildcardType wt) {
return !isResolvable(wt.getLowerBounds()) || !isResolvable(wt.getUpperBounds());
}
if (o instanceof ParameterizedType pt) {
return isResolvable(pt.getActualTypeArguments());
}
return unwantedMethodVariables.contains(o);
});
}
private static Set<Type> getResolvableTypeVariables(Method method) {
Set<Type> simpleTypeVariables = new HashSet<>();
for (TypeVariable<Method> typeParameter : method.getTypeParameters()) {
if (isClassBounded(typeParameter.getBounds())) {
simpleTypeVariables.add(typeParameter);
}
}
return simpleTypeVariables;
}
private static Set<Type> getUnwantedMethodVariables(Method method) {
Set<Type> unwanted = new HashSet<>();
for (TypeVariable<Method> typeParameter : method.getTypeParameters()) {
if (!isClassBounded(typeParameter.getBounds())) {
unwanted.add(typeParameter);
}
}
return unwanted;
}
/**
* Check whether the {@link Method} has unresolvable generics when being considered in the context of the
* implementation class.
*
* @return
*/
public boolean hasUnresolvableGenerics() {
ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, implClass);
if (isUnresolvable(resolvableType)) {
return true;
}
for (int i = 0; i < method.getParameterCount(); i++) {
if (isUnresolvable(ResolvableType.forMethodParameter(method, i, implClass))) {
return true;
}
}
return false;
}
private boolean isUnresolvable(TypeInformation<?> typeInformation) {
return isUnresolvable(typeInformation.toResolvableType());
}
private boolean isUnresolvable(ResolvableType resolvableType) {
if (isResolvable(resolvableType)) {
return false;
}
if (isUnwanted(resolvableType)) {
return true;
}
if (resolvableType.isAssignableFrom(Class.class)) {
return isUnresolvable(resolvableType.getGeneric(0));
}
TypeInformation<?> typeInformation = TypeInformation.of(resolvableType);
if (typeInformation.isMap() || typeInformation.isCollectionLike()) {
for (ResolvableType type : resolvableType.getGenerics()) {
if (isUnresolvable(type)) {
return true;
}
}
return false;
}
if (typeInformation.getActualType() != null && typeInformation.getActualType() != typeInformation) {
return isUnresolvable(typeInformation.getRequiredActualType());
}
return resolvableType.hasUnresolvableGenerics();
}
private boolean isResolvable(Type[] types) {
for (Type type : types) {
if (resolvableTypeVariables.contains(type)) {
continue;
}
if (isClass(type)) {
continue;
}
return false;
}
return true;
}
private boolean isResolvable(ResolvableType resolvableType) {
return testGenericType(resolvableType, it -> {
if (resolvableTypeVariables.contains(it)) {
return true;
}
if (it instanceof WildcardType wt) {
return isClassBounded(wt.getLowerBounds()) && isClassBounded(wt.getUpperBounds());
}
return false;
});
}
private boolean isUnwanted(ResolvableType resolvableType) {
return testGenericType(resolvableType, o -> {
if (o instanceof WildcardType wt) {
return !isResolvable(wt.getLowerBounds()) || !isResolvable(wt.getUpperBounds());
}
return unwantedMethodVariables.contains(o);
});
}
private static boolean testGenericType(ResolvableType resolvableType, Predicate<Type> predicate) {
if (predicate.test(resolvableType.getType())) {
return true;
}
ResolvableType[] generics = resolvableType.getGenerics();
for (ResolvableType generic : generics) {
if (testGenericType(generic, predicate)) {
return true;
}
}
return false;
}
private static boolean isClassBounded(Type[] bounds) {
for (Type bound : bounds) {
if (isClass(bound)) {
continue;
}
return false;
}
return true;
}
private static boolean isClass(Type type) {
return type instanceof Class;
}
}

101
src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java

@ -15,9 +15,10 @@ @@ -15,9 +15,10 @@
*/
package org.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import example.UserRepository.User;
@ -35,7 +36,6 @@ import org.junit.jupiter.api.Test; @@ -35,7 +36,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Answers;
import org.springframework.aot.generate.Generated;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.ResolvableType;
@ -51,10 +51,12 @@ import org.springframework.data.repository.core.support.RepositoryFragment; @@ -51,10 +51,12 @@ import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.DefaultParameters;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeSpec;
import org.springframework.stereotype.Repository;
import org.springframework.util.ClassUtils;
/**
* Unit tests for {@link AotRepositoryCreator}.
@ -73,8 +75,7 @@ class AotRepositoryCreatorUnitTests { @@ -73,8 +75,7 @@ class AotRepositoryCreatorUnitTests {
doReturn(UserRepository.class).when(repositoryInformation).getRepositoryInterface();
}
@Test
// GH-3279
@Test // GH-3279
void writesClassSkeleton() {
AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons",
@ -87,8 +88,7 @@ class AotRepositoryCreatorUnitTests { @@ -87,8 +88,7 @@ class AotRepositoryCreatorUnitTests {
.contains("public UserRepositoryImpl"); // default constructor if not arguments to wire
}
@Test
// GH-3279
@Test // GH-3279
void appliesCtorArguments() {
AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons",
@ -112,8 +112,7 @@ class AotRepositoryCreatorUnitTests { @@ -112,8 +112,7 @@ class AotRepositoryCreatorUnitTests {
.doesNotContain("this.ctorScoped = ctorScoped");
}
@Test
// GH-3279
@Test // GH-3279
void appliesCtorCodeBlock() {
AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons",
@ -127,8 +126,7 @@ class AotRepositoryCreatorUnitTests { @@ -127,8 +126,7 @@ class AotRepositoryCreatorUnitTests {
"UserRepositoryImpl() { throw new IllegalStateException(\"initialization error\"); }");
}
@Test
// GH-3279
@Test // GH-3279
void appliesClassCustomizations() {
AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons",
@ -156,8 +154,7 @@ class AotRepositoryCreatorUnitTests { @@ -156,8 +154,7 @@ class AotRepositoryCreatorUnitTests {
.containsIgnoringWhitespaces("void oops() { }");
}
@Test
// GH-3279
@Test // GH-3279
void appliesQueryMethodContributor() {
AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(
@ -186,8 +183,7 @@ class AotRepositoryCreatorUnitTests { @@ -186,8 +183,7 @@ class AotRepositoryCreatorUnitTests {
.containsIgnoringWhitespaces("void oops() { }");
}
@Test
// GH-3279
@Test // GH-3279
void shouldContributeFragmentImplementationMetadata() {
AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(
@ -207,8 +203,7 @@ class AotRepositoryCreatorUnitTests { @@ -207,8 +203,7 @@ class AotRepositoryCreatorUnitTests {
assertThat(method.fragment().implementation()).isEqualTo(DummyQuerydslPredicateExecutor.class.getName());
}
@Test
// GH-3339
@Test // GH-3339
void usesTargetTypeName() {
AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons",
@ -226,8 +221,7 @@ class AotRepositoryCreatorUnitTests { @@ -226,8 +221,7 @@ class AotRepositoryCreatorUnitTests {
.contains("public %s(Metric param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName()));
}
@Test
// GH-3339
@Test // GH-3339
void usesGenericConstructorArguments() {
AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons",
@ -246,8 +240,7 @@ class AotRepositoryCreatorUnitTests { @@ -246,8 +240,7 @@ class AotRepositoryCreatorUnitTests {
"public %s(List<Metric> param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName()));
}
@Test
// GH-3374
@Test // GH-3374
void skipsMethodWithUnresolvableGenericReturnType() {
SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory();
@ -261,27 +254,48 @@ class AotRepositoryCreatorUnitTests { @@ -261,27 +254,48 @@ class AotRepositoryCreatorUnitTests {
QueryMethod queryMethod = new QueryMethod(method, repositoryInformation, spelAwareProxyProjectionFactory,
DefaultParameters::new);
return new MethodContributor<>(queryMethod, Map::of) {
@Override
public MethodSpec contribute(AotQueryMethodGenerationContext context) {
return MethodSpec.methodBuilder(context.getMethod().getName()).addCode("// 1 = 1").build();
}
@Override
public boolean contributesMethodSpec() {
return true;
return MethodContributor.forQueryMethod(queryMethod).withMetadata(Map::of).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
if (!ClassUtils.isVoidType(method.getReturnType())) {
builder.addStatement("return null");
}
};
return builder.build();
});
});
// same package as source repo
String generated = generate(repositoryCreator);
assertThat(generated).contains("someMethod").contains("findByFirstname").contains("project1ByFirstname")
.contains("project2ByFirstname").contains("geoQuery").contains("rangeQuery");
assertThat(generated) //
// very simple ones with fixed type signature
.contains("findByFirstname") //
.contains("rangeQuery") //
.contains("public String someMethod()") //
.contains("public String findByPrimitive(int value)") //
.contains("public String findByPrimitiveArray(int[] values)") //
.containsSubsequence("public", "User typedInputParameter(", "User value)") //
.containsSubsequence("List<", "User> findByFirstnameIn(List<String> firstname)") //
assertThat(generated).doesNotContain("baseProjection").doesNotContain("upperBoundedProjection")
// <T> List<T> findByFirstname(String firstname, Class<T> type);
.contains("public <T> List findByFirstname(String firstname, Class type)")
// <T extends User> List<T> project1ByFirstname(String firstname, Class type);
.contains("public <T> List project2ByFirstname(String firstname, Class type)")
// <T extends User> List<T> project2ByFirstname(String firstname, Class<? extends T> type)
.contains("public <T> List project2ByFirstname(String firstname, Class type)")
// List<User> geoQuery(GeoJson<?> geoJson);
.containsSubsequence("public List<", "User> geoQuery(", "GeoJson geoJson)")
// <T> List<T> genInputType(String firstname, T value);
.contains("public <T> List genInputType(String firstname, Object value)")
.doesNotContain("baseProjection") //
.doesNotContain("upperBoundedProjection") //
.doesNotContain("lowerBoundedProjection()");
}
@ -305,16 +319,14 @@ class AotRepositoryCreatorUnitTests { @@ -305,16 +319,14 @@ class AotRepositoryCreatorUnitTests {
@MethodSource("declaredUserRepositoryMethods")
void shouldResolveGenerics(Method method) {
assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics())
.isFalse();
assertThat(ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()).isFalse();
}
@ParameterizedTest
@MethodSource("resolvedRepositoryMethods")
void shouldResolveInterfaceGenerics(Method method) {
assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics())
.isFalse();
assertThat(ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()).isFalse();
}
@ParameterizedTest
@ -323,8 +335,7 @@ class AotRepositoryCreatorUnitTests { @@ -323,8 +335,7 @@ class AotRepositoryCreatorUnitTests {
assumeThat(method.getDeclaringClass()).isEqualTo(BaseRepository.class);
assertThat(AotRepositoryCreator.ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics())
.isTrue();
assertThat(ResolvableGenerics.of(method, UserRepository.class).hasUnresolvableGenerics()).isTrue();
}
private AotRepositoryCreator.AotBundle doCreate(AotRepositoryCreator creator) {
@ -368,14 +379,24 @@ class AotRepositoryCreatorUnitTests { @@ -368,14 +379,24 @@ class AotRepositoryCreatorUnitTests {
T parametrizedSelection(String firstname);
T typedInputParameter(T value);
}
interface UserRepository extends BaseRepository<User, String> {
String someMethod();
List<User> findByFirstnameIn(List<String> firstname);
String findByPrimitive(int value);
String findByPrimitiveArray(int[] values);
<T> List<T> findByFirstname(String firstname, Class<T> type);
<T> List<T> genInputType(String firstname, T value);
<T extends User> List<T> project1ByFirstname(String firstname, Class<? super T> type);
<T extends User> List<T> project2ByFirstname(String firstname, Class<? extends T> type);

Loading…
Cancel
Save