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

Support for just resolving type variables #12

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/main/java/net/jodah/typetools/TypeDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
*/
package net.jodah.typetools;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* A Java field or method type. This class can be used to make it easier to manipulate type and
* method descriptors.
Expand Down Expand Up @@ -150,6 +154,32 @@ final class TypeDescriptor {
public static final TypeDescriptor VOID_TYPE = new TypeDescriptor(VOID, null, ('V' << 24) | (5 << 16) | (0 << 8) | 0,
1);

private static final Map<Class<?>, Class<?>> PRIMITIVES_TO_WRAPPERS;

static {
Map<Class<?>, Class<?>> map = new HashMap<>();
map.put(boolean.class, Boolean.class);
map.put(byte.class, Byte.class);
map.put(char.class, Character.class);
map.put(double.class, Double.class);
map.put(float.class, Float.class);
map.put(int.class, Integer.class);
map.put(long.class, Long.class);
map.put(short.class, Short.class);
map.put(void.class, Void.class);
PRIMITIVES_TO_WRAPPERS = Collections.unmodifiableMap(map);
}

public static Class<?> primitiveToWrapper(Class<?> clazz) {
if (clazz.isPrimitive()) {
Class<?> wrapper = PRIMITIVES_TO_WRAPPERS.get(clazz);
if (wrapper != null) {
return wrapper;
}
}
return clazz;
}

/**
* A buffer containing the internal name of this Java type. This field is only used for reference
* types.
Expand Down
113 changes: 95 additions & 18 deletions src/main/java/net/jodah/typetools/TypeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,7 @@

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -181,6 +175,49 @@ public static Class<?>[] resolveRawArguments(Type genericType, Class<?> subType)
return result;
}

/**
* Returns an array of types representing arguments for the {@code genericType} using type variable information
* from the {@code subType}. Arguments for {@code genericType} that cannot be resolved are returned as
* {@code Unknown.class}. If no arguments can be resolved then {@code null} is returned.
*
* This method only resolves type variables, it does not resolve anything further than that. It is primarily useful
* for getting the types of lambda parameters.
*
* @param genericType to resolve arguments for
* @param subType to extract type variable information from
* @return array of types representing arguments for the {@code genericType} else {@code null} if no type
* arguments are declared
*/
public static Type[] resolveTypeArgumentVariables(Type genericType, Class<?> subType) {
Type[] result = null;
Class<?> functionalInterface = null;

// Handle lambdas
if (SUPPORTS_LAMBDAS && subType.isSynthetic()) {
Class<?> fi = genericType instanceof ParameterizedType
&& ((ParameterizedType) genericType).getRawType() instanceof Class
? (Class<?>) ((ParameterizedType) genericType).getRawType()
: genericType instanceof Class ? (Class<?>) genericType : null;
if (fi != null && fi.isInterface())
functionalInterface = fi;
}

if (genericType instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericType;
result = paramType.getActualTypeArguments();
} else if (genericType instanceof TypeVariable) {
result = new Type[1];
result[0] = resolveTypeVariable((TypeVariable<?>) genericType, subType, functionalInterface);
} else if (genericType instanceof Class) {
TypeVariable<?>[] typeParams = ((Class<?>) genericType).getTypeParameters();
result = new Type[typeParams.length];
for (int i = 0; i < typeParams.length; i++)
result[i] = resolveTypeVariable(typeParams[i], subType, functionalInterface);
}

return result;
}

/**
* Returns the generic {@code type} using type variable information from the {@code subType} else {@code null} if the
* generic type cannot be resolved.
Expand Down Expand Up @@ -229,7 +266,7 @@ public static Class<?> resolveRawClass(Type genericType, Class<?> subType) {

private static Class<?> resolveRawClass(Type genericType, Class<?> subType, Class<?> functionalInterface) {
if (genericType instanceof Class) {
return (Class<?>) genericType;
return TypeDescriptor.primitiveToWrapper((Class<?>) genericType);
} else if (genericType instanceof ParameterizedType) {
return resolveRawClass(((ParameterizedType) genericType).getRawType(), subType, functionalInterface);
} else if (genericType instanceof GenericArrayType) {
Expand All @@ -246,6 +283,12 @@ private static Class<?> resolveRawClass(Type genericType, Class<?> subType, Clas
return genericType instanceof Class ? (Class<?>) genericType : Unknown.class;
}

private static Type resolveTypeVariable(TypeVariable<?> typeVariable, Class<?> subType, Class<?> functionalInterface) {
Type genericType = getTypeVariableMap(subType, functionalInterface).get(typeVariable);
genericType = genericType == null ? resolveBound(typeVariable) : genericType;
return genericType != null ? genericType : Unknown.class;
}

private static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targetType,
Class<?> functionalInterface) {
Reference<Map<TypeVariable<?>, Type>> ref = typeVariableCache.get(targetType);
Expand Down Expand Up @@ -323,26 +366,60 @@ private static void populateLambdaArgs(Class<?> functionalInterface, final Class
}
}

// Try to load the class that the method ref is defined on
Class<?> ownerClass = TypeDescriptor.getObjectType(methodRefInfo[0]).getType(lambdaType.getClassLoader());

// Now load the argument classes
TypeDescriptor[] arguments = TypeDescriptor.getArgumentTypes(methodRefInfo[2]);
Class<?>[] argumentClasses = new Class<?>[arguments.length];
for (int i = 0; i < arguments.length; i++) {
argumentClasses[i] = arguments[i].getType(lambdaType.getClassLoader());
}

// Now try to locate the referenced method
Executable method = null;

if (ownerClass != null) {
if (methodRefInfo[1].equals("<init>")) {
method = ownerClass.getDeclaredConstructor(argumentClasses);
} else {
method = ownerClass.getDeclaredMethod(methodRefInfo[1], argumentClasses);
}
}

if (returnTypeVar instanceof TypeVariable) {
Class<?> returnType = TypeDescriptor.getReturnType(methodRefInfo[2]).getType(lambdaType.getClassLoader());
if (!returnType.equals(Void.class))
Type returnType;
if (method != null) {
if (method instanceof Method) {
returnType = ((Method) method).getGenericReturnType();
} else {
returnType = ownerClass;
}
} else {
returnType = TypeDescriptor.getReturnType(methodRefInfo[2]).getType(lambdaType.getClassLoader());
}
if (!Void.class.equals(returnType))
map.put((TypeVariable<?>) returnTypeVar, returnType);
}

TypeDescriptor[] arguments = TypeDescriptor.getArgumentTypes(methodRefInfo[2]);

// Handle arbitrary object instance method references
int offset = 0;
if (paramTypeVars[0] instanceof TypeVariable && paramTypeVars.length == arguments.length + 1) {
Class<?> instanceType = TypeDescriptor.getObjectType(methodRefInfo[0])
.getType(lambdaType.getClassLoader());
map.put((TypeVariable<?>) paramTypeVars[0], instanceType);
map.put((TypeVariable<?>) paramTypeVars[0], ownerClass);
offset = 1;
}

for (int i = 0; i < arguments.length; i++)
if (paramTypeVars[i + offset] instanceof TypeVariable)
map.put((TypeVariable<?>) paramTypeVars[i + offset], arguments[i].getType(lambdaType.getClassLoader()));
for (int i = 0; i < arguments.length; i++) {
if (paramTypeVars[i + offset] instanceof TypeVariable) {
Type paramType;
if (method != null) {
paramType = method.getGenericParameterTypes()[i];
} else {
paramType = arguments[i].getType(lambdaType.getClassLoader());
}
map.put((TypeVariable<?>) paramTypeVars[i + offset], paramType);
}
}
break;
}
}
Expand Down
61 changes: 61 additions & 0 deletions src/test/java/net/jodah/typetools/functional/LambdaTest.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package net.jodah.typetools.functional;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Comparator;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -243,4 +247,61 @@ private <T, R> void handlePassedFunction(Function<T, R> fn) {
assertEquals(typeArgs[0], UUID.class);
assertEquals(typeArgs[1], String.class);
}

/**
* Asserts that when resolving type argument variables for a method ref, the correct ParameterizedType is returned.
*/
public void shouldResolveMethodRefGenericArguments() {
Consumer<Optional<String>> consumer = this::foo;

Type[] arguments = TypeResolver.resolveTypeArgumentVariables(Consumer.class, consumer.getClass());
assertEquals(arguments.length, 1);
Type argument = arguments[0];
assertTrue(argument instanceof ParameterizedType);
ParameterizedType type = (ParameterizedType) argument;
assertEquals(type.getRawType(), Optional.class);
assertEquals(type.getActualTypeArguments()[0], String.class);
}

private void foo(Optional<String> foo) {
}

/**
* Asserts that when resolving type argument variables for a lambda, the correct ParameterizedType is returned.
*
* Note: disabled since the Java compiler doesn't include generic information in synthetic lambda method signatures.
*/
@Test(enabled = false)
public void shouldResolveLambdaGenericArguments() {
Consumer<Optional<String>> consumer = (Optional<String> optString) -> System.out.println(optString);

Type[] arguments = TypeResolver.resolveTypeArgumentVariables(Consumer.class, consumer.getClass());
assertEquals(arguments.length, 1);
Type argument = arguments[0];
assertTrue(argument instanceof ParameterizedType);
ParameterizedType type = (ParameterizedType) argument;
assertEquals(type.getRawType(), Optional.class);
assertEquals(type.getActualTypeArguments()[0], String.class);
}

public static class Foo2 {
public Foo2(Optional<String> arg) {}
}

/**
* Asserts that when resolving type argument variables for a constructor ref, the correct ParameterizedType is
* returned.
*/
public void shouldResolveConstructorRefArguments() {
Consumer<Optional<String>> consumer = Foo2::new;

Type[] arguments = TypeResolver.resolveTypeArgumentVariables(Consumer.class, consumer.getClass());
assertEquals(arguments.length, 1);
Type argument = arguments[0];
assertTrue(argument instanceof ParameterizedType);
ParameterizedType type = (ParameterizedType) argument;
assertEquals(type.getRawType(), Optional.class);
assertEquals(type.getActualTypeArguments()[0], String.class);
}

}