Skip to content

Commit

Permalink
add support to traverse JavaType signature
Browse files Browse the repository at this point in the history
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 <peter.gafert@archunit.org>
  • Loading branch information
codecholeric committed Apr 9, 2024
1 parent 99e5dae commit 8456198
Show file tree
Hide file tree
Showing 7 changed files with 640 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,11 @@ public Set<JavaClass> getAllInvolvedRawTypes() {
return ImmutableSet.of(getBaseComponentType());
}

@Override
public void traverseSignature(SignatureVisitor visitor) {
SignatureTraversal.from(visitor).visitClass(this);
}

@PublicAPI(usage = ACCESS)
public Optional<JavaClass> getRawSuperclass() {
return superclass.getRaw();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public Set<JavaClass> getAllInvolvedRawTypes() {
return this.componentType.getAllInvolvedRawTypes();
}

@Override
public void traverseSignature(SignatureVisitor visitor) {
SignatureTraversal.from(visitor).visitGenericArrayType(this);
}

@Override
public String toString() {
return getClass().getSimpleName() + '{' + getName() + '}';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public interface JavaParameterizedType extends JavaType {
*/
@PublicAPI(usage = ACCESS)
List<JavaType> getActualTypeArguments();

@Override
default void traverseSignature(SignatureVisitor visitor) {
SignatureTraversal.from(visitor).visitParameterizedType(this);
}
}
175 changes: 175 additions & 0 deletions archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,6 +93,94 @@ public interface JavaType extends HasName {
@PublicAPI(usage = ACCESS)
Set<JavaClass> getAllInvolvedRawTypes();

/**
* Traverses through the signature of this {@link JavaType}.<br>
* 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}.<br>
* The following is a simple visualization of such a signature tree:
* <pre><code>
* List&lt;Map&lt;? extends Serializable, String[]&gt;&gt;
* |
* Map&lt;? extends Serializable, String[]&gt;
* / \
* ? extends Serializable String[]
* |
* Serializable
* </code></pre>
* 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}.<br><br>
* Note that the traversal will continue to traverse bounds of type variables,
* even if that type variable isn't declared in this signature itself.<br>
* E.g. take the following scenario
* <pre><code>
* class Example&lt;T extends String&gt; {
* T field;
* }</code></pre>
* 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}.<br><br>
* Also, note that the traversal will not continue down the type parameters of a raw type
* declared in a signature.<br>
* E.g. given the signature {@code class Example<T extends Map>} 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}.
*/
Expand All @@ -101,3 +198,81 @@ public JavaClass apply(JavaType input) {
};
}
}

class SignatureTraversal implements JavaType.SignatureVisitor {
private final Set<JavaType> 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<T extends Map>`, traversing into the type variables `K` and `V` of `Map` would be surprising).
Supplier<Iterable<JavaTypeVariable<JavaClass>>> 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 <CURRENT extends JavaType, NEXT extends JavaType> Result visit(
CURRENT type,
Function<CURRENT, Result> visitCurrent,
Supplier<Iterable<NEXT>> 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<? extends JavaType> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ public Set<JavaClass> getAllInvolvedRawTypes() {
.collect(toSet());
}

@Override
public void traverseSignature(SignatureVisitor visitor) {
SignatureTraversal.from(visitor).visitTypeVariable(this);
}

@Override
public String toString() {
String bounds = printExtendsClause() ? " extends " + joinTypeNames(upperBounds) : "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ public Set<JavaClass> getAllInvolvedRawTypes() {
.collect(toSet());
}

@Override
public void traverseSignature(SignatureVisitor visitor) {
SignatureTraversal.from(visitor).visitWildcardType(this);
}

@Override
public String toString() {
return getClass().getSimpleName() + '{' + getName() + '}';
Expand Down
Loading

0 comments on commit 8456198

Please sign in to comment.