Skip to content

Commit 8c8db21

Browse files
authored
add method JavaClass.getTransitiveDependenciesFromSelf #401
Signed-off-by: Manfred Hanke <[email protected]>
2 parents 790ba9b + 29abd8f commit 8c8db21

File tree

4 files changed

+265
-2
lines changed

4 files changed

+265
-2
lines changed

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,9 @@ public Set<JavaMember> get() {
145145
.build();
146146
}
147147
});
148-
private JavaClassDependencies javaClassDependencies;
149-
private ReverseDependencies reverseDependencies;
148+
private JavaClassDependencies javaClassDependencies = new JavaClassDependencies(this); // just for stubs; will be overwritten for imported classes
149+
private ReverseDependencies reverseDependencies = ReverseDependencies.EMPTY; // just for stubs; will be overwritten for imported classes
150+
private boolean fullyImported = false;
150151

151152
JavaClass(JavaClassBuilder builder) {
152153
source = checkNotNull(builder.getSource());
@@ -1030,6 +1031,17 @@ public Set<Dependency> getDirectDependenciesFromSelf() {
10301031
return javaClassDependencies.getDirectDependenciesFromClass();
10311032
}
10321033

1034+
/**
1035+
* Returns the transitive closure of all dependencies originating from this class, i.e. its direct dependencies
1036+
* and the dependencies from all imported target classes.
1037+
* @return all transitive dependencies (including direct dependencies) from this class
1038+
* @see #getDirectDependenciesFromSelf()
1039+
*/
1040+
@PublicAPI(usage = ACCESS)
1041+
public Set<Dependency> getTransitiveDependenciesFromSelf() {
1042+
return JavaClassTransitiveDependencies.findTransitiveDependenciesFrom(this);
1043+
}
1044+
10331045
/**
10341046
* Like {@link #getDirectDependenciesFromSelf()}, but instead returns all dependencies where this class
10351047
* is target.
@@ -1149,6 +1161,17 @@ public Set<InstanceofCheck> getInstanceofChecksWithTypeOfSelf() {
11491161
return reverseDependencies.getInstanceofChecksWithTypeOf(this);
11501162
}
11511163

1164+
/**
1165+
* @return Whether this class has been fully imported, including all dependencies.<br>
1166+
* Classes that are only transitively imported are not necessarily fully imported.<br><br>
1167+
* Suppose you only import a class {@code Foo} that calls a method of class {@code Bar}.
1168+
* Then {@code Bar} is, as a dependency of the fully imported class {@code Foo}, only transitively imported.
1169+
*/
1170+
@PublicAPI(usage = ACCESS)
1171+
public boolean isFullyImported() {
1172+
return fullyImported;
1173+
}
1174+
11521175
/**
11531176
* @param clazz An arbitrary type
11541177
* @return true, if this {@link JavaClass} represents the same class as the supplied {@link Class}, otherwise false
@@ -1301,6 +1324,7 @@ JavaClassDependencies completeFrom(ImportContext context) {
13011324
codeUnit.completeFrom(context);
13021325
}
13031326
javaClassDependencies = new JavaClassDependencies(this);
1327+
fullyImported = true;
13041328
return javaClassDependencies;
13051329
}
13061330

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2014-2020 TNG Technology Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.tngtech.archunit.core.domain;
17+
18+
import com.google.common.collect.ImmutableSet;
19+
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
23+
class JavaClassTransitiveDependencies {
24+
private JavaClassTransitiveDependencies() {
25+
}
26+
27+
static Set<Dependency> findTransitiveDependenciesFrom(JavaClass javaClass) {
28+
ImmutableSet.Builder<Dependency> transitiveDependencies = ImmutableSet.builder();
29+
Set<JavaClass> analyzedClasses = new HashSet<>(); // to avoid infinite recursion for cyclic dependencies
30+
addTransitiveDependenciesFrom(javaClass, transitiveDependencies, analyzedClasses);
31+
return transitiveDependencies.build();
32+
}
33+
34+
private static void addTransitiveDependenciesFrom(JavaClass javaClass, ImmutableSet.Builder<Dependency> transitiveDependencies, Set<JavaClass> analyzedClasses) {
35+
analyzedClasses.add(javaClass); // currently being analyzed
36+
Set<JavaClass> targetClassesToRecurse = new HashSet<>();
37+
for (Dependency dependency : javaClass.getDirectDependenciesFromSelf()) {
38+
transitiveDependencies.add(dependency);
39+
targetClassesToRecurse.add(dependency.getTargetClass().getBaseComponentType());
40+
}
41+
for (JavaClass targetClass : targetClassesToRecurse) {
42+
if (!analyzedClasses.contains(targetClass)) {
43+
addTransitiveDependenciesFrom(targetClass, transitiveDependencies, analyzedClasses);
44+
}
45+
}
46+
}
47+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.tngtech.archunit.core.domain;
2+
3+
import com.tngtech.archunit.core.importer.ClassFileImporter;
4+
import org.junit.Test;
5+
6+
import static com.tngtech.archunit.testutil.Assertions.assertThatDependencies;
7+
8+
public class JavaClassTransitiveDependenciesTest {
9+
10+
@SuppressWarnings("unused")
11+
static class AcyclicGraph {
12+
static class A {
13+
B b;
14+
C[][] c;
15+
}
16+
17+
static class B {
18+
Integer i;
19+
}
20+
21+
static class C {
22+
D d;
23+
}
24+
25+
static class D {
26+
String s;
27+
}
28+
}
29+
30+
@Test
31+
public void findsTransitiveDependenciesInAcyclicGraph() {
32+
Class<?> a = AcyclicGraph.A.class;
33+
Class<?> b = AcyclicGraph.B.class;
34+
Class<?> c = AcyclicGraph.C.class;
35+
Class<?> d = AcyclicGraph.D.class;
36+
JavaClasses classes = new ClassFileImporter().importClasses(a, b, c, d);
37+
Class<?> cArray = AcyclicGraph.C[][].class;
38+
39+
// @formatter:off
40+
assertThatDependencies(classes.get(a).getTransitiveDependenciesFromSelf())
41+
.contain(a, Object.class)
42+
.contain(a, b)
43+
.contain(b, Object.class)
44+
.contain(b, Integer.class)
45+
.contain(a, cArray)
46+
.contain(c, Object.class)
47+
.contain(c, d)
48+
.contain(d, Object.class)
49+
.contain(d, String.class);
50+
51+
assertThatDependencies(classes.get(b).getTransitiveDependenciesFromSelf())
52+
.contain(b, Object.class)
53+
.contain(b, Integer.class);
54+
55+
assertThatDependencies(classes.get(c).getTransitiveDependenciesFromSelf())
56+
.contain(c, Object.class)
57+
.contain(c, d)
58+
.contain(d, Object.class)
59+
.contain(d, String.class);
60+
// @formatter:on
61+
}
62+
63+
@SuppressWarnings("unused")
64+
static class CyclicGraph {
65+
static class A {
66+
B b;
67+
C[][] c;
68+
D d;
69+
}
70+
71+
static class B {
72+
Integer i;
73+
}
74+
75+
static class C {
76+
A a;
77+
}
78+
79+
static class D {
80+
E e;
81+
}
82+
83+
static class E {
84+
A a;
85+
String s;
86+
}
87+
}
88+
89+
@Test
90+
public void findsTransitiveDependenciesInCyclicGraph() {
91+
Class<?> a = CyclicGraph.A.class;
92+
Class<?> b = CyclicGraph.B.class;
93+
Class<?> c = CyclicGraph.C.class;
94+
Class<?> d = CyclicGraph.D.class;
95+
Class<?> e = CyclicGraph.E.class;
96+
JavaClasses classes = new ClassFileImporter().importClasses(a, b, c, d, e);
97+
Class<?> cArray = CyclicGraph.C[][].class;
98+
99+
// @formatter:off
100+
assertThatDependencies(classes.get(a).getTransitiveDependenciesFromSelf())
101+
.contain(a, Object.class)
102+
.contain(a, b)
103+
.contain(b, Object.class)
104+
.contain(b, Integer.class)
105+
.contain(a, cArray)
106+
.contain(c, Object.class)
107+
.contain(c, a)
108+
.contain(a, d)
109+
.contain(d, Object.class)
110+
.contain(d, e)
111+
.contain(e, Object.class)
112+
.contain(e, a)
113+
.contain(e, String.class);
114+
115+
assertThatDependencies(classes.get(c).getTransitiveDependenciesFromSelf())
116+
.contain(c, Object.class)
117+
.contain(c, a)
118+
.contain(a, Object.class)
119+
.contain(a, b)
120+
.contain(b, Object.class)
121+
.contain(b, Integer.class)
122+
.contain(a, cArray)
123+
.contain(a, d)
124+
.contain(d, Object.class)
125+
.contain(d, e)
126+
.contain(e, Object.class)
127+
.contain(e, a)
128+
.contain(e, String.class);
129+
130+
assertThatDependencies(classes.get(d).getTransitiveDependenciesFromSelf())
131+
.contain(d, Object.class)
132+
.contain(d, e)
133+
.contain(e, Object.class)
134+
.contain(e, a)
135+
.contain(a, Object.class)
136+
.contain(a, b)
137+
.contain(b, Object.class)
138+
.contain(b, Integer.class)
139+
.contain(a, cArray)
140+
.contain(c, Object.class)
141+
.contain(c, a)
142+
.contain(a, d)
143+
.contain(e, String.class);
144+
// @formatter:on
145+
}
146+
}

archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@
205205
import static com.tngtech.archunit.testutil.ReflectionTestUtils.field;
206206
import static com.tngtech.archunit.testutil.ReflectionTestUtils.method;
207207
import static com.tngtech.archunit.testutil.TestUtils.namesOf;
208+
import static com.tngtech.java.junit.dataprovider.DataProviders.$;
209+
import static com.tngtech.java.junit.dataprovider.DataProviders.$$;
208210
import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach;
209211
import static java.nio.charset.StandardCharsets.UTF_8;
210212
import static org.junit.Assume.assumeTrue;
@@ -242,6 +244,7 @@ public void imports_simple_class_details() throws Exception {
242244
ImportedClasses classes = classesIn("testexamples/simpleimport");
243245
JavaClass javaClass = classes.get(ClassToImportOne.class);
244246

247+
assertThat(javaClass.isFullyImported()).isTrue();
245248
assertThat(javaClass.getName()).as("full name").isEqualTo(ClassToImportOne.class.getName());
246249
assertThat(javaClass.getSimpleName()).as("simple name").isEqualTo(ClassToImportOne.class.getSimpleName());
247250
assertThat(javaClass.getPackageName()).as("package name").isEqualTo(ClassToImportOne.class.getPackage().getName());
@@ -1711,6 +1714,49 @@ public void resolve_missing_dependencies_from_classpath_can_be_toogled() throws
17111714
assertThat(clazz.getSuperClass().get().getMethods()).isEmpty();
17121715
}
17131716

1717+
@DataProvider
1718+
public static Object[][] classes_not_fully_imported() {
1719+
class Element {
1720+
}
1721+
@SuppressWarnings("unused")
1722+
class DependsOnArray {
1723+
Element[] array;
1724+
}
1725+
ArchConfiguration.get().setResolveMissingDependenciesFromClassPath(true);
1726+
JavaClass resolvedFromClasspath = new ClassFileImporter().importClasses(DependsOnArray.class)
1727+
.get(DependsOnArray.class).getField("array").getRawType().getComponentType();
1728+
1729+
ArchConfiguration.get().setResolveMissingDependenciesFromClassPath(false);
1730+
JavaClass stub = new ClassFileImporter().importClasses(DependsOnArray.class)
1731+
.get(DependsOnArray.class).getField("array").getRawType().getComponentType();
1732+
1733+
return $$(
1734+
$("Resolved from classpath", resolvedFromClasspath),
1735+
$("Stub class", stub)
1736+
);
1737+
}
1738+
1739+
@Test
1740+
@UseDataProvider("classes_not_fully_imported")
1741+
public void classes_not_fully_imported_have_flag_fullyImported_false_and_empty_dependencies(@SuppressWarnings("unused") String description, JavaClass notFullyImported) {
1742+
assertThat(notFullyImported.isFullyImported()).isFalse();
1743+
assertThat(notFullyImported.getDirectDependenciesFromSelf()).isEmpty();
1744+
assertThat(notFullyImported.getDirectDependenciesToSelf()).isEmpty();
1745+
assertThat(notFullyImported.getFieldAccessesToSelf()).isEmpty();
1746+
assertThat(notFullyImported.getMethodCallsToSelf()).isEmpty();
1747+
assertThat(notFullyImported.getConstructorCallsToSelf()).isEmpty();
1748+
assertThat(notFullyImported.getAccessesToSelf()).isEmpty();
1749+
assertThat(notFullyImported.getFieldsWithTypeOfSelf()).isEmpty();
1750+
assertThat(notFullyImported.getMethodsWithParameterTypeOfSelf()).isEmpty();
1751+
assertThat(notFullyImported.getMethodsWithReturnTypeOfSelf()).isEmpty();
1752+
assertThat(notFullyImported.getMethodThrowsDeclarationsWithTypeOfSelf()).isEmpty();
1753+
assertThat(notFullyImported.getConstructorsWithParameterTypeOfSelf()).isEmpty();
1754+
assertThat(notFullyImported.getConstructorsWithThrowsDeclarationTypeOfSelf()).isEmpty();
1755+
assertThat(notFullyImported.getAnnotationsWithTypeOfSelf()).isEmpty();
1756+
assertThat(notFullyImported.getAnnotationsWithParameterTypeOfSelf()).isEmpty();
1757+
assertThat(notFullyImported.getInstanceofChecksWithTypeOfSelf()).isEmpty();
1758+
}
1759+
17141760
@Test
17151761
public void import_is_resilient_against_broken_class_files() throws Exception {
17161762
Class<?> expectedClass = getClass();

0 commit comments

Comments
 (0)