From 1633f41727c5d6009dd2dba73573dd9d48273c80 Mon Sep 17 00:00:00 2001 From: junhyeong9812 Date: Wed, 22 Jul 2026 08:59:33 +0900 Subject: [PATCH] Restrict type variable name fallback to subtype narrowing The name-based fallback in resolveVariable(TypeVariable) matched type variables purely by name once the owner type was null, so for top-level declarations a variable could be resolved against an unrelated declaration that merely shares the same name. The identity-based match introduced in gh-36890 avoids this for nested types (their owner recursion returns before the fallback), but top-level types such as a controller implementing two generic interfaces with identically named type variables still resolved to the wrong argument. Guard the fallback so it only applies when the variable is declared by a class that the resolved type is assignable from, i.e. a genuine subtype narrowing a parameterized supertype (such as ArrayList narrowing List). A name match against a sibling declaration or a method-level type variable is no longer accepted. See gh-36890 Signed-off-by: junhyeong9812 --- .../springframework/core/ResolvableType.java | 13 +++-- .../core/GenericTypeResolverTests.java | 53 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) 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 { +}