Handle generics with identical names in different positions

Update `TypeUtils` to handle generics with identical names in different
positions.

See gh-45011

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
This commit is contained in:
Dmytro Nosan
2025-04-05 21:16:44 +03:00
committed by Phillip Webb
parent 8dd0c91fe5
commit e24fd50615
3 changed files with 114 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@@ -23,7 +23,6 @@ import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -135,7 +134,7 @@ class TypeUtils {
if (type == null) {
return null;
}
return type.accept(this.typeExtractor, createTypeDescriptor(element));
return type.accept(this.typeExtractor, resolveTypeDescriptor(element));
}
/**
@@ -394,37 +393,29 @@ class TypeUtils {
private final Map<TypeVariable, TypeMirror> generics = new HashMap<>();
Map<TypeVariable, TypeMirror> getGenerics() {
return Collections.unmodifiableMap(this.generics);
}
TypeMirror resolveGeneric(TypeVariable typeVariable) {
return resolveGeneric(getParameterName(typeVariable));
}
TypeMirror resolveGeneric(String parameterName) {
return this.generics.entrySet()
.stream()
.filter((e) -> getParameterName(e.getKey()).equals(parameterName))
.findFirst()
.map(Entry::getValue)
.orElse(null);
if (this.generics.containsKey(typeVariable)) {
TypeMirror resolvedType = this.generics.get(typeVariable);
// Unresolved <T> -> <T>
if (resolvedType == typeVariable) {
return resolvedType;
}
// <T> -> <T1> -> <T2>
if (resolvedType instanceof TypeVariable) {
return resolveGeneric((TypeVariable) resolvedType);
}
// Resolved e.g. java.lang.String
return resolvedType;
}
return null;
}
private void registerIfNecessary(TypeMirror variable, TypeMirror resolution) {
if (variable instanceof TypeVariable typeVariable) {
if (this.generics.keySet()
.stream()
.noneMatch((candidate) -> getParameterName(candidate).equals(getParameterName(typeVariable)))) {
this.generics.put(typeVariable, resolution);
}
this.generics.put(typeVariable, resolution);
}
}
private String getParameterName(TypeVariable typeVariable) {
return typeVariable.asElement().getSimpleName().toString();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 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.
@@ -17,16 +17,23 @@
package org.springframework.boot.configurationprocessor;
import java.time.Duration;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.junit.jupiter.api.Test;
import org.springframework.boot.configurationprocessor.TypeUtils.TypeDescriptor;
import org.springframework.boot.configurationprocessor.test.RoundEnvironmentTester;
import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor;
import org.springframework.boot.configurationsample.generic.AbstractGenericProperties;
import org.springframework.boot.configurationsample.generic.AbstractIntermediateGenericProperties;
import org.springframework.boot.configurationsample.generic.MixGenericNameProperties;
import org.springframework.boot.configurationsample.generic.SimpleGenericProperties;
import org.springframework.boot.configurationsample.generic.UnresolvedGenericProperties;
import org.springframework.core.test.tools.SourceFile;
import org.springframework.core.test.tools.TestCompiler;
@@ -41,40 +48,53 @@ import static org.assertj.core.api.Assertions.assertThat;
class TypeUtilsTests {
@Test
void resolveTypeDescriptorOnConcreteClass() {
void resolveTypeOnConcreteClass() {
process(SimpleGenericProperties.class, (roundEnv, typeUtils) -> {
TypeDescriptor typeDescriptor = typeUtils
.resolveTypeDescriptor(roundEnv.getRootElement(SimpleGenericProperties.class));
assertThat(typeDescriptor.getGenerics().keySet().stream().map(Object::toString)).containsOnly("A", "B",
"C");
assertThat(typeDescriptor.resolveGeneric("A")).hasToString(String.class.getName());
assertThat(typeDescriptor.resolveGeneric("B")).hasToString(Integer.class.getName());
assertThat(typeDescriptor.resolveGeneric("C")).hasToString(Duration.class.getName());
TypeElement typeElement = roundEnv.getRootElement(SimpleGenericProperties.class);
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
.hasToString(constructMapType(Integer.class, Duration.class));
});
}
@Test
void resolveTypeDescriptorOnIntermediateClass() {
void resolveTypeOnIntermediateClass() {
process(AbstractIntermediateGenericProperties.class, (roundEnv, typeUtils) -> {
TypeDescriptor typeDescriptor = typeUtils
.resolveTypeDescriptor(roundEnv.getRootElement(AbstractIntermediateGenericProperties.class));
assertThat(typeDescriptor.getGenerics().keySet().stream().map(Object::toString)).containsOnly("A", "B",
"C");
assertThat(typeDescriptor.resolveGeneric("A")).hasToString(String.class.getName());
assertThat(typeDescriptor.resolveGeneric("B")).hasToString(Integer.class.getName());
assertThat(typeDescriptor.resolveGeneric("C")).hasToString("C");
TypeElement typeElement = roundEnv.getRootElement(AbstractIntermediateGenericProperties.class);
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
.hasToString(constructMapType(Integer.class, Object.class));
});
}
@Test
void resolveTypeDescriptorWithOnlyGenerics() {
void resolveTypeWithOnlyGenerics() {
process(AbstractGenericProperties.class, (roundEnv, typeUtils) -> {
TypeDescriptor typeDescriptor = typeUtils
.resolveTypeDescriptor(roundEnv.getRootElement(AbstractGenericProperties.class));
assertThat(typeDescriptor.getGenerics().keySet().stream().map(Object::toString)).containsOnly("A", "B",
"C");
TypeElement typeElement = roundEnv.getRootElement(AbstractGenericProperties.class);
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(Object.class.getName());
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
.hasToString(constructMapType(Object.class, Object.class));
});
}
@Test
void resolveTypeWithUnresolvedGenericProperties() {
process(UnresolvedGenericProperties.class, (roundEnv, typeUtils) -> {
TypeElement typeElement = roundEnv.getRootElement(UnresolvedGenericProperties.class);
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
.hasToString(constructMapType(Number.class, Object.class));
});
}
@Test
void resolvedTypeMixGenericNamePropertiesProperties() {
process(MixGenericNameProperties.class, (roundEnv, typeUtils) -> {
TypeElement typeElement = roundEnv.getRootElement(MixGenericNameProperties.class);
assertThat(getTypeOfField(typeUtils, typeElement, "name")).hasToString(String.class.getName());
assertThat(getTypeOfField(typeUtils, typeElement, "mappings"))
.hasToString(constructMapType(Number.class, Object.class));
});
}
@@ -87,4 +107,29 @@ class TypeUtilsTests {
});
}
private String constructMapType(Class<?> keyType, Class<?> valueType) {
return "%s<%s,%s>".formatted(Map.class.getName(), keyType.getName(), valueType.getName());
}
private String getTypeOfField(TypeUtils typeUtils, TypeElement typeElement, String name) {
TypeMirror field = findField(typeUtils, typeElement, name);
if (field == null) {
throw new IllegalStateException("Unable to find field '" + name + "' in " + typeElement);
}
return typeUtils.getType(typeElement, field);
}
private TypeMirror findField(TypeUtils typeUtils, TypeElement typeElement, String name) {
for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {
if (variableElement.getSimpleName().contentEquals(name)) {
return variableElement.asType();
}
}
TypeMirror superclass = typeElement.getSuperclass();
if (superclass != null && !superclass.toString().equals(Object.class.getName())) {
return findField(typeUtils, (TypeElement) typeUtils.asElement(superclass), name);
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2012-2025 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.boot.configurationsample.generic;
/**
* Properties with unresolved generic types that use identical generic parameter names but
* differ in their positions.
*
* @param <C> mapping name type
* @param <B> mapping value type
* @author Dmytro Nosan
*/
public class MixGenericNameProperties<B, C extends Number> extends AbstractGenericProperties<String, C, B> {
}