[ 
https://issues.apache.org/jira/browse/LANG-1836?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18123785#comment-18123785
 ] 

Gary D. Gregory commented on LANG-1836:
---------------------------------------

Hello [~dwilliam] 

Thank you for your report.

Feel free to propose a PR on GitHub.

> TypeUtils.containsTypeVariables(Type) ignores the owner type of a 
> ParameterizedType
> -----------------------------------------------------------------------------------
>
>                 Key: LANG-1836
>                 URL: https://issues.apache.org/jira/browse/LANG-1836
>             Project: Commons Lang
>          Issue Type: Bug
>    Affects Versions: 3.21.0
>            Reporter: William Degrange
>            Priority: Major
>
> *Description*
> {{TypeUtils.containsTypeVariables(Type)}} returns {{false}} for a 
> {{ParameterizedType}} whose owner type contains a type variable. The 
> {{ParameterizedType}} branch only checks {{{}getActualTypeArguments(){}}}; 
> {{getOwnerType()}} is never inspected.
> This contradicts the Javadoc ("Tests, recursively, whether any of the type 
> parameters associated with {{type}} are bound to variables"): the type 
> parameters of the owner are part of the type's parameterization.
> Also, in the {{ParameterizedType}} branch of 
> {{{}TypeUtils.unrollVariables(Map, Type){}}}:
>  # *The owner type is never unrolled.* Only {{getActualTypeArguments()}} are 
> processed; the result is built with 
> {{{}parameterizeWithOwner(p.getOwnerType(), ...){}}}, i.e. the original 
> owner. Type variables in the owner are left unresolved. (When the owner is 
> the only place holding variables, the method returns early because 
> {{containsTypeVariables}} ignores the owner, see the related issue. Fixing 
> that issue alone is not enough.)
>  # *Caller assignments are overridden.* When the owner is not null, 
> {{getTypeArguments(p)}} is merged into the caller's map with {{{}putAll{}}}. 
> For {{{}Outer<T>.Inner<T>{}}}, {{getTypeArguments(p)}} returns \{T -> T, U -> 
> T}, and the identity entry {{T -> T}} replaces the caller's {{{}T -> 
> String{}}}.
> This causes infinite recursion.
> *Steps to reproduce*
> {code:java}
> import java.lang.reflect.Type;
> import java.lang.reflect.TypeVariable;
> import java.util.Collections;
> import org.apache.commons.lang3.reflect.TypeUtils;
> public class ContainsTypeVariablesOwnerRepro {    
>     static class Outer<T> {
>         class Inner {}
>         Inner inner; // generic type is Outer<T>.Inner
>     }
>     public static void main(String[] args) throws Exception {
>         Type fromReflection = 
> Outer.class.getDeclaredField("inner").getGenericType();
>         System.out.println(TypeUtils.containsTypeVariables(fromReflection)); 
> // false, expected true
>         TypeVariable<?> t = Outer.class.getTypeParameters()[0];
>         Type built = 
> TypeUtils.parameterizeWithOwner(TypeUtils.parameterize(Outer.class, t), 
> Outer.Inner.class);
>         System.out.println(TypeUtils.containsTypeVariables(built)); // false, 
> expected true  
>       
>         // Consequence: unrollVariables short-circuits and does not 
> substitute T
>         
> System.out.println(TypeUtils.unrollVariables(Collections.singletonMap(t, 
> String.class), fromReflection));
>         // prints Outer<T>.Inner, expected Outer<String>.Inner
>     }
> }{code}
> {code:java}
> import java.lang.reflect.Type;
> import java.lang.reflect.TypeVariable;
> import java.util.Collections;
> import java.util.Map;
> import org.apache.commons.lang3.reflect.TypeUtils;
> public class UnrollOwnerRepro {    
>     static class Outer<T> {
>         class Inner<U> {}
>         Inner<Integer> a; // Outer<T>.Inner<Integer>
>         Inner<T> b;       // Outer<T>.Inner<T>
>     }    
>     public static void main(String[] args) throws Exception {
>         TypeVariable<?> t = Outer.class.getTypeParameters()[0];
>         Map<TypeVariable<?>, Type> map = Collections.singletonMap(t, 
> String.class);    
>     
>         Type a = Outer.class.getDeclaredField("a").getGenericType();
>         System.out.println(TypeUtils.unrollVariables(map, a));
>         // actual:   Outer<T>.Inner<java.lang.Integer>
>         // expected: Outer<java.lang.String>.Inner<java.lang.Integer>      
>   
>         Type b = Outer.class.getDeclaredField("b").getGenericType();
>         System.out.println(TypeUtils.unrollVariables(map, b));
>         // actual:   java.lang.StackOverflowError
>         // expected: Outer<java.lang.String>.Inner<java.lang.String>
>     }
>     
> } {code}
> *Suggested fix*
> Recurse into the owner type, but only when it is not a raw {{{}Class{}}}. For 
> a static nested type such as {{{}Map.Entry<String, Integer>{}}}, the owner is 
> the raw {{{}Map.class{}}}, and the {{Class}} branch would return {{true}} 
> because {{Map}} declares type parameters:
> {code:java}
> if (type instanceof ParameterizedType) {
>     final ParameterizedType parameterizedType = (ParameterizedType) type;
>     for (final Type arg : parameterizedType.getActualTypeArguments()) {
>         if (containsTypeVariables(arg)) {
>             return true;
>         }
>     }
>     final Type ownerType = parameterizedType.getOwnerType();
>     return ownerType != null && !(ownerType instanceof Class<?>) && 
> containsTypeVariables(ownerType);
> } {code}
>  Unroll the owner like the arguments, with the caller's assignments, and keep 
> the owner unchanged when it is a raw {{Class}} (static nested types such as 
> {{{}Map.Entry{}}}):
> {code:java}
> if (type instanceof ParameterizedType) {
>     final ParameterizedType p = (ParameterizedType) type;
>     final Type[] args = p.getActualTypeArguments().clone();
>     for (int i = 0; i < args.length; i++) {
>         final Type unrolled = unrollVariables(typeArguments, args[i], 
> visited);
>         if (unrolled != null) {
>             args[i] = unrolled;
>         }
>     }
>     Type owner = p.getOwnerType();
>     if (owner != null && !(owner instanceof Class<?>)) {
>         final Type unrolledOwner = unrollVariables(typeArguments, owner, 
> visited);
>         if (unrolledOwner != null) {
>             owner = unrolledOwner;
>         }
>     }
>     return parameterizeWithOwner(owner, (Class<?>) p.getRawType(), args);
> } {code}
>  
> The {{putAll(getTypeArguments(p))}} merge is dropped. The actual type 
> arguments of {{p}} are expressed in terms of the enclosing scope's variables, 
> not the raw type's own parameters, so merging the raw type's parameter 
> mapping is not needed to resolve them. It is also what introduces the {{T -> 
> T}} identity entry. If maintainers rely on that merge elsewhere, an 
> alternative is to never let it override existing caller entries.
> Suggested regression tests (1):
>  * {{Outer<T>.Inner}} → {{true}}
>  * {{Outer<String>.Inner}} → {{false}}
>  * {{Map.Entry<String, Integer>}} → {{false}} (the owner is the raw 
> {{{}Map.class{}}})
> Suggested regression tests (2), with \{T -> String} :
>  * {{Outer<T>.Inner<Integer>}} → {{Outer<String>.Inner<Integer>}}
>  * {{Outer<T>.Inner<T>}} → {{Outer<String>.Inner<String>}}
>  * {{Map.Entry<T, Integer>}} → {{Map.Entry<String, Integer>}} (the raw owner 
> {{Map.class}} is left unchanged)



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to