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 9e473bddf0..74686e19b7 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 @@ -1031,6 +1031,17 @@ public Set getDirectDependenciesFromSelf() { return javaClassDependencies.getDirectDependenciesFromClass(); } + /** + * Returns the transitive closure of all dependencies originating from this class, i.e. its direct dependencies + * and the dependencies from all imported target classes. + * @return all transitive dependencies (including direct dependencies) from this class + * @see #getDirectDependenciesFromSelf() + */ + @PublicAPI(usage = ACCESS) + public Set getTransitiveDependenciesFromSelf() { + return JavaClassTransitiveDependencies.findTransitiveDependenciesFrom(this); + } + /** * Like {@link #getDirectDependenciesFromSelf()}, but instead returns all dependencies where this class * is target. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependencies.java new file mode 100644 index 0000000000..2a4e53b304 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependencies.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2020 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.core.domain; + +import com.google.common.collect.ImmutableSet; + +import java.util.HashSet; +import java.util.Set; + +class JavaClassTransitiveDependencies { + private JavaClassTransitiveDependencies() { + } + + static Set findTransitiveDependenciesFrom(JavaClass javaClass) { + ImmutableSet.Builder transitiveDependencies = ImmutableSet.builder(); + Set analyzedClasses = new HashSet<>(); // to avoid infinite recursion for cyclic dependencies + addTransitiveDependenciesFrom(javaClass, transitiveDependencies, analyzedClasses); + return transitiveDependencies.build(); + } + + private static void addTransitiveDependenciesFrom(JavaClass javaClass, ImmutableSet.Builder transitiveDependencies, Set analyzedClasses) { + analyzedClasses.add(javaClass); // currently being analyzed + Set targetClassesToRecurse = new HashSet<>(); + for (Dependency dependency : javaClass.getDirectDependenciesFromSelf()) { + transitiveDependencies.add(dependency); + targetClassesToRecurse.add(dependency.getTargetClass().getBaseComponentType()); + } + for (JavaClass targetClass : targetClassesToRecurse) { + if (!analyzedClasses.contains(targetClass)) { + addTransitiveDependenciesFrom(targetClass, transitiveDependencies, analyzedClasses); + } + } + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependenciesTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependenciesTest.java new file mode 100644 index 0000000000..94b6a01913 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependenciesTest.java @@ -0,0 +1,146 @@ +package com.tngtech.archunit.core.domain; + +import com.tngtech.archunit.core.importer.ClassFileImporter; +import org.junit.Test; + +import static com.tngtech.archunit.testutil.Assertions.assertThatDependencies; + +public class JavaClassTransitiveDependenciesTest { + + @SuppressWarnings("unused") + static class AcyclicGraph { + static class A { + B b; + C[][] c; + } + + static class B { + Integer i; + } + + static class C { + D d; + } + + static class D { + String s; + } + } + + @Test + public void findsTransitiveDependenciesInAcyclicGraph() { + Class a = AcyclicGraph.A.class; + Class b = AcyclicGraph.B.class; + Class c = AcyclicGraph.C.class; + Class d = AcyclicGraph.D.class; + JavaClasses classes = new ClassFileImporter().importClasses(a, b, c, d); + Class cArray = AcyclicGraph.C[][].class; + + // @formatter:off + assertThatDependencies(classes.get(a).getTransitiveDependenciesFromSelf()) + .contain(a, Object.class) + .contain(a, b) + .contain(b, Object.class) + .contain(b, Integer.class) + .contain(a, cArray) + .contain(c, Object.class) + .contain(c, d) + .contain(d, Object.class) + .contain(d, String.class); + + assertThatDependencies(classes.get(b).getTransitiveDependenciesFromSelf()) + .contain(b, Object.class) + .contain(b, Integer.class); + + assertThatDependencies(classes.get(c).getTransitiveDependenciesFromSelf()) + .contain(c, Object.class) + .contain(c, d) + .contain(d, Object.class) + .contain(d, String.class); + // @formatter:on + } + + @SuppressWarnings("unused") + static class CyclicGraph { + static class A { + B b; + C[][] c; + D d; + } + + static class B { + Integer i; + } + + static class C { + A a; + } + + static class D { + E e; + } + + static class E { + A a; + String s; + } + } + + @Test + public void findsTransitiveDependenciesInCyclicGraph() { + Class a = CyclicGraph.A.class; + Class b = CyclicGraph.B.class; + Class c = CyclicGraph.C.class; + Class d = CyclicGraph.D.class; + Class e = CyclicGraph.E.class; + JavaClasses classes = new ClassFileImporter().importClasses(a, b, c, d, e); + Class cArray = CyclicGraph.C[][].class; + + // @formatter:off + assertThatDependencies(classes.get(a).getTransitiveDependenciesFromSelf()) + .contain(a, Object.class) + .contain(a, b) + .contain(b, Object.class) + .contain(b, Integer.class) + .contain(a, cArray) + .contain(c, Object.class) + .contain(c, a) + .contain(a, d) + .contain(d, Object.class) + .contain(d, e) + .contain(e, Object.class) + .contain(e, a) + .contain(e, String.class); + + assertThatDependencies(classes.get(c).getTransitiveDependenciesFromSelf()) + .contain(c, Object.class) + .contain(c, a) + .contain(a, Object.class) + .contain(a, b) + .contain(b, Object.class) + .contain(b, Integer.class) + .contain(a, cArray) + .contain(a, d) + .contain(d, Object.class) + .contain(d, e) + .contain(e, Object.class) + .contain(e, a) + .contain(e, String.class); + + assertThatDependencies(classes.get(d).getTransitiveDependenciesFromSelf()) + .contain(d, Object.class) + .contain(d, e) + .contain(e, Object.class) + .contain(e, a) + .contain(a, Object.class) + .contain(a, b) + .contain(b, Object.class) + .contain(b, Integer.class) + .contain(a, cArray) + .contain(c, Object.class) + .contain(c, a) + .contain(a, d) + .contain(e, String.class); + // @formatter:on + } +}