From 8456198a22c07270f032440acef4acb02b9e5193 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sat, 9 Mar 2024 15:00:31 +0100 Subject: [PATCH] add support to traverse `JavaType` signature At the moment, handling `JavaType`s is not convenient, because there are many different subtypes. And effectively the only way to handle those is a chain of `instanceof` checks with individual handling for each type. We can make this more convenient by adding a visitor pattern API that allows to simply define what to do on every partial type encountered in the signature (i.e. what to do when a parameterized type is encountered, what to do when each actual type argument of the parameterized type is encountered, and so on). As a benefit, we can use this API in the next step to fix the infinite recursion problem we have for the `getAllRawTypes()` method at the moment. Because, there we haven't handled the case where type variables are defined recursively. Adding this visitor pattern API we can solve this problem in a generic way once at the infrastructure level, i.e. implement the traversal correctly once and utilize it in more specific use cases. Signed-off-by: Peter Gafert --- .../archunit/core/domain/JavaClass.java | 5 + .../core/domain/JavaGenericArrayType.java | 5 + .../core/domain/JavaParameterizedType.java | 5 + .../archunit/core/domain/JavaType.java | 175 +++++++ .../core/domain/JavaTypeVariable.java | 5 + .../core/domain/JavaWildcardType.java | 5 + .../core/domain/JavaTypeTraversalTest.java | 440 ++++++++++++++++++ 7 files changed, 640 insertions(+) create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/domain/JavaTypeTraversalTest.java diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java index 3d9e12c7b..2081852b9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java @@ -662,6 +662,11 @@ public Set getAllInvolvedRawTypes() { return ImmutableSet.of(getBaseComponentType()); } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitClass(this); + } + @PublicAPI(usage = ACCESS) public Optional getRawSuperclass() { return superclass.getRaw(); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java index 53baaceb6..92c7ea37d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java @@ -74,6 +74,11 @@ public Set getAllInvolvedRawTypes() { return this.componentType.getAllInvolvedRawTypes(); } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitGenericArrayType(this); + } + @Override public String toString() { return getClass().getSimpleName() + '{' + getName() + '}'; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java index 0c280bc39..35ec64f02 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java @@ -35,4 +35,9 @@ public interface JavaParameterizedType extends JavaType { */ @PublicAPI(usage = ACCESS) List getActualTypeArguments(); + + @Override + default void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitParameterizedType(this); + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java index 7ba992e44..50a8accfd 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java @@ -16,13 +16,22 @@ package com.tngtech.archunit.core.domain; import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashSet; import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import com.google.common.collect.Iterables; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.core.domain.properties.HasName; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; +import static com.tngtech.archunit.core.domain.JavaType.SignatureVisitor.Result.CONTINUE; +import static com.tngtech.archunit.core.domain.JavaType.SignatureVisitor.Result.STOP; +import static java.util.Collections.singleton; /** * Represents a general Java type. This can e.g. be a class like {@code java.lang.String}, a parameterized type @@ -84,6 +93,94 @@ public interface JavaType extends HasName { @PublicAPI(usage = ACCESS) Set getAllInvolvedRawTypes(); + /** + * Traverses through the signature of this {@link JavaType}.
+ * This method considers the type signature as a tree, + * where e.g. a {@link JavaClass} is a simple leaf, + * but a {@link JavaParameterizedType} has the type as root and then + * branches out into its actual type arguments, which in turn can have type arguments + * or upper/lower bounds in case of {@link JavaTypeVariable} or {@link JavaWildcardType}.
+ * The following is a simple visualization of such a signature tree: + *

+     * List<Map<? extends Serializable, String[]>>
+     *                    |
+     *    Map<? extends Serializable, String[]>
+     *              /                   \
+     *  ? extends Serializable         String[]
+     *            |
+     *      Serializable
+     * 
+ * For every node visited the respective method of the provided {@code visitor} + * will be invoked. The traversal happens depth first, i.e. in this case the {@code visitor} + * would be invoked for all types down to {@code Serializable} before visiting the {@code String[]} + * array type of the second branch. At every step it is possible to continue the traversal + * by returning {@link SignatureVisitor.Result#CONTINUE CONTINUE} or stop at that point by + * returning {@link SignatureVisitor.Result#STOP STOP}.

+ * Note that the traversal will continue to traverse bounds of type variables, + * even if that type variable isn't declared in this signature itself.
+ * E.g. take the following scenario + *

+     * class Example<T extends String> {
+     *     T field;
+     * }
+ * Traversing the {@link JavaField#getType() field type} of {@code field} will continue + * down to the upper bounds of the type variable {@code T} and thus end at the type {@code String}.

+ * Also, note that the traversal will not continue down the type parameters of a raw type + * declared in a signature.
+ * E.g. given the signature {@code class Example} the traversal would stop at + * {@code Map} and not traverse down the type parameters {@code K} and {@code V} of {@code Map}. + * + * @param visitor A {@link SignatureVisitor} to invoke for every encountered {@link JavaType} + * while traversing this signature. + */ + @PublicAPI(usage = ACCESS) + void traverseSignature(SignatureVisitor visitor); + + /** + * @see #traverseSignature(SignatureVisitor) + */ + @PublicAPI(usage = INHERITANCE) + interface SignatureVisitor { + default Result visitClass(JavaClass type) { + return CONTINUE; + } + + default Result visitParameterizedType(JavaParameterizedType type) { + return CONTINUE; + } + + default Result visitTypeVariable(JavaTypeVariable type) { + return CONTINUE; + } + + default Result visitGenericArrayType(JavaGenericArrayType type) { + return CONTINUE; + } + + default Result visitWildcardType(JavaWildcardType type) { + return CONTINUE; + } + + /** + * Result of a single step {@link #traverseSignature(SignatureVisitor) traversing a signature}. + * After each step it's possible to either {@link #STOP stop} or {@link #CONTINUE continue} + * the traversal. + */ + @PublicAPI(usage = ACCESS) + enum Result { + /** + * Causes the traversal to continue + */ + @PublicAPI(usage = ACCESS) + CONTINUE, + /** + * Causes the traversal to stop + */ + @PublicAPI(usage = ACCESS) + STOP + } + } + /** * Predefined {@link ChainableFunction functions} to transform {@link JavaType}. */ @@ -101,3 +198,81 @@ public JavaClass apply(JavaType input) { }; } } + +class SignatureTraversal implements JavaType.SignatureVisitor { + private final Set visited = new HashSet<>(); + private final JavaType.SignatureVisitor delegate; + private Result lastResult; + + private SignatureTraversal(JavaType.SignatureVisitor delegate) { + this.delegate = delegate; + } + + @Override + public Result visitClass(JavaClass type) { + // We only traverse type parameters of a JavaClass if the traversal was started *at the JavaClass* itself. + // Otherwise, we can only encounter a regular class as a raw type in a type signature. + // In these cases we don't want to traverse further down, as that would be surprising behavior + // (consider `class MyClass`, traversing into the type variables `K` and `V` of `Map` would be surprising). + Supplier>> getFurtherTypesToTraverse = visited.isEmpty() ? type::getTypeParameters : Collections::emptyList; + return visit(type, delegate::visitClass, getFurtherTypesToTraverse); + } + + @Override + public Result visitParameterizedType(JavaParameterizedType type) { + return visit(type, delegate::visitParameterizedType, type::getActualTypeArguments); + } + + @Override + public Result visitTypeVariable(JavaTypeVariable type) { + return visit(type, delegate::visitTypeVariable, type::getUpperBounds); + } + + @Override + public Result visitGenericArrayType(JavaGenericArrayType type) { + return visit(type, delegate::visitGenericArrayType, () -> singleton(type.getComponentType())); + } + + @Override + public Result visitWildcardType(JavaWildcardType type) { + return visit(type, delegate::visitWildcardType, () -> Iterables.concat(type.getUpperBounds(), type.getLowerBounds())); + } + + private Result visit( + CURRENT type, + Function visitCurrent, + Supplier> nextTypes + ) { + if (visited.contains(type)) { + // if we've encountered this type already we continue traversing the siblings, + // but we won't descend further into this type signature + return setLast(CONTINUE); + } + visited.add(type); + if (visitCurrent.apply(type) == CONTINUE) { + Result result = visit(nextTypes.get()); + return setLast(result); + } else { + return setLast(STOP); + } + } + + private Result visit(Iterable types) { + for (JavaType nextType : types) { + nextType.traverseSignature(this); + if (lastResult == STOP) { + return STOP; + } + } + return CONTINUE; + } + + private Result setLast(Result result) { + lastResult = result; + return result; + } + + static SignatureTraversal from(JavaType.SignatureVisitor visitor) { + return visitor instanceof SignatureTraversal ? (SignatureTraversal) visitor : new SignatureTraversal(visitor); + } +} \ No newline at end of file diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java index d25f63c5a..ce333a71f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java @@ -129,6 +129,11 @@ public Set getAllInvolvedRawTypes() { .collect(toSet()); } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitTypeVariable(this); + } + @Override public String toString() { String bounds = printExtendsClause() ? " extends " + joinTypeNames(upperBounds) : ""; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java index 3d36ef326..596296ca1 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java @@ -106,6 +106,11 @@ public Set getAllInvolvedRawTypes() { .collect(toSet()); } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitWildcardType(this); + } + @Override public String toString() { return getClass().getSimpleName() + '{' + getName() + '}'; diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaTypeTraversalTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaTypeTraversalTest.java new file mode 100644 index 000000000..c784d7fb4 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaTypeTraversalTest.java @@ -0,0 +1,440 @@ +package com.tngtech.archunit.core.domain; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.tngtech.archunit.base.ForwardingList; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import org.junit.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.testutil.Assertions.assertThatTypes; +import static org.assertj.core.api.Assertions.assertThat; + +public class JavaTypeTraversalTest { + + @Test + public void traverses_simple_class() { + class SimpleClass { + } + + JavaClass clazz = new ClassFileImporter().importClass(SimpleClass.class); + + List traversedClasses = new ArrayList<>(); + clazz.traverseSignature(new AllRejectingSignatureVisitor() { + @Override + public Result visitClass(JavaClass type) { + traversedClasses.add(type); + return Result.STOP; + } + }); + + assertThatTypes(traversedClasses).matchExactly(SimpleClass.class); + } + + @Test + public void traverses_array_type() { + class SimpleClass { + @SuppressWarnings("unused") + private SimpleClass[][] field; + } + + JavaType arrayType = new ClassFileImporter().importClass(SimpleClass.class).getField("field").getType(); + + List traversedClasses = new ArrayList<>(); + arrayType.traverseSignature(new AllRejectingSignatureVisitor() { + @Override + public Result visitClass(JavaClass type) { + traversedClasses.add(type); + return Result.STOP; + } + }); + + assertThatTypes(traversedClasses).matchExactly(SimpleClass[][].class); + } + + @Test + public void traverses_type_variable() { + @SuppressWarnings("unused") + class SomeClass { + T field; + } + JavaTypeVariable typeVariable = (JavaTypeVariable) new ClassFileImporter() + .importClass(SomeClass.class) + .getField("field") + .getType(); + + List traversedTypes = new ArrayList<>(); + typeVariable.traverseSignature(new AllRejectingSignatureVisitor() { + @Override + public Result visitTypeVariable(JavaTypeVariable type) { + traversedTypes.add(type); + return Result.STOP; + } + }); + + assertThat(traversedTypes).containsOnly(typeVariable); + } + + @Test + public void traverses_parameterized_type() { + @SuppressWarnings("unused") + class SomeClass { + List field; + } + JavaParameterizedType parameterizedType = (JavaParameterizedType) new ClassFileImporter() + .importClass(SomeClass.class) + .getField("field") + .getType(); + + List traversedTypes = new ArrayList<>(); + parameterizedType.traverseSignature(new AllRejectingSignatureVisitor() { + @Override + public Result visitParameterizedType(JavaParameterizedType type) { + traversedTypes.add(type); + return Result.STOP; + } + }); + + assertThat(traversedTypes).containsOnly(parameterizedType); + } + + @Test + public void traverses_wildcard_type() { + @SuppressWarnings("unused") + class SomeClass { + List field; + } + JavaParameterizedType parameterizedType = (JavaParameterizedType) new ClassFileImporter() + .importClass(SomeClass.class) + .getField("field") + .getType(); + + JavaWildcardType wildcardType = (JavaWildcardType) getOnlyElement(parameterizedType.getActualTypeArguments()); + + List traversedTypes = new ArrayList<>(); + wildcardType.traverseSignature(new AllRejectingSignatureVisitor() { + @Override + public Result visitWildcardType(JavaWildcardType type) { + traversedTypes.add(type); + return Result.STOP; + } + }); + + assertThat(traversedTypes).containsOnly(wildcardType); + } + + @Test + public void traverses_generic_array_type() { + @SuppressWarnings("unused") + class SomeClass { + T[] field; + } + JavaGenericArrayType genericArrayType = (JavaGenericArrayType) new ClassFileImporter() + .importClass(SomeClass.class) + .getField("field") + .getType(); + + List traversedTypes = new ArrayList<>(); + genericArrayType.traverseSignature(new AllRejectingSignatureVisitor() { + @Override + public Result visitGenericArrayType(JavaGenericArrayType type) { + traversedTypes.add(type); + return Result.STOP; + } + }); + + assertThat(traversedTypes).containsOnly(genericArrayType); + } + + @Test + public void traverses_complex_signature_of_JavaClass() { + @SuppressWarnings("unused") + class SomeClass>, U extends List, V extends File & Set> { + } + + JavaClass classWithComplexSignature = new ClassFileImporter().importClass(SomeClass.class); + + List> typeParameters = classWithComplexSignature.getTypeParameters(); + + ExpectedTypes types = new ExpectedTypes(); + types.expect(classWithComplexSignature); + // Type variable T + JavaTypeVariable typeVariableT = types.expect(typeParameters.get(0)); + JavaParameterizedType mapType = types.expect(typeVariableT.getUpperBounds().get(0)); + JavaWildcardType mapKeyWildCard = types.expect(mapType.getActualTypeArguments().get(0)); + JavaGenericArrayType genericArrayType2DimT = types.expect(mapKeyWildCard.getUpperBounds().get(0)); + types.expect(genericArrayType2DimT.getComponentType()); + JavaParameterizedType mapValueParameterizedTypeList = types.expect(mapType.getActualTypeArguments().get(1)); + types.expect(mapValueParameterizedTypeList.getActualTypeArguments().get(0)); + // Type variable U + JavaTypeVariable typeVariableU = types.expect(typeParameters.get(1)); + JavaParameterizedType listType = types.expect(typeVariableU.getUpperBounds().get(0)); + JavaWildcardType listWildCard = types.expect(listType.getActualTypeArguments().get(0)); + types.expect(listWildCard.getLowerBounds().get(0)); + // Type variable V + JavaTypeVariable typeVariableV = types.expect(typeParameters.get(2)); + types.expect(typeVariableV.getUpperBounds().get(0)); + JavaParameterizedType setType = types.expect(typeVariableV.getUpperBounds().get(1)); + types.expect(setType.getActualTypeArguments().get(0)); + // End expected types + + List traversedTypes = new ArrayList<>(); + classWithComplexSignature.traverseSignature(newTrackingVisitor(traversedTypes)); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + @Test + public void traverses_complex_signature_of_JavaTypeVariable() { + @SuppressWarnings("unused") + class SomeClass>> { + } + + JavaType typeVariable = new ClassFileImporter().importClass(SomeClass.class) + .getTypeParameters().get(0); + + ExpectedTypes types = new ExpectedTypes(); + JavaTypeVariable typeVariableT = types.expect(typeVariable); + JavaParameterizedType mapType = types.expect(typeVariableT.getUpperBounds().get(0)); + JavaWildcardType mapKeyWildCard = types.expect(mapType.getActualTypeArguments().get(0)); + JavaGenericArrayType genericArrayType2DimT = types.expect(mapKeyWildCard.getUpperBounds().get(0)); + types.expect(genericArrayType2DimT.getComponentType()); + JavaParameterizedType mapValueParameterizedTypeList = types.expect(mapType.getActualTypeArguments().get(1)); + types.expect(mapValueParameterizedTypeList.getActualTypeArguments().get(0)); + + List traversedTypes = new ArrayList<>(); + typeVariable.traverseSignature(newTrackingVisitor(traversedTypes)); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + @Test + public void traverses_complex_signature_of_JavaParameterizedType() { + @SuppressWarnings("unused") + class SomeClass>> { + } + + JavaType parameterizedType = new ClassFileImporter().importClass(SomeClass.class) + .getTypeParameters().get(0).getUpperBounds().get(0); + + ExpectedTypes types = new ExpectedTypes(); + JavaParameterizedType mapType = types.expect(parameterizedType); + JavaWildcardType mapKeyWildCard = types.expect(mapType.getActualTypeArguments().get(0)); + JavaGenericArrayType genericArrayType2DimT = types.expect(mapKeyWildCard.getUpperBounds().get(0)); + JavaGenericArrayType genericArrayType1DimT = types.expect(genericArrayType2DimT.getComponentType()); + types.expect(genericArrayType1DimT.getComponentType()); + JavaParameterizedType mapValueParameterizedTypeList = types.expect(mapType.getActualTypeArguments().get(1)); + types.expect(mapValueParameterizedTypeList.getActualTypeArguments().get(0)); + + List traversedTypes = new ArrayList<>(); + parameterizedType.traverseSignature(newTrackingVisitor(traversedTypes)); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + @Test + public void traverses_complex_signature_of_generic_array_type_from_type_variable() { + @SuppressWarnings("unused") + class SomeClass { + T[][] field; + } + + JavaType arrayType = new ClassFileImporter().importClass(SomeClass.class) + .getField("field").getType(); + + ExpectedTypes types = new ExpectedTypes(); + JavaGenericArrayType tArray2Dim = types.expect(arrayType); + JavaGenericArrayType tArray1Dim = types.expect(tArray2Dim.getComponentType()); + JavaTypeVariable typeVariableT = types.expect(tArray1Dim.getComponentType()); + types.expect(typeVariableT.getUpperBounds().get(0)); + + List traversedTypes = new ArrayList<>(); + arrayType.traverseSignature(newTrackingVisitor(traversedTypes)); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + @Test + public void traverses_complex_signature_of_JavaWildcardType() { + @SuppressWarnings("unused") + class SomeClass>> { + } + + JavaParameterizedType mapType = (JavaParameterizedType) new ClassFileImporter().importClass(SomeClass.class) + .getTypeParameters().get(0).getUpperBounds().get(0); + JavaType mapKeyWildcard = mapType.getActualTypeArguments().get(0); + + ExpectedTypes types = new ExpectedTypes(); + JavaWildcardType mapKeyWildCard = types.expect(mapKeyWildcard); + JavaGenericArrayType genericArrayType2DimT = types.expect(mapKeyWildCard.getUpperBounds().get(0)); + JavaGenericArrayType genericArrayType1DimT = types.expect(genericArrayType2DimT.getComponentType()); + types.expect(genericArrayType1DimT.getComponentType()); + // from here on we recurse through type variable in the signature + types.expect(mapType); + JavaParameterizedType mapValueParameterizedTypeList = types.expect(mapType.getActualTypeArguments().get(1)); + types.expect(mapValueParameterizedTypeList.getActualTypeArguments().get(0)); + + List traversedTypes = new ArrayList<>(); + mapKeyWildcard.traverseSignature(newTrackingVisitor(traversedTypes)); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + @Test + public void does_not_traverse_into_type_variables_of_raw_type_in_signature() { + @SuppressWarnings({"unused", "rawtypes"}) + class SomeClass { + } + + JavaType type = new ClassFileImporter().importClasses(SomeClass.class, Map.class) + .get(SomeClass.class).getTypeParameters().get(0); + + ExpectedTypes types = new ExpectedTypes(); + JavaTypeVariable typeVariable = types.expect(type); + types.expect(typeVariable.getUpperBounds().get(0)); + + List traversedTypes = new ArrayList<>(); + type.traverseSignature(newTrackingVisitor(traversedTypes)); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + @Test + public void traverses_into_type_variables_of_method_signature_type() { + @SuppressWarnings("unused") + class SomeClass { + > void method(Set param) { + } + } + + JavaType type = new ClassFileImporter().importClass(SomeClass.class) + .getMethod("method", Set.class).getParameterTypes().get(0); + + ExpectedTypes types = new ExpectedTypes(); + JavaParameterizedType setType = types.expect(type); + JavaWildcardType wildcardType = types.expect(setType.getActualTypeArguments().get(0)); + JavaTypeVariable typeVariableT = types.expect(wildcardType.getUpperBounds().get(0)); + JavaParameterizedType listType = types.expect(typeVariableT.getUpperBounds().get(0)); + types.expect(listType.getActualTypeArguments().get(0)); + + List traversedTypes = new ArrayList<>(); + type.traverseSignature(newTrackingVisitor(traversedTypes)); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + @Test + public void stops_depth_first_traversal_once_STOP_is_received() { + @SuppressWarnings("unused") + class SomeClass { + & Serializable> void method(T param) { + } + } + + JavaType type = new ClassFileImporter().importClass(SomeClass.class) + .getMethod("method", Map.class).getParameterTypes().get(0); + + ExpectedTypes types = new ExpectedTypes(); + JavaTypeVariable typeVariableT = types.expect(type); + types.expect(typeVariableT.getUpperBounds().get(0)); + + List traversedTypes = new ArrayList<>(); + type.traverseSignature(new AllRejectingSignatureVisitor() { + @Override + public Result visitTypeVariable(JavaTypeVariable type) { + traversedTypes.add(type); + return Result.CONTINUE; + } + + @Override + public Result visitParameterizedType(JavaParameterizedType type) { + traversedTypes.add(type); + return Result.STOP; + } + }); + + assertThat(traversedTypes).containsExactlyElementsOf(types); + } + + private static JavaType.SignatureVisitor newTrackingVisitor(List traversedTypes) { + return new JavaType.SignatureVisitor() { + @Override + public Result visitClass(JavaClass type) { + traversedTypes.add(type); + return JavaType.SignatureVisitor.super.visitClass(type); + } + + @Override + public Result visitParameterizedType(JavaParameterizedType type) { + traversedTypes.add(type); + return JavaType.SignatureVisitor.super.visitParameterizedType(type); + } + + @Override + public Result visitTypeVariable(JavaTypeVariable type) { + traversedTypes.add(type); + return JavaType.SignatureVisitor.super.visitTypeVariable(type); + } + + @Override + public Result visitGenericArrayType(JavaGenericArrayType type) { + traversedTypes.add(type); + return JavaType.SignatureVisitor.super.visitGenericArrayType(type); + } + + @Override + public Result visitWildcardType(JavaWildcardType type) { + traversedTypes.add(type); + return JavaType.SignatureVisitor.super.visitWildcardType(type); + } + }; + } + + private static class ExpectedTypes extends ForwardingList { + private final List delegate = new ArrayList<>(); + + @Override + protected List delegate() { + return delegate; + } + + // some inherently unsafe syntactic sugar, trust the caller + @SuppressWarnings("unchecked") + T expect(JavaType javaType) { + add(javaType); + return (T) javaType; + } + } + + private static class AllRejectingSignatureVisitor implements JavaType.SignatureVisitor { + @Override + public Result visitClass(JavaClass type) { + throw new UnsupportedOperationException("should not be called"); + } + + @Override + public Result visitParameterizedType(JavaParameterizedType type) { + throw new UnsupportedOperationException("should not be called"); + } + + @Override + public Result visitTypeVariable(JavaTypeVariable type) { + throw new UnsupportedOperationException("should not be called"); + } + + @Override + public Result visitGenericArrayType(JavaGenericArrayType type) { + throw new UnsupportedOperationException("should not be called"); + } + + @Override + public Result visitWildcardType(JavaWildcardType type) { + throw new UnsupportedOperationException("should not be called"); + } + } +}