Skip to content

Commit db5c5b3

Browse files
Refine TypeName discovery.
Add Additional tests for arrays and back off when detecting unresolvable return type. See: #3374 Original Pull Request: #3378
1 parent 26579e1 commit db5c5b3

File tree

11 files changed

+251
-38
lines changed

11 files changed

+251
-38
lines changed

src/main/java/org/springframework/data/javapoet/TypeNames.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
*/
1616
package org.springframework.data.javapoet;
1717

18+
import java.util.Arrays;
19+
1820
import org.springframework.core.ResolvableType;
21+
import org.springframework.javapoet.ArrayTypeName;
22+
import org.springframework.javapoet.ClassName;
23+
import org.springframework.javapoet.ParameterizedTypeName;
1924
import org.springframework.javapoet.TypeName;
2025
import org.springframework.util.ClassUtils;
2126

@@ -28,6 +33,7 @@
2833
* Mainly for internal use within the framework
2934
*
3035
* @author Mark Paluch
36+
* @author Christoph Strobl
3137
* @since 4.0
3238
*/
3339
public abstract class TypeNames {
@@ -65,6 +71,42 @@ public static TypeName className(ResolvableType resolvableType) {
6571
return TypeName.get(resolvableType.toClass());
6672
}
6773

74+
/**
75+
* Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a
76+
* type signature with resolved generics or a generic type variable.
77+
*
78+
* @param resolvableType the resolvable type represent.
79+
* @return the corresponding {@link TypeName}.
80+
*/
81+
public static TypeName resolvedTypeName(ResolvableType resolvableType) {
82+
83+
if (resolvableType.equals(ResolvableType.NONE)) {
84+
return TypeName.get(Object.class);
85+
}
86+
87+
if (!resolvableType.hasGenerics()) {
88+
Class<?> resolvedType = resolvableType.toClass();
89+
if (!resolvableType.isArray()) {
90+
return TypeName.get(resolvedType);
91+
}
92+
93+
if (resolvedType.isArray()) {
94+
return TypeName.get(resolvedType);
95+
}
96+
if (resolvableType.isArray()) {
97+
return ArrayTypeName.of(resolvedType);
98+
}
99+
return TypeName.get(resolvedType);
100+
}
101+
102+
if (resolvableType.hasResolvableGenerics()) {
103+
return ParameterizedTypeName.get(ClassName.get(resolvableType.toClass()),
104+
Arrays.stream(resolvableType.getGenerics()).map(TypeNames::resolvedTypeName).toArray(TypeName[]::new));
105+
}
106+
107+
return ClassName.get(resolvableType.toClass());
108+
}
109+
68110
/**
69111
* Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a
70112
* type signature or a generic type variable.
@@ -98,7 +140,7 @@ public static TypeName typeNameOrWrapper(Class<?> type) {
98140
public static TypeName typeNameOrWrapper(ResolvableType resolvableType) {
99141
return ClassUtils.isPrimitiveOrWrapper(resolvableType.toClass())
100142
? TypeName.get(ClassUtils.resolvePrimitiveIfNecessary(resolvableType.toClass()))
101-
: typeName(resolvableType);
143+
: resolvedTypeName(resolvableType);
102144
}
103145

104146
private TypeNames() {}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import org.apache.commons.logging.Log;
2828
import org.apache.commons.logging.LogFactory;
2929
import org.jspecify.annotations.Nullable;
30-
30+
import org.springframework.core.ResolvableType;
3131
import org.springframework.data.projection.ProjectionFactory;
3232
import org.springframework.data.repository.core.RepositoryInformation;
3333
import org.springframework.data.repository.core.support.RepositoryComposition;
@@ -298,6 +298,19 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
298298
return;
299299
}
300300

301+
// TODO: should we do this even before we do something with the method to protect the modules?
302+
if (ResolvableType.forMethodReturnType(method, repositoryInformation.getRepositoryInterface())
303+
.hasUnresolvableGenerics()) {
304+
305+
if (logger.isTraceEnabled()) {
306+
logger.trace("Skipping method [%s.%s] contribution, unresolvable generic return"
307+
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
308+
}
309+
310+
generationMetadata.addDelegateMethod(method, contributor);
311+
return;
312+
}
313+
301314
if (contributor.contributesMethodSpec() && !repositoryInformation.isReactiveRepository()) {
302315
generationMetadata.addRepositoryMethod(method, contributor);
303316
} else {

src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.jspecify.annotations.Nullable;
2828

2929
import org.springframework.core.ResolvableType;
30+
import org.springframework.data.javapoet.TypeNames;
3031
import org.springframework.data.repository.core.support.RepositoryFragment;
3132
import org.springframework.data.repository.query.QueryMethod;
3233
import org.springframework.javapoet.ParameterizedTypeName;
@@ -147,26 +148,10 @@ public Map<String, DelegateMethod> getDelegateMethods() {
147148
}
148149

149150
static TypeName typeNameOf(ResolvableType type) {
151+
return TypeNames.resolvedTypeName(type);
152+
}
150153

151-
if (type.equals(ResolvableType.NONE)) {
152-
return TypeName.get(Object.class);
153-
}
154-
155-
if (!type.hasResolvableGenerics()) {
156-
return TypeName.get(type.getType());
157-
}
158-
159-
return ParameterizedTypeName.get(type.toClass(), type.resolveGenerics());
160-
}
161-
162-
/**
163-
* Constructor argument metadata.
164-
*
165-
* @param parameterName
166-
* @param parameterType
167-
* @param bindToField
168-
*/
169-
public record ConstructorArgument(String parameterName, ResolvableType parameterType, boolean bindToField,
154+
public record ConstructorArgument(String parameterName, ResolvableType parameterType, boolean bindToField,
170155
AotRepositoryConstructorBuilder.ParameterOrigin parameterOrigin) {
171156

172157
boolean isBoundToField() {

src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import javax.lang.model.element.Modifier;
2626

27+
import org.springframework.data.javapoet.TypeNames;
2728
import org.springframework.javapoet.CodeBlock;
2829
import org.springframework.javapoet.MethodSpec;
2930
import org.springframework.javapoet.ParameterSpec;
@@ -101,7 +102,7 @@ public MethodSpec buildMethod() {
101102
private MethodSpec.Builder initializeMethodBuilder() {
102103

103104
MethodSpec.Builder builder = MethodSpec.methodBuilder(context.getMethod().getName()).addModifiers(Modifier.PUBLIC);
104-
builder.returns(TypeName.get(context.getReturnType().getType()));
105+
builder.returns(TypeNames.resolvedTypeName(context.getTargetMethodMetadata().getReturnType()));
105106

106107
TypeVariable<Method>[] tvs = context.getMethod().getTypeParameters();
107108
for (TypeVariable<Method> tv : tvs) {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.core.MethodParameter;
3232
import org.springframework.core.ParameterNameDiscoverer;
3333
import org.springframework.core.ResolvableType;
34+
import org.springframework.data.javapoet.TypeNames;
3435
import org.springframework.data.repository.core.RepositoryInformation;
3536
import org.springframework.data.util.TypeInformation;
3637
import org.springframework.javapoet.ParameterSpec;
@@ -80,11 +81,11 @@ private static void initializeMethodArguments(Method method, ParameterNameDiscov
8081

8182
for (Parameter parameter : method.getParameters()) {
8283

83-
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
84+
MethodParameter methodParameter = MethodParameter.forParameter(parameter).withContainingClass(repositoryInterface.resolve());
8485
methodParameter.initParameterNameDiscovery(nameDiscoverer);
85-
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter, repositoryInterface);
86+
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter);
8687

87-
TypeName parameterType = TypeName.get(resolvableParameterType.getType());
88+
TypeName parameterType = TypeNames.resolvedTypeName(resolvableParameterType);
8889

8990
ParameterSpec parameterSpec = ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build();
9091

src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public MethodReturn(ReturnedType returnedType, ResolvableType returnType) {
6161

6262
this.returnedType = returnedType;
6363
this.returnType = returnType;
64-
this.typeName = TypeNames.typeName(returnType);
64+
this.typeName = TypeNames.resolvedTypeName(returnType);
6565
this.className = TypeNames.className(returnType);
6666

6767
Class<?> returnClass = returnType.toClass();
@@ -72,7 +72,7 @@ public MethodReturn(ReturnedType returnedType, ResolvableType returnType) {
7272

7373
if (actualType != null) {
7474
this.actualType = actualType.toResolvableType();
75-
this.actualTypeName = TypeNames.typeName(this.actualType);
75+
this.actualTypeName = TypeNames.resolvedTypeName(this.actualType);
7676
this.actualClassName = TypeNames.className(this.actualType);
7777
this.actualReturnClass = actualType.getType();
7878
} else {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example;
17+
18+
import org.springframework.data.repository.CrudRepository;
19+
import org.springframework.data.repository.NoRepositoryBean;
20+
21+
/**
22+
* @author Christoph Strobl
23+
*/
24+
@NoRepositoryBean
25+
public interface BaseRepository<T, ID> extends CrudRepository<T, ID> {
26+
27+
T findInBaseRepository(ID id);
28+
}

src/test/java/example/UserRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* @author Christoph Strobl
2626
*/
27-
public interface UserRepository extends CrudRepository<User, Long>, UserRepositoryExtension {
27+
public interface UserRepository extends BaseRepository<User, Long>, UserRepositoryExtension {
2828

2929
User findByFirstname(String firstname);
3030

src/test/java/org/springframework/data/javapoet/TypeNamesUnitTests.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@
1717

1818
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
1919

20+
import java.util.List;
2021
import java.util.Set;
2122
import java.util.stream.Stream;
2223

24+
import org.junit.jupiter.api.Test;
2325
import org.junit.jupiter.params.ParameterizedTest;
2426
import org.junit.jupiter.params.provider.Arguments;
2527
import org.junit.jupiter.params.provider.MethodSource;
28+
import org.springframework.core.MethodParameter;
2629
import org.springframework.core.ResolvableType;
30+
import org.springframework.data.geo.Distance;
31+
import org.springframework.data.geo.GeoResult;
32+
import org.springframework.data.geo.Point;
33+
import org.springframework.javapoet.ClassName;
2734
import org.springframework.javapoet.ParameterizedTypeName;
2835
import org.springframework.javapoet.TypeName;
36+
import org.springframework.javapoet.TypeVariableName;
37+
import org.springframework.util.ReflectionUtils;
2938

3039
/**
3140
* @author Christoph Strobl
@@ -59,4 +68,105 @@ void classNames(ResolvableType resolvableType, TypeName expected) {
5968
assertThat(TypeNames.className(resolvableType)).isEqualTo(expected);
6069
}
6170

71+
@Test // GH-3374
72+
void resolvedTypeNamesWithoutGenerics() {
73+
74+
ResolvableType resolvableType = ResolvableType.forClass(List.class);
75+
assertThat(TypeNames.resolvedTypeName(resolvableType)).extracting(TypeName::toString).isEqualTo("java.util.List");
76+
}
77+
78+
@Test // GH-3374
79+
void resolvedTypeNamesForMethodParameters() {
80+
81+
ReflectionUtils.doWithMethods(Concrete.class, method -> {
82+
if (!method.getName().contains("baseMethod")) {
83+
return;
84+
}
85+
86+
MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class);
87+
ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter);
88+
assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("T"));
89+
assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(MyType.class));
90+
91+
MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1)
92+
.withContainingClass(Concrete.class);
93+
ResolvableType resolvedCollectionParameterType = ResolvableType
94+
.forMethodParameter(refiedCollectionMethodParameter);
95+
assertThat(TypeNames.typeName(resolvedCollectionParameterType))
96+
.isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T")));
97+
assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType))
98+
.isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class));
99+
100+
MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class);
101+
ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter);
102+
assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("T[]");
103+
assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString)
104+
.isEqualTo("org.springframework.data.javapoet.TypeNamesUnitTests.MyType[]");
105+
106+
ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class);
107+
assertThat(TypeNames.typeName(resolvedReturnType))
108+
.isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("T")));
109+
assertThat(TypeNames.resolvedTypeName(resolvedReturnType))
110+
.isEqualTo(ParameterizedTypeName.get(java.util.List.class, MyType.class));
111+
});
112+
113+
ReflectionUtils.doWithMethods(Concrete.class, method -> {
114+
if (!method.getName().contains("otherMethod")) {
115+
return;
116+
}
117+
118+
MethodParameter refiedObjectMethodParameter = new MethodParameter(method, 0).withContainingClass(Concrete.class);
119+
ResolvableType resolvedObjectParameterType = ResolvableType.forMethodParameter(refiedObjectMethodParameter);
120+
assertThat(TypeNames.typeName(resolvedObjectParameterType)).isEqualTo(TypeVariableName.get("RT"));
121+
assertThat(TypeNames.resolvedTypeName(resolvedObjectParameterType)).isEqualTo(TypeName.get(Object.class));
122+
123+
MethodParameter refiedCollectionMethodParameter = new MethodParameter(method, 1)
124+
.withContainingClass(Concrete.class);
125+
ResolvableType resolvedCollectionParameterType = ResolvableType
126+
.forMethodParameter(refiedCollectionMethodParameter);
127+
assertThat(TypeNames.typeName(resolvedCollectionParameterType))
128+
.isEqualTo(ParameterizedTypeName.get(ClassName.get(java.util.List.class), TypeVariableName.get("RT")));
129+
assertThat(TypeNames.resolvedTypeName(resolvedCollectionParameterType))
130+
.isEqualTo(ClassName.get(java.util.List.class));
131+
132+
MethodParameter refiedArrayMethodParameter = new MethodParameter(method, 2).withContainingClass(Concrete.class);
133+
ResolvableType resolvedArrayParameterType = ResolvableType.forMethodParameter(refiedArrayMethodParameter);
134+
assertThat(TypeNames.typeName(resolvedArrayParameterType)).extracting(TypeName::toString).isEqualTo("RT[]");
135+
assertThat(TypeNames.resolvedTypeName(resolvedArrayParameterType)).extracting(TypeName::toString)
136+
.isEqualTo("java.lang.Object[]");
137+
138+
ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class);
139+
assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo("RT");
140+
assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(TypeName.get(Object.class));
141+
});
142+
143+
ReflectionUtils.doWithMethods(Concrete.class, method -> {
144+
if (!method.getName().contains("findByLocationNear")) {
145+
return;
146+
}
147+
148+
ResolvableType resolvedReturnType = ResolvableType.forMethodReturnType(method, Concrete.class);
149+
150+
assertThat(TypeNames.typeName(resolvedReturnType)).extracting(TypeName::toString).isEqualTo(
151+
"java.util.List<org.springframework.data.geo.GeoResult<org.springframework.data.javapoet.TypeNamesUnitTests.MyType>>");
152+
assertThat(TypeNames.resolvedTypeName(resolvedReturnType)).isEqualTo(ParameterizedTypeName
153+
.get(ClassName.get(java.util.List.class), ParameterizedTypeName.get(GeoResult.class, MyType.class)));
154+
});
155+
156+
}
157+
158+
interface GenericBase<T> {
159+
160+
java.util.List<T> baseMethod(T arg0, java.util.List<T> arg1, T... arg2);
161+
162+
<RT> RT otherMethod(RT arg0, java.util.List<RT> arg1, RT... arg2);
163+
}
164+
165+
interface Concrete extends GenericBase<MyType> {
166+
167+
List<GeoResult<MyType>> findByLocationNear(Point point, Distance maxDistance);
168+
}
169+
170+
static class MyType {}
171+
62172
}

0 commit comments

Comments
 (0)