diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 3d69e9fd92c2..e8057a21cf75 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -963,10 +963,15 @@ ResolvableType resolveType() { if (ownerType != null) { return forType(ownerType, this.variableResolver).resolveVariable(variableToCompare); } - // Fallback: comparison by variable name, independent of generic declaration context. - for (int i = 0; i < variables.length; i++) { - if (ObjectUtils.nullSafeEquals(variables[i].getName(), variableToCompare.getName())) { - return forType(typeArguments[i], this.variableResolver); + // Fallback: comparison by variable name, limited to a subtype narrowing the + // resolved supertype (for example, ArrayList narrowing List). A name + // match against an unrelated declaration must not be accepted (gh-36890). + if (variableToCompare.getGenericDeclaration() instanceof Class declaringClass && + resolved.isAssignableFrom(declaringClass)) { + for (int i = 0; i < variables.length; i++) { + if (ObjectUtils.nullSafeEquals(variables[i].getName(), variableToCompare.getName())) { + return forType(typeArguments[i], this.variableResolver); + } } } } diff --git a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java index 150259ce79ed..b0e91290b3a8 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -266,6 +266,29 @@ void resolveTypeAgainstSameNamedVariables() { assertThat(resolvedType).isEqualTo(Long.class); } + @Test // gh-36890 + void resolveTypeAgainstSameNamedVariablesInTopLevelDeclarations() { + Type resolvedType = resolveType( + method(TopCreate.class, "create", Object.class).getGenericParameterTypes()[0], TopController.class); + assertThat(resolvedType).isEqualTo(Long.class); + } + + @Test // gh-36890 + void resolveMethodLevelTypeVariableIsNotShadowedByClassVariable() { + Type resolvedType = resolveType( + method(TopRepo.class, "convert", Object.class).getGenericReturnType(), TopStringRepo.class); + assertThat(resolvedType).isInstanceOf(TypeVariable.class); + } + + @Test // gh-36890 + void resolveTypeVariableByNameWhenNarrowingParameterizedSupertype() { + // A raw subtype narrowing a parameterized supertype must still carry the argument + // across by variable name, even though Box and Container are distinct declarations. + ResolvableType containerOfString = ResolvableType.forClassWithGenerics(Container.class, String.class); + ResolvableType box = ResolvableType.forType(Box.class, containerOfString); + assertThat(box.getGeneric().resolve()).isEqualTo(String.class); + } + private static Method method(Class target, String methodName, Class... parameterTypes) { Method method = findMethod(target, methodName, parameterTypes); assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull(); @@ -523,3 +546,33 @@ static class Controller implements Search, Create { } } + + +interface TopSearch { +} + +interface TopCreate { + + default O create(I body) { + return null; + } +} + +class TopController implements TopSearch, TopCreate { +} + +class TopRepo { + + T convert(Object o) { + return null; + } +} + +class TopStringRepo extends TopRepo { +} + +interface Container { +} + +class Box implements Container { +}