diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java index 9a672db5f9..6c844ddbfe 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java @@ -972,6 +972,9 @@ Stream OnionArchitectureTest() { .by(field(ShoppingService.class, "productRepository").ofType(ProductRepository.class)) .by(field(ShoppingService.class, "shoppingCartRepository").ofType(ShoppingCartRepository.class)) + .by(method(AdministrationCLI.class, "handle") + .referencingClassObject(ProductRepository.class) + .inLine(16)) .by(callFromMethod(AdministrationCLI.class, "handle", String[].class, AdministrationPort.class) .toMethod(ProductRepository.class, "getTotalCount") .inLine(17).asDependency()) diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java index 4a2a19b5dd..ecf6fa2f66 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java @@ -190,7 +190,11 @@ public ExpectedDependency withAnnotationType(Class annotationType) { } public AddsLineNumber checkingInstanceOf(Class target) { - return new AddsLineNumber(owner, getOriginName(), target); + return new AddsLineNumber(owner, getOriginName(), "checks instanceof", target); + } + + public AddsLineNumber referencingClassObject(Class target) { + return new AddsLineNumber(owner, getOriginName(), "references class object", target); } private String getOriginName() { @@ -201,15 +205,17 @@ public static class AddsLineNumber { private final Class owner; private final String origin; private final Class target; + private final String dependencyType; - private AddsLineNumber(Class owner, String origin, Class target) { + private AddsLineNumber(Class owner, String origin, String dependencyType, Class target) { this.owner = owner; this.origin = origin; this.target = target; + this.dependencyType = dependencyType; } public ExpectedDependency inLine(int lineNumber) { - String dependencyPattern = getDependencyPattern(origin, "checks instanceof", target.getName(), lineNumber); + String dependencyPattern = getDependencyPattern(origin, dependencyType, target.getName(), lineNumber); return new ExpectedDependency(owner, target, dependencyPattern); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java index 1065485389..af2850301b 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java @@ -122,6 +122,12 @@ static Set tryCreateFromInstanceofCheck(InstanceofCheck instanceofCh instanceofCheck.getRawType(), instanceofCheck.getSourceCodeLocation()); } + static Set tryCreateFromReferencedClassObject(ReferencedClassObject referencedClassObject) { + return tryCreateDependency( + referencedClassObject.getOwner(), "references class object", + referencedClassObject.getRawType(), referencedClassObject.getSourceCodeLocation()); + } + static Set tryCreateFromAnnotation(JavaAnnotation target) { Origin origin = findSuitableOrigin(target, target.getAnnotatedElement()); return tryCreateDependency(origin, "is annotated with", target.getRawType()); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java index c08dfd60e4..3d8d957884 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java @@ -52,6 +52,7 @@ public Set get() { result.addAll(constructorParameterDependenciesFromSelf()); result.addAll(annotationDependenciesFromSelf()); result.addAll(instanceofCheckDependenciesFromSelf()); + result.addAll(referencedClassObjectDependenciesFromSelf()); result.addAll(typeParameterDependenciesFromSelf()); return result.build(); } @@ -158,6 +159,14 @@ private Set instanceofCheckDependenciesFromSelf() { return result.build(); } + private Set referencedClassObjectDependenciesFromSelf() { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (ReferencedClassObject referencedClassObject : javaClass.getReferencedClassObjects()) { + result.addAll(Dependency.tryCreateFromReferencedClassObject(referencedClassObject)); + } + return result.build(); + } + private Set typeParameterDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaTypeVariable typeVariable : javaClass.getTypeParameters()) { diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java index 3d7aed0736..da3786e878 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java @@ -6,13 +6,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.nio.file.FileSystem; import java.util.Set; import com.google.common.base.MoreObjects; +import com.google.common.collect.FluentIterable; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.testobjects.ClassWithArrayDependencies; import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck; import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck.InstanceOfCheckTarget; +import com.tngtech.archunit.core.domain.testobjects.DependenciesOnClassObjects; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.testutil.Assertions; import com.tngtech.archunit.testutil.assertion.DependenciesAssertion; @@ -24,6 +27,7 @@ import org.junit.runner.RunWith; import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.base.Guava.toGuava; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_ORIGIN_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_TARGET_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; @@ -32,6 +36,7 @@ import static com.tngtech.archunit.core.domain.TestUtils.importClassWithContext; import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext; import static com.tngtech.archunit.core.domain.TestUtils.simulateCall; +import static com.tngtech.archunit.core.domain.properties.HasType.Predicates.rawType; import static com.tngtech.archunit.testutil.Assertions.assertThat; import static com.tngtech.archunit.testutil.Assertions.assertThatDependencies; import static com.tngtech.archunit.testutil.Assertions.assertThatType; @@ -289,6 +294,24 @@ class ClassWithTypeParameters extends Base { ClassWithTypeParameters.class.getName(), Base.class.getName(), String.class.getName(), getClass().getSimpleName())); } + @Test + public void Dependency_from_referenced_class_object() { + JavaMethod origin = new ClassFileImporter() + .importClass(DependenciesOnClassObjects.class) + .getMethod("referencedClassObjectsInMethod"); + ReferencedClassObject referencedClassObject = getOnlyElement(FluentIterable.from(origin.getReferencedClassObjects()) + .filter(toGuava(rawType(FileSystem.class))) + .toSet()); + + Dependency dependency = getOnlyElement(Dependency.tryCreateFromReferencedClassObject(referencedClassObject)); + + assertThatType(dependency.getOriginClass()).matches(DependenciesOnClassObjects.class); + assertThatType(dependency.getTargetClass()).matches(FileSystem.class); + assertThat(dependency.getDescription()).as("description") + .contains(String.format("Method <%s> references class object <%s> in (%s.java:%d)", + origin.getFullName(), FileSystem.class.getName(), DependenciesOnClassObjects.class.getSimpleName(), 22)); + } + @Test public void origin_predicates_match() { assertThatDependency(Origin.class, Target.class) diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java index ca7c62f975..b764a45f62 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java @@ -2,8 +2,13 @@ import java.io.BufferedInputStream; import java.io.File; +import java.io.FilterInputStream; import java.io.Serializable; import java.lang.annotation.Retention; +import java.nio.Buffer; +import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.Path; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; @@ -25,6 +30,7 @@ import com.tngtech.archunit.core.domain.testobjects.ArrayComponentTypeDependencies; import com.tngtech.archunit.core.domain.testobjects.B; import com.tngtech.archunit.core.domain.testobjects.ComponentTypeDependency; +import com.tngtech.archunit.core.domain.testobjects.DependenciesOnClassObjects; import com.tngtech.archunit.core.domain.testobjects.InterfaceForA; import com.tngtech.archunit.core.domain.testobjects.SuperA; import com.tngtech.archunit.core.importer.ClassFileImporter; @@ -639,6 +645,31 @@ class ClassWithTypeParameters< ); } + @Test + public void direct_dependencies_from_self_by_referenced_class_objects() { + JavaClass javaClass = new ClassFileImporter().importClass(DependenciesOnClassObjects.class); + + assertThatDependencies(javaClass.getDirectDependenciesFromSelf()) + .contain(from(DependenciesOnClassObjects.class).to(FilterInputStream.class).inLocation(DependenciesOnClassObjects.class, 16) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(Buffer.class).inLocation(DependenciesOnClassObjects.class, 16) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(File.class).inLocation(DependenciesOnClassObjects.class, 19) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(Path.class).inLocation(DependenciesOnClassObjects.class, 19) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(FileSystem.class).inLocation(DependenciesOnClassObjects.class, 22) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(Charset.class).inLocation(DependenciesOnClassObjects.class, 22) + .withDescriptionContaining("references class object") + ); + } + @Test public void direct_dependencies_from_self_finds_correct_set_of_target_types() { JavaClass javaClass = importPackagesOf(getClass()).get(ClassWithAnnotationDependencies.class); @@ -872,6 +903,21 @@ class SecondDependingOnOtherThroughTypeParameter< ); } + @Test + public void direct_dependencies_to_self_by_referenced_class_objects() { + JavaClass javaClass = new ClassFileImporter() + .importClasses(DependenciesOnClassObjects.class, DependenciesOnClassObjects.MoreDependencies.class, Charset.class) + .get(Charset.class); + + assertThatDependencies(javaClass.getDirectDependenciesToSelf()) + .contain(from(DependenciesOnClassObjects.class).to(Charset.class).inLocation(DependenciesOnClassObjects.class, 22) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.MoreDependencies.class).to(Charset.class).inLocation(DependenciesOnClassObjects.class, 27) + .withDescriptionContaining("references class object") + ); + } + @Test public void direct_dependencies_to_self_finds_correct_set_of_origin_types() { JavaClasses classes = importPackagesOf(getClass()); @@ -879,17 +925,17 @@ public void direct_dependencies_to_self_finds_correct_set_of_origin_types() { Set origins = getOriginsOfDependenciesTo(classes.get(WithType.class)); assertThatTypes(origins).matchInAnyOrder( - ClassWithAnnotationDependencies.class, ClassWithSelfReferences.class, WithNestedAnnotations.class, OnMethodParam.class); + getClass(), ClassWithAnnotationDependencies.class, ClassWithSelfReferences.class, WithNestedAnnotations.class, OnMethodParam.class); origins = getOriginsOfDependenciesTo(classes.get(B.class)); assertThatTypes(origins).matchInAnyOrder( - ClassWithAnnotationDependencies.class, OnMethodParam.class, AAccessingB.class, AhavingMembersOfTypeB.class); + getClass(), ClassWithAnnotationDependencies.class, OnMethodParam.class, AAccessingB.class, AhavingMembersOfTypeB.class); origins = getOriginsOfDependenciesTo(classes.get(SomeEnumAsNestedAnnotationParameter.class)); assertThatTypes(origins).matchInAnyOrder( - ClassWithAnnotationDependencies.class, WithEnum.class); + getClass(), ClassWithAnnotationDependencies.class, WithEnum.class); } private Set getOriginsOfDependenciesTo(JavaClass withType) { diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/DependenciesOnClassObjects.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/DependenciesOnClassObjects.java new file mode 100644 index 0000000000..06a3bb0531 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/DependenciesOnClassObjects.java @@ -0,0 +1,30 @@ +package com.tngtech.archunit.core.domain.testobjects; + +import java.io.File; +import java.io.FilterInputStream; +import java.nio.Buffer; +import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +@SuppressWarnings({"FieldMayBeFinal", "unused"}) +public class DependenciesOnClassObjects { + static { + List> referencedClassObjectsInStaticInitializer = ImmutableList.of(FilterInputStream.class, Buffer.class); + } + + List> referencedClassObjectsInConstructor = ImmutableList.>of(File.class, Path.class); + + List> referencedClassObjectsInMethod() { + return ImmutableList.of(FileSystem.class, Charset.class); + } + + public static class MoreDependencies { + Class moreReferencedClassObjectsInMethod() { + return Charset.class; + } + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java index 40d9c6d476..120111cf97 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java @@ -934,7 +934,7 @@ public void areNotInnerClasses_predicate(ClassesThat c public void areAnonymousClasses_predicate(ClassesThat classesShouldOnlyBeBy) { Set classes = filterClassesAppearingInFailureReport( classesShouldOnlyBeBy.areAnonymousClasses()) - .on(ClassAccessingAnonymousClass.class, anonymousClassBeingAccessed.getClass(), + .on(ClassAccessingAnonymousClass, anonymousClassBeingAccessed.getClass(), ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); assertThatTypes(classes).matchInAnyOrder(ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); @@ -945,10 +945,10 @@ public void areAnonymousClasses_predicate(ClassesThat public void areNotAnonymousClasses_predicate(ClassesThat classesShouldOnlyBeBy) { Set classes = filterClassesAppearingInFailureReport( classesShouldOnlyBeBy.areNotAnonymousClasses()) - .on(ClassAccessingAnonymousClass.class, anonymousClassBeingAccessed.getClass(), + .on(ClassAccessingAnonymousClass, anonymousClassBeingAccessed.getClass(), ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); - assertThatTypes(classes).matchInAnyOrder(ClassAccessingAnonymousClass.class, anonymousClassBeingAccessed.getClass()); + assertThatTypes(classes).matchInAnyOrder(ClassAccessingAnonymousClass, anonymousClassBeingAccessed.getClass()); } @Test @@ -1050,6 +1050,14 @@ private static DescribedPredicate classWithNameOf(Class type) { return GET_NAME.is(equalTo(type.getName())); } + private static Class classForName(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + private static class ClassAccessedByFoo { void method() { } @@ -1306,6 +1314,9 @@ void access() { private class InnerMemberClassBeingAccessed { } + // This must be loaded via Reflection, otherwise the test will be tainted by the dependency on the class object + private static final Class ClassAccessingAnonymousClass = classForName("com.tngtech.archunit.lang.syntax.elements.ShouldOnlyByClassesThatTest$ClassAccessingAnonymousClass"); + private static class ClassAccessingAnonymousClass { @SuppressWarnings("unused") void access() { @@ -1322,13 +1333,18 @@ public void run() { private static class ClassBeingAccessedByLocalClass { static Class getLocalClass() { + @SuppressWarnings({"unused", "InstantiationOfUtilityClass"}) class LocalClass { - @SuppressWarnings("unused") void access() { new ClassBeingAccessedByLocalClass(); } } - return LocalClass.class; + try { + // This must be loaded via Reflection, otherwise the test will be tainted by the dependency on the class object + return Class.forName("com.tngtech.archunit.lang.syntax.elements.ShouldOnlyByClassesThatTest$ClassBeingAccessedByLocalClass$1LocalClass"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } } } }