Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve method type parameters using argument types #3771

Open
michaelhixson opened this issue Jan 15, 2020 · 7 comments
Open

Resolve method type parameters using argument types #3771

michaelhixson opened this issue Jan 15, 2020 · 7 comments

Comments

@michaelhixson
Copy link

Add some API for resolving the type parameters of methods, in particular when some or all of the argument types for a method invocation are known.

TypeToken#resolveType(Type) is useful for resolving type parameters of classes, but I don't see a way to resolve type parameters of methods.

Take the method Lists#reverse(List) for example. Suppose I have obtained the following somehow.

  • The java.lang.reflect.Method object representing Lists#reverse(List). (I just have a reference to a Method. I don't have static knowledge that it's the reverse method specifically.)
  • The actual Type of the argument I'd like to provide to this method, such as ArrayList<String>.
  • The actual Type of the variable in which I'd like to store the result returned from this method, such as Collection<String>.

and I want to answer these questions.

  • Is it type safe to provide this argument to that method? (yes)
  • Is it type safe to store the result of this method into this variable? (yes)

I don't think there is any API for answering either of those questions right now. This code doesn't get there.

Invokable<Lists, Object> method =
    TypeToken.of(Lists.class).method(
        Lists.class.getMethod("reverse", List.class));
System.out.println(method);
// public static <T> List<T> reverse(List<T> list)

TypeToken<?> parameterType = method.getParameters().get(0).getType();
System.out.println(parameterType);
// List<T>

TypeToken<?> returnType = method.getReturnType();
System.out.println(returnType);
// List<T>

TypeToken<?> argumentType = new TypeToken<ArrayList<String>>() {};
System.out.println(argumentType);
// ArrayList<String>

TypeToken<?> variableType = new TypeToken<Collection<String>>() {};
System.out.println(variableType);
// Collection<String>

boolean isArgumentSafe = argumentType.isSubtypeOf(parameterType);
System.out.println(isArgumentSafe);
// false (but that's not right...)

boolean isVariableSafe = returnType.isSubtypeOf(variableType);
System.out.println(isVariableSafe);
// false (also not right...)

// See if using TypeToken#resolveType(Type) helps.

TypeToken<?> wrongResolvedParameterType =
    argumentType.resolveType(parameterType.getType());
System.out.println(wrongResolvedParameterType);
// List<T> (nope...)
// probably because the argument's T comes from List.class,
// whereas this T comes from the Lists.reverse method.

TypeToken<?> wrongResolvedReturnType =
    argumentType.resolveType(returnType.getType());
System.out.println(wrongResolvedReturnType);
// List<T> (nope...)

I'm not sure what such an API would look like. Here's one idea.

Invokable<Lists, Object> resolvedMethod =
    method.withArgumentTypes(argumentType);
//         ^ new method

System.out.println(resolvedMethod.getParameters().get(0).getType());
// List<String>

System.out.println(resolvedMethod.getReturnType());
// List<String>
@fluentfuture
Copy link

Maybe something like this?

new TypeResolver()
    .where(invokable.getParameters().get(0).getType().getType(), actualArgType)
    .resolveType(invokable.getReturnType().getType());

@michaelhixson
Copy link
Author

I get IllegalArgumentException: Inconsistent raw type: java.util.List<T> vs. java.util.ArrayList<java.lang.String> from TypeResolver.visitParameterizedType, but it seems happy if I upcast my argument type to the parameter type.

Type resolvedParameterType =
    new TypeResolver()
        .where(
            parameterType.getType(),
            argumentType.getSupertype((Class) parameterType.getRawType()).getType())
        .resolveType(parameterType.getType());

System.out.println(resolvedParameterType);
// List<String>

Type resolvedReturnType =
    new TypeResolver()
        .where(
            parameterType.getType(),
            argumentType.getSupertype((Class) parameterType.getRawType()).getType())
        .resolveType(returnType.getType());

System.out.println(resolvedReturnType);
// List<String>

Thanks! I forgot that TypeResolver is public. I want to take a closer look at this tomorrow but maybe this gives me all the tools I could need.

@michaelhixson
Copy link
Author

michaelhixson commented Jan 16, 2020

There are some issues with this approach, which I've tried to illustrate in this gist: https://gist.github.com/michaelhixson/bd159355c8ebf454882fb9672c84bb92

The issues make me think TypeResolver.where wasn't designed to solve this problem, and so even where it "works" I'm nervous that it's working for the wrong reasons.

Is the gist working as intended (and so there really is no API for solving this problem), or is it highlighting bugs in TypeResolver? Assuming there aren't bugs in the gist itself...

@fluentfuture
Copy link

Yeah. Wildcard is likely a problem. For example in where(<? extends T>, String), we don't yet have logic to try to infer T, and actually, it may not always be inferred to String, since maybe you'll have another where() call that further restricts it to a supertype.

@michaelhixson
Copy link
Author

michaelhixson commented Jan 16, 2020

The following patch to TypeResolver makes all the test cases in my gist work as expected. Do you think this is worth putting into a PR?

diff --git a/guava/src/com/google/common/reflect/TypeResolver.java b/guava/src/com/google/common/reflect/TypeResolver.java
index 339eb43b5..cecbc376e 100644
--- a/guava/src/com/google/common/reflect/TypeResolver.java
+++ b/guava/src/com/google/common/reflect/TypeResolver.java
@@ -30,6 +30,7 @@ import java.lang.reflect.Type;
 import java.lang.reflect.TypeVariable;
 import java.lang.reflect.WildcardType;
 import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -135,7 +136,17 @@ public final class TypeResolver {
       @Override
       void visitWildcardType(WildcardType fromWildcardType) {
         if (!(to instanceof WildcardType)) {
-          return; // okay to say <?> is anything
+          for (Type fromUpperBound : fromWildcardType.getUpperBounds()) {
+            if (!(fromUpperBound instanceof Class)) {
+              populateTypeMappings(mappings, fromUpperBound, to);
+            }
+          }
+          for (Type fromLowerBound : fromWildcardType.getLowerBounds()) {
+            if (!(fromLowerBound instanceof Class)) {
+              populateTypeMappings(mappings, fromLowerBound, to);
+            }
+          }
+          return;
         }
         WildcardType toWildcardType = (WildcardType) to;
         Type[] fromUpperBounds = fromWildcardType.getUpperBounds();
@@ -161,6 +172,10 @@ public final class TypeResolver {
         if (to instanceof WildcardType) {
           return; // Okay to say Foo<A> is <?>
         }
+        if (to instanceof Class && fromParameterizedType.getRawType().equals(Class.class)) {
+          populateTypeMappings(mappings, fromParameterizedType.getActualTypeArguments()[0], to);
+          return;
+        }
         ParameterizedType toParameterizedType = expectArgument(ParameterizedType.class, to);
         if (fromParameterizedType.getOwnerType() != null
             && toParameterizedType.getOwnerType() != null) {
@@ -168,7 +183,7 @@ public final class TypeResolver {
               mappings, fromParameterizedType.getOwnerType(), toParameterizedType.getOwnerType());
         }
         checkArgument(
-            fromParameterizedType.getRawType().equals(toParameterizedType.getRawType()),
+            ((Class<?>) fromParameterizedType.getRawType()).isAssignableFrom((Class<?>) toParameterizedType.getRawType()),
             "Inconsistent raw type: %s vs. %s",
             fromParameterizedType,
             to);
@@ -287,15 +302,31 @@ public final class TypeResolver {
 
     /** Returns a new {@code TypeResolver} with {@code variable} mapping to {@code type}. */
     final TypeTable where(Map<TypeVariableKey, ? extends Type> mappings) {
-      ImmutableMap.Builder<TypeVariableKey, Type> builder = ImmutableMap.builder();
-      builder.putAll(map);
+      Map<TypeVariableKey, Type> builder = new LinkedHashMap<>(map);
       for (Entry<TypeVariableKey, ? extends Type> mapping : mappings.entrySet()) {
         TypeVariableKey variable = mapping.getKey();
         Type type = mapping.getValue();
         checkArgument(!variable.equalsType(type), "Type variable %s bound to itself", variable);
-        builder.put(variable, type);
+        builder.merge(
+            variable,
+            type,
+            (oldType, newType) -> {
+              if (TypeToken.of(newType).isSubtypeOf(oldType)) {
+                return newType;
+              }
+              if (TypeToken.of(oldType).isSubtypeOf(newType)) {
+                return oldType;
+              }
+              throw new IllegalArgumentException(
+                  "Incompatible types for "
+                      + variable
+                      + ": "
+                      + oldType.getTypeName()
+                      + " and "
+                      + newType.getTypeName());
+            });
       }
-      return new TypeTable(builder.build());
+      return new TypeTable(ImmutableMap.copyOf(builder));
     }
 
     final Type resolve(final TypeVariable<?> var) {

It causes testWhere_duplicateMapping to fail, but I don't know if the behavior that test asserts is desirable anyway.

@fluentfuture
Copy link

I recall there was a lot of complexities in dealing with wildcard inference.

For example, how does one resolve Foo<String> arg against a Foo<? super T> param? Perhaps T=String?

But then if the method has another List<Object> arg against List<? super T> param, we'd want T=Object.

What if the args' compile-time types also involve wildcard?

TypeResolver so far mostly assumes invariance which works for cases when you want to know what List<T> is given T is String.

But when covariance or contra-variance is needed (such as at call-site), the type deduction rules seem very complicated to get right all the time. And particularly, TypeResolver().resolveType() does not know the caller wants covariance or contra-variance context.

@michaelhixson
Copy link
Author

I recall there was a lot of complexities in dealing with wildcard inference.

Well, I'm diving in! master...michaelhixson:type-resolver-fixes

Currently it knows that this method

static <T> T example(List<? extends T> a, List<? extends T> b) { return null; }

with arguments ArrayList<String> and ArrayList<Integer> will return Serializable & Comparable<?>, which is pretty cool. And it'll throw if you TypeResolver.where yourself into impossible constraints given the method signature and argument types, e.g.

IllegalArgumentException: No type can satisfy the constraints of T, which must be java.lang.Number, a subtype of java.io.Serializable, and a supertype of java.lang.String

What if the args' compile-time types also involve wildcard?

Haven't tried this one yet. Also haven't tried methods that use both class-level type variables and method-level type variables.

I'm going to keep working on this for a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants
@cgdecker @michaelhixson @fluentfuture and others