Skip to content

Commit 39f2945

Browse files
committed
TypeUtils does not handle generics with identical names in different positions
This commit improves the TypeUtils class to handle generic parameters that share similar names but are positioned differently. Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
1 parent 3e9dddb commit 39f2945

File tree

3 files changed

+114
-49
lines changed

3 files changed

+114
-49
lines changed

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java

+17-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@
2323
import java.util.HashMap;
2424
import java.util.List;
2525
import java.util.Map;
26-
import java.util.Map.Entry;
2726
import java.util.regex.Matcher;
2827
import java.util.regex.Pattern;
2928
import java.util.stream.Collectors;
@@ -135,7 +134,7 @@ String getType(TypeElement element, TypeMirror type) {
135134
if (type == null) {
136135
return null;
137136
}
138-
return type.accept(this.typeExtractor, createTypeDescriptor(element));
137+
return type.accept(this.typeExtractor, resolveTypeDescriptor(element));
139138
}
140139

141140
/**
@@ -394,37 +393,29 @@ static class TypeDescriptor {
394393

395394
private final Map<TypeVariable, TypeMirror> generics = new HashMap<>();
396395

397-
Map<TypeVariable, TypeMirror> getGenerics() {
398-
return Collections.unmodifiableMap(this.generics);
399-
}
400-
401396
TypeMirror resolveGeneric(TypeVariable typeVariable) {
402-
return resolveGeneric(getParameterName(typeVariable));
403-
}
404-
405-
TypeMirror resolveGeneric(String parameterName) {
406-
return this.generics.entrySet()
407-
.stream()
408-
.filter((e) -> getParameterName(e.getKey()).equals(parameterName))
409-
.findFirst()
410-
.map(Entry::getValue)
411-
.orElse(null);
397+
if (this.generics.containsKey(typeVariable)) {
398+
TypeMirror resolvedType = this.generics.get(typeVariable);
399+
// Unresolved <T> -> <T>
400+
if (resolvedType == typeVariable) {
401+
return resolvedType;
402+
}
403+
// <T> -> <T1> -> <T2>
404+
if (resolvedType instanceof TypeVariable) {
405+
return resolveGeneric((TypeVariable) resolvedType);
406+
}
407+
// Resolved e.g. java.lang.String
408+
return resolvedType;
409+
}
410+
return null;
412411
}
413412

414413
private void registerIfNecessary(TypeMirror variable, TypeMirror resolution) {
415414
if (variable instanceof TypeVariable typeVariable) {
416-
if (this.generics.keySet()
417-
.stream()
418-
.noneMatch((candidate) -> getParameterName(candidate).equals(getParameterName(typeVariable)))) {
419-
this.generics.put(typeVariable, resolution);
420-
}
415+
this.generics.put(typeVariable, resolution);
421416
}
422417
}
423418

424-
private String getParameterName(TypeVariable typeVariable) {
425-
return typeVariable.asElement().getSimpleName().toString();
426-
}
427-
428419
}
429420

430421
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,16 +17,23 @@
1717
package org.springframework.boot.configurationprocessor;
1818

1919
import java.time.Duration;
20+
import java.util.Map;
2021
import java.util.function.BiConsumer;
2122

23+
import javax.lang.model.element.TypeElement;
24+
import javax.lang.model.element.VariableElement;
25+
import javax.lang.model.type.TypeMirror;
26+
import javax.lang.model.util.ElementFilter;
27+
2228
import org.junit.jupiter.api.Test;
2329

24-
import org.springframework.boot.configurationprocessor.TypeUtils.TypeDescriptor;
2530
import org.springframework.boot.configurationprocessor.test.RoundEnvironmentTester;
2631
import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor;
2732
import org.springframework.boot.configurationsample.generic.AbstractGenericProperties;
2833
import org.springframework.boot.configurationsample.generic.AbstractIntermediateGenericProperties;
34+
import org.springframework.boot.configurationsample.generic.MixGenericNameProperties;
2935
import org.springframework.boot.configurationsample.generic.SimpleGenericProperties;
36+
import org.springframework.boot.configurationsample.generic.UnresolvedGenericProperties;
3037
import org.springframework.core.test.tools.SourceFile;
3138
import org.springframework.core.test.tools.TestCompiler;
3239

@@ -41,40 +48,53 @@
4148
class TypeUtilsTests {
4249

4350
@Test
44-
void resolveTypeDescriptorOnConcreteClass() {
51+
void resolveTypeOnConcreteClass() {
4552
process(SimpleGenericProperties.class, (roundEnv, typeUtils) -> {
46-
TypeDescriptor typeDescriptor = typeUtils
47-
.resolveTypeDescriptor(roundEnv.getRootElement(SimpleGenericProperties.class));
48-
assertThat(typeDescriptor.getGenerics().keySet().stream().map(Object::toString)).containsOnly("A", "B",
49-
"C");
50-
assertThat(typeDescriptor.resolveGeneric("A")).hasToString(String.class.getName());
51-
assertThat(typeDescriptor.resolveGeneric("B")).hasToString(Integer.class.getName());
52-
assertThat(typeDescriptor.resolveGeneric("C")).hasToString(Duration.class.getName());
53+
TypeElement typeElement = roundEnv.getRootElement(SimpleGenericProperties.class);
54+
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
55+
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
56+
.hasToString(constructMapType(Integer.class, Duration.class));
5357

5458
});
5559
}
5660

5761
@Test
58-
void resolveTypeDescriptorOnIntermediateClass() {
62+
void resolveTypeOnIntermediateClass() {
5963
process(AbstractIntermediateGenericProperties.class, (roundEnv, typeUtils) -> {
60-
TypeDescriptor typeDescriptor = typeUtils
61-
.resolveTypeDescriptor(roundEnv.getRootElement(AbstractIntermediateGenericProperties.class));
62-
assertThat(typeDescriptor.getGenerics().keySet().stream().map(Object::toString)).containsOnly("A", "B",
63-
"C");
64-
assertThat(typeDescriptor.resolveGeneric("A")).hasToString(String.class.getName());
65-
assertThat(typeDescriptor.resolveGeneric("B")).hasToString(Integer.class.getName());
66-
assertThat(typeDescriptor.resolveGeneric("C")).hasToString("C");
64+
TypeElement typeElement = roundEnv.getRootElement(AbstractIntermediateGenericProperties.class);
65+
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
66+
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
67+
.hasToString(constructMapType(Integer.class, Object.class));
6768
});
6869
}
6970

7071
@Test
71-
void resolveTypeDescriptorWithOnlyGenerics() {
72+
void resolveTypeWithOnlyGenerics() {
7273
process(AbstractGenericProperties.class, (roundEnv, typeUtils) -> {
73-
TypeDescriptor typeDescriptor = typeUtils
74-
.resolveTypeDescriptor(roundEnv.getRootElement(AbstractGenericProperties.class));
75-
assertThat(typeDescriptor.getGenerics().keySet().stream().map(Object::toString)).containsOnly("A", "B",
76-
"C");
74+
TypeElement typeElement = roundEnv.getRootElement(AbstractGenericProperties.class);
75+
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(Object.class.getName());
76+
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
77+
.hasToString(constructMapType(Object.class, Object.class));
78+
});
79+
}
80+
81+
@Test
82+
void resolveTypeWithUnresolvedGenericProperties() {
83+
process(UnresolvedGenericProperties.class, (roundEnv, typeUtils) -> {
84+
TypeElement typeElement = roundEnv.getRootElement(UnresolvedGenericProperties.class);
85+
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
86+
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
87+
.hasToString(constructMapType(Number.class, Object.class));
88+
});
89+
}
7790

91+
@Test
92+
void resolvedTypeMixGenericNamePropertiesProperties() {
93+
process(MixGenericNameProperties.class, (roundEnv, typeUtils) -> {
94+
TypeElement typeElement = roundEnv.getRootElement(MixGenericNameProperties.class);
95+
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
96+
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
97+
.hasToString(constructMapType(Number.class, Object.class));
7898
});
7999
}
80100

@@ -87,4 +107,29 @@ private void process(Class<?> target, BiConsumer<RoundEnvironmentTester, TypeUti
87107
});
88108
}
89109

110+
private String constructMapType(Class<?> keyType, Class<?> valueType) {
111+
return "%s<%s,%s>".formatted(Map.class.getName(), keyType.getName(), valueType.getName());
112+
}
113+
114+
private String getTypeOfField(TypeUtils typeUtils, TypeElement typeElement, String name) {
115+
TypeMirror field = findField(typeUtils, typeElement, name);
116+
if (field == null) {
117+
throw new IllegalStateException("Unable to find field '" + name + "' in " + typeElement);
118+
}
119+
return typeUtils.getType(typeElement, field);
120+
}
121+
122+
private TypeMirror findField(TypeUtils typeUtils, TypeElement typeElement, String name) {
123+
for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {
124+
if (variableElement.getSimpleName().contentEquals(name)) {
125+
return variableElement.asType();
126+
}
127+
}
128+
TypeMirror superclass = typeElement.getSuperclass();
129+
if (superclass != null && !superclass.toString().equals(Object.class.getName())) {
130+
return findField(typeUtils, (TypeElement) typeUtils.asElement(superclass), name);
131+
}
132+
return null;
133+
}
134+
90135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2025 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+
* https://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+
17+
package org.springframework.boot.configurationsample.generic;
18+
19+
/**
20+
* Properties with unresolved generic types that use identical generic parameter names but
21+
* differ in their positions.
22+
*
23+
* @param <C> mapping name type
24+
* @param <B> mapping value type
25+
* @author Dmytro Nosan
26+
*/
27+
public class MixGenericNameProperties<B, C extends Number> extends AbstractGenericProperties<String, C, B> {
28+
29+
}

0 commit comments

Comments
 (0)