Skip to content

Commit

Permalink
add Types#captureTypeVariable and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
JHahnHRO committed Aug 16, 2024
1 parent c53bc10 commit 4ba2011
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
import java.lang.reflect.WildcardType;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -51,9 +49,8 @@ private static Map<TypeVariable<?>, Type> getKnownTypes(Type type) {
final Class<?> rawType = (Class<?>) relatedType.getRawType();
final TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
final Type[] actualTypeArguments = relatedType.getActualTypeArguments();
final Map<TypeVariable<?>, Type> copy = Map.copyOf(result);
for (int i = 0; i < typeParameters.length; i++) {
result.put(typeParameters[i], resolveInternal(actualTypeArguments[i], copy));
result.put(typeParameters[i], resolveInternal(actualTypeArguments[i], result));
}
});
result.entrySet().removeIf(entry -> Objects.equals(entry.getKey(), entry.getValue()));
Expand Down Expand Up @@ -151,8 +148,8 @@ public Set<Type> resolvedTypeClosure(Type type) {
}
final Class<?> erasedType = Types.erasure(type);

final Set<Class<?>> superClasses = Types.superClasses(erasedType);
final Set<Class<?>> superInterfaces = Types.superInterfaces(erasedType);
final var superClasses = Types.superClasses(erasedType);
final var superInterfaces = Types.superInterfaces(erasedType);
return Stream.concat(superClasses.stream(), superInterfaces.stream())
.map(this::resolve)
.collect(Collectors.toUnmodifiableSet());
Expand Down
86 changes: 66 additions & 20 deletions Core/src/main/java/io/github/jhahnhro/enhancedcdi/types/Types.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package io.github.jhahnhro.enhancedcdi.types;

import java.lang.invoke.MethodType;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.SequencedSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;

import io.github.jhahnhro.enhancedcdi.util.Iteration;

Expand All @@ -32,24 +38,17 @@ public static Class<?> erasure(Type type) {
/**
* Returns the set of all direct and indirect {@link Class#getSuperclass() super-classes} of the given class,
* including {@code Object.class} and the given class itself if it is indeed a class and not an interface. The
* returned set has a fixed iteration order given by assignability; it begins with {@code clazz} and ends with
* {@code Object.class}.
* returned list is ordered by assignability; it begins with {@code clazz} and ends with {@code Object.class}.
*
* @param clazz a Class
* @param <T> the type
* @return the set of super-classes
* @return the super-classes
*/
public static <T> Set<Class<?>> superClasses(Class<T> clazz) {
public static <T> List<Class<?>> superClasses(Class<T> clazz) {
if (clazz.isInterface()) {
return Set.of(Object.class);
return List.of(Object.class);
}

SequencedSet<Class<?>> result = new LinkedHashSet<>();
for (Class<?> superClass = clazz; superClass != null; superClass = superClass.getSuperclass()) {
result.add(superClass);
}

return Collections.unmodifiableSequencedSet(result);
return Stream.<Class<?>>iterate(clazz, Objects::nonNull, Class::getSuperclass).toList();
}

/**
Expand All @@ -62,14 +61,61 @@ public static <T> Set<Class<?>> superClasses(Class<T> clazz) {
* @return the set of super-interfaces
*/
public static <T> Set<Class<?>> superInterfaces(Class<T> clazz) {
Set<Class<?>> result = Iteration.breadthFirstSearch(clazz, aClass -> Arrays.stream(aClass.getInterfaces()));
if (!clazz.isInterface()) {
result = new LinkedHashSet<>(result);
result.remove(clazz);
result = Collections.unmodifiableSet(result);
}
final Function<Class<?>, Stream<Class<?>>> edges = aClass -> {
final Class<?> superclass = aClass.getSuperclass();
final Stream<Class<?>> interfaces = Arrays.stream(aClass.getInterfaces());
return superclass == null ? interfaces : Stream.concat(Stream.of(superclass), interfaces);
};
final List<Class<?>> superInterfaces = Iteration.breadthFirstSearch(clazz, edges)
.stream()
.filter(Class::isInterface)
.toList();
return Collections.unmodifiableSequencedSet(new LinkedHashSet<>(superInterfaces));
}

/**
* Convenience method to capture a {@link TypeVariable} by name from the context surrounding the caller. Only the
* immediate surroundings are considered:
* <ul>
* <li>If this method is called from a static initializer, a type variable of the class will be captured.</li>
* <li>If this method is called from a constructor, a type variable of the constructor will be captured.</li>
* <li>If this method is called from another method, a type variable of the method will be captured.</li>
* </li>
* </ul>
*
* @param name the name of the type variable that should be captured.
* @return the type variable of the given name.
* @throws NoSuchElementException if there is no type variable with the given name.
*/
@SuppressWarnings("java:S1452") // Sonar does not like returning wildcards, but here it is necessary
public static TypeVariable<?> captureTypeVariable(String name) {
final GenericDeclaration callingMethod = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(stackFrames -> stackFrames.dropWhile(f -> f.getClassName().equals(Types.class.getName()))
.map(Types::reflectMethod)
.findFirst()
.orElseThrow());

return result;
for (TypeVariable<?> typeParameter : callingMethod.getTypeParameters()) {
if (typeParameter.getName().equals(name)) {
return typeParameter;
}
}
throw new NoSuchElementException("No type variable with name " + name + " on " + callingMethod);
}

private static GenericDeclaration reflectMethod(StackWalker.StackFrame stackFrame) {
final Class<?> declaringClass = stackFrame.getDeclaringClass();
final MethodType methodType = stackFrame.getMethodType();
final String methodName = stackFrame.getMethodName();

try {
return switch (methodName) {
case "<clinit>" -> declaringClass;
case "<init>" -> declaringClass.getConstructor(methodType.parameterArray());
default -> declaringClass.getDeclaredMethod(methodName, methodType.parameterArray());
};
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
}
}
193 changes: 193 additions & 0 deletions Core/src/test/java/io/github/jhahnhro/enhancedcdi/types/TypesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package io.github.jhahnhro.enhancedcdi.types;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import jakarta.enterprise.util.TypeLiteral;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class TypesTest {

@Nested
class TestCaptureTypeVariable {

@Test
<T> void testMethodWithOneVariable() {
TypeVariable<?> t = Types.captureTypeVariable("T");

assertThat(t).isEqualTo(new TypeLiteral<T>() {}.getType());
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> Types.captureTypeVariable("X"));
}

@Test
<T, U> void testMethodWithTwoVariables() {
TypeVariable<?> t = Types.captureTypeVariable("T");
TypeVariable<?> u = Types.captureTypeVariable("U");

assertThat(t).isEqualTo(new TypeLiteral<T>() {}.getType());
assertThat(u).isEqualTo(new TypeLiteral<U>() {}.getType());
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> Types.captureTypeVariable("X"));
}

@Test
void testMethodWithoutVariables() {
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> Types.captureTypeVariable("X"));
}

@Test
void testCaptureFromConstructor() {
class Foobar {

private final TypeVariable<?> captured;
private final TypeLiteral<?> typeLiteral;

public <U> Foobar() {
captured = Types.captureTypeVariable("U");
typeLiteral = new TypeLiteral<U>() {};
}
}

final Foobar object = new Foobar();
final TypeVariable<?> t = object.captured;

assertThat(t).isEqualTo(object.typeLiteral.getType());
}

@Test
void testCaptureFromStaticInitializer() {
class Foobar<T> {
private static final TypeVariable<?> captured = Types.captureTypeVariable("T");
}
final TypeVariable<?> t = Foobar.captured;

assertThat(t).isEqualTo(Foobar.class.getTypeParameters()[0]);
}
}

@Nested
class TestSuperClasses {
@Test
void testObject() {
final var classes = Types.superClasses(Object.class);
assertThat(classes).containsExactly(Object.class);
}

@Test
void testClassHierarchy() {
final var classes = Types.superClasses(Bar.class);
assertThat(classes).containsExactly(Bar.class, Foo.class, Object.class);
}

@Test
void testInterface() {
final var classes = Types.superClasses(List.class);
assertThat(classes).containsExactly(Object.class);
}

static class Foo {}

static class Bar extends Foo {}
}

@Nested
class TestGetSuperInterfaces {
@Test
void testObject() {
final var classes = Types.superInterfaces(Object.class);
assertThat(classes).isEmpty();
}

@Test
void testInterfaceClass() {
final var classes = Types.superInterfaces(Foo.class);
assertThat(classes).containsExactly(Foo.class);
}

@Test
void testInterfaceHierarchy() {
final var classes = Types.superInterfaces(Bar.class);
assertThat(classes).containsExactly(Bar.class, Foo.class);
}

@Test
void testClassImplementingAnInterface() {
final var classes = Types.superInterfaces(Baz.class);
assertThat(classes).containsExactly(Bar.class, Foo.class);
}

@Test
void testClassImplementingAnInterfaceTwice() {
final var classes = Types.superInterfaces(BazFoo.class);
assertThat(classes).containsExactly(Foo.class, Bar.class);
}

interface Foo {}

interface Bar extends Foo {}

static class Baz implements Bar {}

static class BazFoo extends Baz implements Foo {}
}

@Nested
class TestErasure {

public static <T extends Foo> Stream<Arguments> getTypes() {
return Stream.of(arguments(Integer.class, Integer.class),
arguments(new TypeLiteral<List<String>>() {}.getType(), List.class),
arguments(new TypeLiteral<Map<Integer, String>>() {}.getType(), Map.class),
arguments(new TypeLiteral<T>() {}.getType(), Foo.class),
arguments(new TypeLiteral<List<Integer>[]>() {}.getType(), List[].class),
arguments(new TypeLiteral<T[]>() {}.getType(), Foo[].class));
}

@SuppressWarnings("unused")
private static <T extends Foo, U> void methodWithGenericParameters(String arg0, List<String> arg1, T arg2,
U arg3, List<T> arg4, T[] arg5, U[] arg6) {}

public static Stream<Arguments> getJdkArguments() {
try {
final Class<?>[] erasedTypes = {String.class, List.class, Foo.class, Object.class, List.class,
Foo[].class, Object[].class};
final Method method = TestErasure.class.getDeclaredMethod("methodWithGenericParameters", erasedTypes);
final Type[] genericTypes = method.getGenericParameterTypes();

return IntStream.range(0, method.getParameterCount())
.mapToObj(i -> arguments(genericTypes[i], erasedTypes[i]));
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

@ParameterizedTest
@MethodSource("getTypes")
void testErasure(Type type, Class<?> expectedErasure) {
final Class<?> actual = Types.erasure(type);
assertThat(actual).isEqualTo(expectedErasure);
}

@ParameterizedTest
@MethodSource("getJdkArguments")
void testCompatibilityWithJDK(Type type, Class<?> expectedErasure) {
final Class<?> actual = Types.erasure(type);
assertThat(actual).isEqualTo(expectedErasure);
}

static class Foo {}
}
}

0 comments on commit 4ba2011

Please sign in to comment.