Skip to content

Commit

Permalink
add type parameter dependencies from and to self
Browse files Browse the repository at this point in the history
Now that we have the full type parameter info for any `JavaClass` (e.g. `class Foo<T extends Set<? super Bar>>`), we can add type parameter dependencies to `JavaClass.getDirectDependenciesFromSelf()` and `JavaClass.getDirectDependenciesToSelf()`. In particular any other class that appears within the type signature should count as a dependency of the class, e.g. in the former example `Foo` should report type parameter dependencies on `Set` and `Bar`.
I also did consider to add some sort of visitor API to the type signature, but in the end I went with a simple instanceof chain in this one place. I could not come up with a generic, yet easy and use-/meaningful visitor interface that I would consider a good addition to the public API. Since within ArchUnit there is also only one use case so far, I decided that this part of the domain model will likely be stable enough to not cause any maintainability issues (after all I can't think of any other `JavaType` to be added in the near future and we have the Reflection API to peek into which sorts of `JavaType` came up in the wider context over the last decades).

Signed-off-by: Peter Gafert <[email protected]>
  • Loading branch information
codecholeric committed Dec 15, 2020
1 parent 12d1e5d commit 301ccd8
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package com.tngtech.archunit.example.layers.service;

import java.util.Map;
import java.util.Set;

import com.tngtech.archunit.example.layers.controller.SomeUtility;
import com.tngtech.archunit.example.layers.controller.one.SomeEnum;
import com.tngtech.archunit.example.layers.security.Secured;

/**
* Well modelled code always has lots of 'helpers' ;-)
*/
public class ServiceHelper {
@SuppressWarnings("unused")
public class ServiceHelper<
TYPE_PARAMETER_VIOLATING_LAYER_RULE extends SomeUtility,
ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE extends Map<?, Set<? super SomeEnum>>> {

public Object insecure = new Object();
@Secured
public Object properlySecured = new Object();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
import static com.tngtech.archunit.testutils.ExpectedDependency.field;
import static com.tngtech.archunit.testutils.ExpectedDependency.inheritanceFrom;
import static com.tngtech.archunit.testutils.ExpectedDependency.method;
import static com.tngtech.archunit.testutils.ExpectedDependency.typeParameter;
import static com.tngtech.archunit.testutils.ExpectedLocation.javaClass;
import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOf;
import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOfAnonymousClassOf;
Expand Down Expand Up @@ -739,6 +740,8 @@ Stream<DynamicTest> LayerDependencyRulesTest() {
.by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController)
.toMethod(UseCaseTwoController.class, doSomethingTwo)
.inLine(25).asDependency())
.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
.by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod).withParameter(UseCaseTwoController[].class))
Expand Down Expand Up @@ -796,6 +799,8 @@ Stream<DynamicTest> LayerDependencyRulesTest() {
.by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController)
.toMethod(UseCaseTwoController.class, doSomethingTwo)
.inLine(25).asDependency())
.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
.by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod).withParameter(UseCaseTwoController[].class))
Expand Down Expand Up @@ -863,6 +868,8 @@ Stream<DynamicTest> LayeredArchitectureTest() {
.inLine(27)
.asDependency())

.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
.by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public static InheritanceCreator inheritanceFrom(Class<?> clazz) {
return new InheritanceCreator(clazz);
}

public static TypeParameterCreator typeParameter(Class<?> clazz, String typeParameterName) {
return new TypeParameterCreator(clazz, typeParameterName);
}

public static AnnotationDependencyCreator annotatedClass(Class<?> clazz) {
return new AnnotationDependencyCreator(clazz);
}
Expand Down Expand Up @@ -85,6 +89,21 @@ public ExpectedDependency implementing(Class<?> anInterface) {
}
}

public static class TypeParameterCreator {
private final Class<?> clazz;
private final String typeParameterName;

private TypeParameterCreator(Class<?> clazz, String typeParameterName) {
this.clazz = clazz;
this.typeParameterName = typeParameterName;
}

public ExpectedDependency dependingOn(Class<?> typeParameterDependency) {
return new ExpectedDependency(clazz, typeParameterDependency,
getDependencyPattern(clazz.getName(), "has type parameter '" + typeParameterName + "' depending on", typeParameterDependency.getName(), 0));
}
}

public static class AccessCreator {
private final Class<?> originClass;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,26 +120,31 @@ static Set<Dependency> tryCreateFromInstanceofCheck(InstanceofCheck instanceofCh
}

static Set<Dependency> tryCreateFromAnnotation(JavaAnnotation<?> target) {
Origin origin = findSuitableOrigin(target);
Origin origin = findSuitableOrigin(target, target.getAnnotatedElement());
return tryCreateDependency(origin.originClass, origin.originDescription, "is annotated with", target.getRawType());
}

static Set<Dependency> tryCreateFromAnnotationMember(JavaAnnotation<?> annotation, JavaClass memberType) {
Origin origin = findSuitableOrigin(annotation);
Origin origin = findSuitableOrigin(annotation, annotation.getAnnotatedElement());
return tryCreateDependency(origin.originClass, origin.originDescription, "has annotation member of type", memberType);
}

private static Origin findSuitableOrigin(JavaAnnotation<?> annotation) {
Object annotatedElement = annotation.getAnnotatedElement();
if (annotatedElement instanceof JavaMember) {
JavaMember member = (JavaMember) annotatedElement;
static Set<Dependency> tryCreateFromTypeParameter(JavaTypeVariable<?> typeParameter, JavaClass typeParameterDependency) {
String dependencyType = "has type parameter '" + typeParameter.getName() + "' depending on";
Origin origin = findSuitableOrigin(typeParameter, typeParameter.getOwner());
return tryCreateDependency(origin.originClass, origin.originDescription, dependencyType, typeParameterDependency);
}

private static Origin findSuitableOrigin(Object dependencyCause, Object originCandidate) {
if (originCandidate instanceof JavaMember) {
JavaMember member = (JavaMember) originCandidate;
return new Origin(member.getOwner(), member.getDescription());
}
if (annotatedElement instanceof JavaClass) {
JavaClass clazz = (JavaClass) annotatedElement;
if (originCandidate instanceof JavaClass) {
JavaClass clazz = (JavaClass) originCandidate;
return new Origin(clazz, clazz.getDescription());
}
throw new IllegalStateException("Could not find suitable dependency origin for " + annotation);
throw new IllegalStateException("Could not find suitable dependency origin for " + dependencyCause);
}

private static Set<Dependency> tryCreateDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.tngtech.archunit.core.domain.properties.HasAnnotations;

import static com.google.common.base.Suppliers.memoize;
import static com.google.common.collect.Iterables.concat;

class JavaClassDependencies {
private final JavaClass javaClass;
Expand All @@ -49,6 +50,7 @@ public Set<Dependency> get() {
result.addAll(constructorParameterDependenciesFromSelf());
result.addAll(annotationDependenciesFromSelf());
result.addAll(instanceofCheckDependenciesFromSelf());
result.addAll(typeParameterDependenciesFromSelf());
return result.build();
}
});
Expand Down Expand Up @@ -139,6 +141,53 @@ private Set<Dependency> instanceofCheckDependenciesFromSelf() {
return result.build();
}

private Set<Dependency> typeParameterDependenciesFromSelf() {
ImmutableSet.Builder<Dependency> result = ImmutableSet.builder();
for (JavaTypeVariable<?> typeVariable : javaClass.getTypeParameters()) {
result.addAll(getDependenciesFromTypeParameter(typeVariable));
}
return result.build();
}

private Set<Dependency> getDependenciesFromTypeParameter(JavaTypeVariable<?> typeVariable) {
ImmutableSet.Builder<Dependency> dependenciesBuilder = ImmutableSet.builder();
for (JavaType bound : typeVariable.getUpperBounds()) {
for (JavaClass typeParameterDependency : dependenciesOfType(bound)) {
dependenciesBuilder.addAll(Dependency.tryCreateFromTypeParameter(typeVariable, typeParameterDependency));
}
}
return dependenciesBuilder.build();
}

private static Iterable<JavaClass> dependenciesOfType(JavaType javaType) {
ImmutableSet.Builder<JavaClass> result = ImmutableSet.builder();
if (javaType instanceof JavaClass) {
result.add((JavaClass) javaType);
} else if (javaType instanceof JavaParameterizedType) {
result.addAll(dependenciesOfParameterizedType((JavaParameterizedType) javaType));
} else if (javaType instanceof JavaWildcardType) {
result.addAll(dependenciesOfWildcardType((JavaWildcardType) javaType));
}
return result.build();
}

private static Set<JavaClass> dependenciesOfParameterizedType(JavaParameterizedType parameterizedType) {
ImmutableSet.Builder<JavaClass> result = ImmutableSet.<JavaClass>builder()
.add(parameterizedType.toErasure());
for (JavaType typeArgument : parameterizedType.getActualTypeArguments()) {
result.addAll(dependenciesOfType(typeArgument));
}
return result.build();
}

private static Set<JavaClass> dependenciesOfWildcardType(JavaWildcardType javaType) {
ImmutableSet.Builder<JavaClass> result = ImmutableSet.builder();
for (JavaType bound : concat(javaType.getUpperBounds(), javaType.getLowerBounds())) {
result.addAll(dependenciesOfType(bound));
}
return result.build();
}

private <T extends HasDescription & HasAnnotations<?>> Set<Dependency> annotationDependencies(Set<T> annotatedObjects) {
ImmutableSet.Builder<Dependency> result = ImmutableSet.builder();
for (T annotated : annotatedObjects) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,24 @@ public void Dependency_from_member_annotation_member(JavaMember annotatedMember)
.contains(annotatedMember.getDescription() + " has annotation member of type <" + memberType.getName() + ">");
}

@Test
public void Dependency_from_type_parameter() {
@SuppressWarnings("unused")
class ClassWithTypeParameters<T extends String> {
}

JavaClass javaClass = importClassesWithContext(ClassWithTypeParameters.class, String.class).get(ClassWithTypeParameters.class);
JavaTypeVariable<?> typeParameter = javaClass.getTypeParameters().get(0);

Dependency dependency = getOnlyElement(Dependency.tryCreateFromTypeParameter(typeParameter, typeParameter.getUpperBounds().get(0).toErasure()));

assertThatType(dependency.getOriginClass()).matches(ClassWithTypeParameters.class);
assertThatType(dependency.getTargetClass()).matches(String.class);
assertThat(dependency.getDescription()).as("description").contains(String.format(
"Class <%s> has type parameter '%s' depending on <%s> in (%s.java:0)",
ClassWithTypeParameters.class.getName(), typeParameter.getName(), String.class.getName(), getClass().getSimpleName()));
}

@Test
public void origin_predicates_match() {
assertThatDependency(Origin.class, Target.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.tngtech.archunit.core.domain;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.FluentIterable;
Expand Down Expand Up @@ -574,6 +577,59 @@ public void finds_array_component_types_as_dependencies_from_self() {
.inLocation(ArrayComponentTypeDependencies.class, 18));
}

@Test
public void direct_dependencies_from_self_by_type_parameter() {
@SuppressWarnings("unused")
class ClassWithTypeParameters<
FIRST extends List<?> & Serializable & Comparable<FIRST>,
SECOND extends Map<
Map.Entry<FIRST, Map.Entry<String, FIRST>>,
Map<? extends BufferedInputStream[][],
Map<? extends Serializable, List<List<? extends Set<? super Iterable<? super Map<FIRST, ? extends File>>>>>>>>,
SELF extends ClassWithTypeParameters<FIRST, SECOND, SELF>> {
}

JavaClass javaClass = importClasses(ClassWithTypeParameters.class).get(ClassWithTypeParameters.class);

assertThatDependencies(javaClass.getDirectDependenciesFromSelf())
.contain(from(ClassWithTypeParameters.class).to(List.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'FIRST' depending on")

.from(ClassWithTypeParameters.class).to(Serializable.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'FIRST' depending on")

.from(ClassWithTypeParameters.class).to(Comparable.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'FIRST' depending on")

.from(ClassWithTypeParameters.class).to(Map.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(Map.Entry.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(String.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(BufferedInputStream[][].class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(Serializable.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(List.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(Set.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(Iterable.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")

.from(ClassWithTypeParameters.class).to(File.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'SECOND' depending on")
);
}

@Test
public void direct_dependencies_from_self_finds_correct_set_of_target_types() {
JavaClass javaClass = importPackagesOf(getClass()).get(ClassWithAnnotationDependencies.class);
Expand Down Expand Up @@ -743,6 +799,34 @@ public void finds_array_component_types_as_dependencies_to_self() {
.inLocation(ArrayComponentTypeDependencies.class, 18));
}

@Test
public void direct_dependencies_to_self_by_type_parameter() {
class ClassOtherTypeSignaturesDependOn {
}
@SuppressWarnings("unused")
class FirstDependingOnOtherThroughTypeParameter<T extends ClassOtherTypeSignaturesDependOn> {
}
@SuppressWarnings("unused")
class SecondDependingOnOtherThroughTypeParameter<
U extends Map<?, List<? super Set<? extends ClassOtherTypeSignaturesDependOn>>>,
V extends Map<ClassOtherTypeSignaturesDependOn, ClassOtherTypeSignaturesDependOn>> {
}

JavaClass someClass = importClasses(ClassOtherTypeSignaturesDependOn.class, FirstDependingOnOtherThroughTypeParameter.class, SecondDependingOnOtherThroughTypeParameter.class)
.get(ClassOtherTypeSignaturesDependOn.class);

assertThatDependencies(someClass.getDirectDependenciesToSelf())
.contain(from(FirstDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'T' depending on")

.from(SecondDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'U' depending on")

.from(SecondDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0)
.withDescriptionContaining("type parameter 'V' depending on")
);
}

@Test
public void direct_dependencies_to_self_finds_correct_set_of_origin_types() {
JavaClasses classes = importPackagesOf(getClass());
Expand Down

0 comments on commit 301ccd8

Please sign in to comment.