From c2a135a7aac04c00f89033372d2ea9835a682fc2 Mon Sep 17 00:00:00 2001
From: Christoph Strobl
Date: Mon, 13 Oct 2025 13:36:49 +0200
Subject: [PATCH] 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
---
.../aot/generate/AotRepositoryCreator.java | 213 --------------
.../aot/generate/MethodMetadata.java | 25 +-
.../aot/generate/ResolvableGenerics.java | 259 ++++++++++++++++++
.../AotRepositoryCreatorUnitTests.java | 101 ++++---
4 files changed, 341 insertions(+), 257 deletions(-)
create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java
index 24dbf83de..5bfcba6da 100644
--- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java
+++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryCreator.java
@@ -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 {
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.
- *
- * Considers resolvable:
- *
- * - Unbounded method-level type parameters {@code T foo(Class)}
- * - Bounded method-level type parameters that resolve to a class
- * {@code T foo(Class)}
- * - Simple references to interface variables {@code T foo(), List foo(…)}
- * - Unbounded wildcards {@code User foo(GeoJson>)}
- *
- * Considers non-resolvable:
- *
- *
- *
- * @author Mark Paluch
- */
- record ResolvableGenerics(Method method, Class> implClass, Set resolvableTypeVariables,
- Set 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 getResolvableTypeVariables(Method method) {
-
- Set simpleTypeVariables = new HashSet<>();
-
- for (TypeVariable typeParameter : method.getTypeParameters()) {
- if (isClassBounded(typeParameter.getBounds())) {
- simpleTypeVariables.add(typeParameter);
- }
- }
-
- return simpleTypeVariables;
- }
-
- private static Set getUnwantedMethodVariables(Method method) {
-
- Set unwanted = new HashSet<>();
-
- for (TypeVariable 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 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.
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java
index cebc6013e..a41729d2e 100644
--- a/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java
+++ b/src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java
@@ -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;
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 {
ResolvableType repositoryInterface, Map methodArguments,
Map 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 {
}
}
+ @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;
}
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java b/src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java
new file mode 100644
index 000000000..f865d7366
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/aot/generate/ResolvableGenerics.java
@@ -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.
+ *
+ * Considers resolvable:
+ *
+ * - Unbounded method-level type parameters {@code T foo(Class)}
+ * - Bounded method-level type parameters that resolve to a class
+ * {@code T foo(Class)}
+ * - Simple references to interface variables {@code T foo(), List foo(…)}
+ * - Unbounded wildcards {@code User foo(GeoJson>)}
+ *
+ * Considers non-resolvable:
+ *
+ *
+ *
+ * @author Mark Paluch
+ */
+record ResolvableGenerics(Method method, Class> implClass, Set resolvableTypeVariables,
+ Set 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 getResolvableTypeVariables(Method method) {
+
+ Set simpleTypeVariables = new HashSet<>();
+
+ for (TypeVariable typeParameter : method.getTypeParameters()) {
+ if (isClassBounded(typeParameter.getBounds())) {
+ simpleTypeVariables.add(typeParameter);
+ }
+ }
+
+ return simpleTypeVariables;
+ }
+
+ private static Set getUnwantedMethodVariables(Method method) {
+
+ Set unwanted = new HashSet<>();
+
+ for (TypeVariable 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 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;
+ }
+}
diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
index 393e05bcc..72cf43f24 100644
--- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
+++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
@@ -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;
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;
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 {
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 {
.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 {
.doesNotContain("this.ctorScoped = ctorScoped");
}
- @Test
- // GH-3279
+ @Test // GH-3279
void appliesCtorCodeBlock() {
AotRepositoryCreator repositoryCreator = AotRepositoryCreator.forRepository(repositoryInformation, "Commons",
@@ -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 {
.containsIgnoringWhitespaces("void oops() { }");
}
- @Test
- // GH-3279
+ @Test // GH-3279
void appliesQueryMethodContributor() {
AotRepositoryInformation repositoryInformation = new AotRepositoryInformation(
@@ -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 {
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 {
.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 {
"public %s(List 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 {
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 firstname)") //
- assertThat(generated).doesNotContain("baseProjection").doesNotContain("upperBoundedProjection")
+ // List findByFirstname(String firstname, Class type);
+ .contains("public List findByFirstname(String firstname, Class type)")
+
+ // List project1ByFirstname(String firstname, Class type);
+ .contains("public List project2ByFirstname(String firstname, Class type)")
+
+ // List project2ByFirstname(String firstname, Class extends T> type)
+ .contains("public List project2ByFirstname(String firstname, Class type)")
+
+ // List geoQuery(GeoJson> geoJson);
+ .containsSubsequence("public List<", "User> geoQuery(", "GeoJson geoJson)")
+
+ // List genInputType(String firstname, T value);
+ .contains("public List genInputType(String firstname, Object value)")
+
+ .doesNotContain("baseProjection") //
+ .doesNotContain("upperBoundedProjection") //
.doesNotContain("lowerBoundedProjection()");
}
@@ -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 {
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 {
T parametrizedSelection(String firstname);
+ T typedInputParameter(T value);
+
}
interface UserRepository extends BaseRepository {
String someMethod();
+ List findByFirstnameIn(List firstname);
+
+ String findByPrimitive(int value);
+
+ String findByPrimitiveArray(int[] values);
+
List findByFirstname(String firstname, Class type);
+ List genInputType(String firstname, T value);
+
List project1ByFirstname(String firstname, Class super T> type);
List project2ByFirstname(String firstname, Class extends T> type);