Skip to content

Commit 301ccd8

Browse files
committed
add type parameter dependencies from and to self
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]>
1 parent 12d1e5d commit 301ccd8

File tree

7 files changed

+201
-10
lines changed

7 files changed

+201
-10
lines changed

archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/ServiceHelper.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package com.tngtech.archunit.example.layers.service;
22

3+
import java.util.Map;
4+
import java.util.Set;
5+
6+
import com.tngtech.archunit.example.layers.controller.SomeUtility;
7+
import com.tngtech.archunit.example.layers.controller.one.SomeEnum;
38
import com.tngtech.archunit.example.layers.security.Secured;
49

510
/**
611
* Well modelled code always has lots of 'helpers' ;-)
712
*/
8-
public class ServiceHelper {
13+
@SuppressWarnings("unused")
14+
public class ServiceHelper<
15+
TYPE_PARAMETER_VIOLATING_LAYER_RULE extends SomeUtility,
16+
ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE extends Map<?, Set<? super SomeEnum>>> {
17+
918
public Object insecure = new Object();
1019
@Secured
1120
public Object properlySecured = new Object();

archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@
166166
import static com.tngtech.archunit.testutils.ExpectedDependency.field;
167167
import static com.tngtech.archunit.testutils.ExpectedDependency.inheritanceFrom;
168168
import static com.tngtech.archunit.testutils.ExpectedDependency.method;
169+
import static com.tngtech.archunit.testutils.ExpectedDependency.typeParameter;
169170
import static com.tngtech.archunit.testutils.ExpectedLocation.javaClass;
170171
import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOf;
171172
import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOfAnonymousClassOf;
@@ -739,6 +740,8 @@ Stream<DynamicTest> LayerDependencyRulesTest() {
739740
.by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController)
740741
.toMethod(UseCaseTwoController.class, doSomethingTwo)
741742
.inLine(25).asDependency())
743+
.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
744+
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
742745
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
743746
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
744747
.by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod).withParameter(UseCaseTwoController[].class))
@@ -796,6 +799,8 @@ Stream<DynamicTest> LayerDependencyRulesTest() {
796799
.by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController)
797800
.toMethod(UseCaseTwoController.class, doSomethingTwo)
798801
.inLine(25).asDependency())
802+
.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
803+
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
799804
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
800805
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
801806
.by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod).withParameter(UseCaseTwoController[].class))
@@ -863,6 +868,8 @@ Stream<DynamicTest> LayeredArchitectureTest() {
863868
.inLine(27)
864869
.asDependency())
865870

871+
.by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class))
872+
.by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class))
866873
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withParameter(UseCaseTwoController.class))
867874
.by(method(ServiceViolatingLayerRules.class, dependentMethod).withReturnType(SomeGuiController.class))
868875
.by(method(ServiceViolatingLayerRules.class, dependentOnComponentTypeMethod)

archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public static InheritanceCreator inheritanceFrom(Class<?> clazz) {
2525
return new InheritanceCreator(clazz);
2626
}
2727

28+
public static TypeParameterCreator typeParameter(Class<?> clazz, String typeParameterName) {
29+
return new TypeParameterCreator(clazz, typeParameterName);
30+
}
31+
2832
public static AnnotationDependencyCreator annotatedClass(Class<?> clazz) {
2933
return new AnnotationDependencyCreator(clazz);
3034
}
@@ -85,6 +89,21 @@ public ExpectedDependency implementing(Class<?> anInterface) {
8589
}
8690
}
8791

92+
public static class TypeParameterCreator {
93+
private final Class<?> clazz;
94+
private final String typeParameterName;
95+
96+
private TypeParameterCreator(Class<?> clazz, String typeParameterName) {
97+
this.clazz = clazz;
98+
this.typeParameterName = typeParameterName;
99+
}
100+
101+
public ExpectedDependency dependingOn(Class<?> typeParameterDependency) {
102+
return new ExpectedDependency(clazz, typeParameterDependency,
103+
getDependencyPattern(clazz.getName(), "has type parameter '" + typeParameterName + "' depending on", typeParameterDependency.getName(), 0));
104+
}
105+
}
106+
88107
public static class AccessCreator {
89108
private final Class<?> originClass;
90109

archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,26 +120,31 @@ static Set<Dependency> tryCreateFromInstanceofCheck(InstanceofCheck instanceofCh
120120
}
121121

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

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

132-
private static Origin findSuitableOrigin(JavaAnnotation<?> annotation) {
133-
Object annotatedElement = annotation.getAnnotatedElement();
134-
if (annotatedElement instanceof JavaMember) {
135-
JavaMember member = (JavaMember) annotatedElement;
132+
static Set<Dependency> tryCreateFromTypeParameter(JavaTypeVariable<?> typeParameter, JavaClass typeParameterDependency) {
133+
String dependencyType = "has type parameter '" + typeParameter.getName() + "' depending on";
134+
Origin origin = findSuitableOrigin(typeParameter, typeParameter.getOwner());
135+
return tryCreateDependency(origin.originClass, origin.originDescription, dependencyType, typeParameterDependency);
136+
}
137+
138+
private static Origin findSuitableOrigin(Object dependencyCause, Object originCandidate) {
139+
if (originCandidate instanceof JavaMember) {
140+
JavaMember member = (JavaMember) originCandidate;
136141
return new Origin(member.getOwner(), member.getDescription());
137142
}
138-
if (annotatedElement instanceof JavaClass) {
139-
JavaClass clazz = (JavaClass) annotatedElement;
143+
if (originCandidate instanceof JavaClass) {
144+
JavaClass clazz = (JavaClass) originCandidate;
140145
return new Origin(clazz, clazz.getDescription());
141146
}
142-
throw new IllegalStateException("Could not find suitable dependency origin for " + annotation);
147+
throw new IllegalStateException("Could not find suitable dependency origin for " + dependencyCause);
143148
}
144149

145150
private static Set<Dependency> tryCreateDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) {

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.tngtech.archunit.core.domain.properties.HasAnnotations;
2626

2727
import static com.google.common.base.Suppliers.memoize;
28+
import static com.google.common.collect.Iterables.concat;
2829

2930
class JavaClassDependencies {
3031
private final JavaClass javaClass;
@@ -49,6 +50,7 @@ public Set<Dependency> get() {
4950
result.addAll(constructorParameterDependenciesFromSelf());
5051
result.addAll(annotationDependenciesFromSelf());
5152
result.addAll(instanceofCheckDependenciesFromSelf());
53+
result.addAll(typeParameterDependenciesFromSelf());
5254
return result.build();
5355
}
5456
});
@@ -139,6 +141,53 @@ private Set<Dependency> instanceofCheckDependenciesFromSelf() {
139141
return result.build();
140142
}
141143

144+
private Set<Dependency> typeParameterDependenciesFromSelf() {
145+
ImmutableSet.Builder<Dependency> result = ImmutableSet.builder();
146+
for (JavaTypeVariable<?> typeVariable : javaClass.getTypeParameters()) {
147+
result.addAll(getDependenciesFromTypeParameter(typeVariable));
148+
}
149+
return result.build();
150+
}
151+
152+
private Set<Dependency> getDependenciesFromTypeParameter(JavaTypeVariable<?> typeVariable) {
153+
ImmutableSet.Builder<Dependency> dependenciesBuilder = ImmutableSet.builder();
154+
for (JavaType bound : typeVariable.getUpperBounds()) {
155+
for (JavaClass typeParameterDependency : dependenciesOfType(bound)) {
156+
dependenciesBuilder.addAll(Dependency.tryCreateFromTypeParameter(typeVariable, typeParameterDependency));
157+
}
158+
}
159+
return dependenciesBuilder.build();
160+
}
161+
162+
private static Iterable<JavaClass> dependenciesOfType(JavaType javaType) {
163+
ImmutableSet.Builder<JavaClass> result = ImmutableSet.builder();
164+
if (javaType instanceof JavaClass) {
165+
result.add((JavaClass) javaType);
166+
} else if (javaType instanceof JavaParameterizedType) {
167+
result.addAll(dependenciesOfParameterizedType((JavaParameterizedType) javaType));
168+
} else if (javaType instanceof JavaWildcardType) {
169+
result.addAll(dependenciesOfWildcardType((JavaWildcardType) javaType));
170+
}
171+
return result.build();
172+
}
173+
174+
private static Set<JavaClass> dependenciesOfParameterizedType(JavaParameterizedType parameterizedType) {
175+
ImmutableSet.Builder<JavaClass> result = ImmutableSet.<JavaClass>builder()
176+
.add(parameterizedType.toErasure());
177+
for (JavaType typeArgument : parameterizedType.getActualTypeArguments()) {
178+
result.addAll(dependenciesOfType(typeArgument));
179+
}
180+
return result.build();
181+
}
182+
183+
private static Set<JavaClass> dependenciesOfWildcardType(JavaWildcardType javaType) {
184+
ImmutableSet.Builder<JavaClass> result = ImmutableSet.builder();
185+
for (JavaType bound : concat(javaType.getUpperBounds(), javaType.getLowerBounds())) {
186+
result.addAll(dependenciesOfType(bound));
187+
}
188+
return result.build();
189+
}
190+
142191
private <T extends HasDescription & HasAnnotations<?>> Set<Dependency> annotationDependencies(Set<T> annotatedObjects) {
143192
ImmutableSet.Builder<Dependency> result = ImmutableSet.builder();
144193
for (T annotated : annotatedObjects) {

archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,24 @@ public void Dependency_from_member_annotation_member(JavaMember annotatedMember)
257257
.contains(annotatedMember.getDescription() + " has annotation member of type <" + memberType.getName() + ">");
258258
}
259259

260+
@Test
261+
public void Dependency_from_type_parameter() {
262+
@SuppressWarnings("unused")
263+
class ClassWithTypeParameters<T extends String> {
264+
}
265+
266+
JavaClass javaClass = importClassesWithContext(ClassWithTypeParameters.class, String.class).get(ClassWithTypeParameters.class);
267+
JavaTypeVariable<?> typeParameter = javaClass.getTypeParameters().get(0);
268+
269+
Dependency dependency = getOnlyElement(Dependency.tryCreateFromTypeParameter(typeParameter, typeParameter.getUpperBounds().get(0).toErasure()));
270+
271+
assertThatType(dependency.getOriginClass()).matches(ClassWithTypeParameters.class);
272+
assertThatType(dependency.getTargetClass()).matches(String.class);
273+
assertThat(dependency.getDescription()).as("description").contains(String.format(
274+
"Class <%s> has type parameter '%s' depending on <%s> in (%s.java:0)",
275+
ClassWithTypeParameters.class.getName(), typeParameter.getName(), String.class.getName(), getClass().getSimpleName()));
276+
}
277+
260278
@Test
261279
public void origin_predicates_match() {
262280
assertThatDependency(Origin.class, Target.class)

archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.tngtech.archunit.core.domain;
22

3+
import java.io.BufferedInputStream;
4+
import java.io.File;
35
import java.io.Serializable;
46
import java.lang.annotation.Retention;
57
import java.util.AbstractList;
68
import java.util.ArrayList;
79
import java.util.Collection;
810
import java.util.HashSet;
911
import java.util.List;
12+
import java.util.Map;
1013
import java.util.Set;
1114

1215
import com.google.common.collect.FluentIterable;
@@ -574,6 +577,59 @@ public void finds_array_component_types_as_dependencies_from_self() {
574577
.inLocation(ArrayComponentTypeDependencies.class, 18));
575578
}
576579

580+
@Test
581+
public void direct_dependencies_from_self_by_type_parameter() {
582+
@SuppressWarnings("unused")
583+
class ClassWithTypeParameters<
584+
FIRST extends List<?> & Serializable & Comparable<FIRST>,
585+
SECOND extends Map<
586+
Map.Entry<FIRST, Map.Entry<String, FIRST>>,
587+
Map<? extends BufferedInputStream[][],
588+
Map<? extends Serializable, List<List<? extends Set<? super Iterable<? super Map<FIRST, ? extends File>>>>>>>>,
589+
SELF extends ClassWithTypeParameters<FIRST, SECOND, SELF>> {
590+
}
591+
592+
JavaClass javaClass = importClasses(ClassWithTypeParameters.class).get(ClassWithTypeParameters.class);
593+
594+
assertThatDependencies(javaClass.getDirectDependenciesFromSelf())
595+
.contain(from(ClassWithTypeParameters.class).to(List.class).inLocation(getClass(), 0)
596+
.withDescriptionContaining("type parameter 'FIRST' depending on")
597+
598+
.from(ClassWithTypeParameters.class).to(Serializable.class).inLocation(getClass(), 0)
599+
.withDescriptionContaining("type parameter 'FIRST' depending on")
600+
601+
.from(ClassWithTypeParameters.class).to(Comparable.class).inLocation(getClass(), 0)
602+
.withDescriptionContaining("type parameter 'FIRST' depending on")
603+
604+
.from(ClassWithTypeParameters.class).to(Map.class).inLocation(getClass(), 0)
605+
.withDescriptionContaining("type parameter 'SECOND' depending on")
606+
607+
.from(ClassWithTypeParameters.class).to(Map.Entry.class).inLocation(getClass(), 0)
608+
.withDescriptionContaining("type parameter 'SECOND' depending on")
609+
610+
.from(ClassWithTypeParameters.class).to(String.class).inLocation(getClass(), 0)
611+
.withDescriptionContaining("type parameter 'SECOND' depending on")
612+
613+
.from(ClassWithTypeParameters.class).to(BufferedInputStream[][].class).inLocation(getClass(), 0)
614+
.withDescriptionContaining("type parameter 'SECOND' depending on")
615+
616+
.from(ClassWithTypeParameters.class).to(Serializable.class).inLocation(getClass(), 0)
617+
.withDescriptionContaining("type parameter 'SECOND' depending on")
618+
619+
.from(ClassWithTypeParameters.class).to(List.class).inLocation(getClass(), 0)
620+
.withDescriptionContaining("type parameter 'SECOND' depending on")
621+
622+
.from(ClassWithTypeParameters.class).to(Set.class).inLocation(getClass(), 0)
623+
.withDescriptionContaining("type parameter 'SECOND' depending on")
624+
625+
.from(ClassWithTypeParameters.class).to(Iterable.class).inLocation(getClass(), 0)
626+
.withDescriptionContaining("type parameter 'SECOND' depending on")
627+
628+
.from(ClassWithTypeParameters.class).to(File.class).inLocation(getClass(), 0)
629+
.withDescriptionContaining("type parameter 'SECOND' depending on")
630+
);
631+
}
632+
577633
@Test
578634
public void direct_dependencies_from_self_finds_correct_set_of_target_types() {
579635
JavaClass javaClass = importPackagesOf(getClass()).get(ClassWithAnnotationDependencies.class);
@@ -743,6 +799,34 @@ public void finds_array_component_types_as_dependencies_to_self() {
743799
.inLocation(ArrayComponentTypeDependencies.class, 18));
744800
}
745801

802+
@Test
803+
public void direct_dependencies_to_self_by_type_parameter() {
804+
class ClassOtherTypeSignaturesDependOn {
805+
}
806+
@SuppressWarnings("unused")
807+
class FirstDependingOnOtherThroughTypeParameter<T extends ClassOtherTypeSignaturesDependOn> {
808+
}
809+
@SuppressWarnings("unused")
810+
class SecondDependingOnOtherThroughTypeParameter<
811+
U extends Map<?, List<? super Set<? extends ClassOtherTypeSignaturesDependOn>>>,
812+
V extends Map<ClassOtherTypeSignaturesDependOn, ClassOtherTypeSignaturesDependOn>> {
813+
}
814+
815+
JavaClass someClass = importClasses(ClassOtherTypeSignaturesDependOn.class, FirstDependingOnOtherThroughTypeParameter.class, SecondDependingOnOtherThroughTypeParameter.class)
816+
.get(ClassOtherTypeSignaturesDependOn.class);
817+
818+
assertThatDependencies(someClass.getDirectDependenciesToSelf())
819+
.contain(from(FirstDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0)
820+
.withDescriptionContaining("type parameter 'T' depending on")
821+
822+
.from(SecondDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0)
823+
.withDescriptionContaining("type parameter 'U' depending on")
824+
825+
.from(SecondDependingOnOtherThroughTypeParameter.class).to(ClassOtherTypeSignaturesDependOn.class).inLocation(getClass(), 0)
826+
.withDescriptionContaining("type parameter 'V' depending on")
827+
);
828+
}
829+
746830
@Test
747831
public void direct_dependencies_to_self_finds_correct_set_of_origin_types() {
748832
JavaClasses classes = importPackagesOf(getClass());

0 commit comments

Comments
 (0)