Skip to content

Restrict type variable name fallback to subtype narrowing#37082

Open
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/resolvabletype-toplevel-variable-name-fallback
Open

Restrict type variable name fallback to subtype narrowing#37082
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/resolvabletype-toplevel-variable-name-fallback

Conversation

@junhyeong9812

Copy link
Copy Markdown
Contributor

Overview

Follow-up to gh-36890. That fix resolves same-named type variables against the correct declaration for nested types, but the same scenario is still broken for top-level declarations, which is exactly the shape of the originally reported case (a REST controller implementing two generic interfaces).

Problem

ResolvableType.resolveVariable(TypeVariable) falls back to matching type variables purely by name when the parameterized type has no owner type:

Type ownerType = parameterizedType.getOwnerType();
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);
    }
}

For a nested type the owner recursion returns before the fallback is reached, so gh-36890's regression test (which uses nested types) passes. But a top-level type has a null owner type and therefore still falls through to the name-only comparison, matching an unrelated declaration that merely shares a variable name.

Given:

interface TopSearch<I, O> {}
interface TopCreate<I, O> { O create(I body); }
class TopController implements TopSearch<String, Long>, TopCreate<Long, Long> {}

resolving TopCreate's I against TopController returns String (from the sibling TopSearch.I) instead of Long. The identical structure nested inside a class resolves to Long, so nested and top-level diverge. The same name-only fallback also lets a class-level type argument leak into a shadowing method-level type variable.

Fix

Guard the name fallback so it only applies to a genuine subtype narrowing a parameterized supertype (for example ArrayList narrowing List<String>), which is the one case where matching differently-declared variables by name is legitimate:

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);
        }
    }
}

A name match against a sibling declaration or a method-level type variable is no longer accepted.

Added three tests in GenericTypeResolverTests: the top-level sibling-interface case, the method-level shadowing case, and a positive test confirming the legitimate subtype-narrowing path still resolves by name. The existing ResolvableTypeTests.narrow() and gh-34386/gh-36890 tests continue to pass.

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
spring-projectsgh-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<String>). A name match against a sibling declaration or a method-level
type variable is no longer accepted.

See spring-projectsgh-36890

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged or decided on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants