Skip to content

Commit

Permalink
add method JavaClass.getTransitiveDependenciesFromSelf #401
Browse files Browse the repository at this point in the history
Signed-off-by: Manfred Hanke <[email protected]>
  • Loading branch information
codecholeric authored Dec 16, 2020
2 parents 44698c9 + 27dae94 commit fd57c1f
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ public Set<JavaMember> get() {
.build();
}
});
private JavaClassDependencies javaClassDependencies;
private ReverseDependencies reverseDependencies;
private JavaClassDependencies javaClassDependencies = new JavaClassDependencies(this); // just for stubs; will be overwritten for imported classes
private ReverseDependencies reverseDependencies = ReverseDependencies.EMPTY; // just for stubs; will be overwritten for imported classes
private boolean fullyImported = false;

JavaClass(JavaClassBuilder builder) {
source = checkNotNull(builder.getSource());
Expand Down Expand Up @@ -1030,6 +1031,17 @@ public Set<Dependency> 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<Dependency> getTransitiveDependenciesFromSelf() {
return JavaClassTransitiveDependencies.findTransitiveDependenciesFrom(this);
}

/**
* Like {@link #getDirectDependenciesFromSelf()}, but instead returns all dependencies where this class
* is target.
Expand Down Expand Up @@ -1149,6 +1161,17 @@ public Set<InstanceofCheck> getInstanceofChecksWithTypeOfSelf() {
return reverseDependencies.getInstanceofChecksWithTypeOf(this);
}

/**
* @return Whether this class has been fully imported, including all dependencies.<br>
* Classes that are only transitively imported are not necessarily fully imported.<br><br>
* Suppose you only import a class {@code Foo} that calls a method of class {@code Bar}.
* Then {@code Bar} is, as a dependency of the fully imported class {@code Foo}, only transitively imported.
*/
@PublicAPI(usage = ACCESS)
public boolean isFullyImported() {
return fullyImported;
}

/**
* @param clazz An arbitrary type
* @return true, if this {@link JavaClass} represents the same class as the supplied {@link Class}, otherwise false
Expand Down Expand Up @@ -1301,6 +1324,7 @@ JavaClassDependencies completeFrom(ImportContext context) {
codeUnit.completeFrom(context);
}
javaClassDependencies = new JavaClassDependencies(this);
fullyImported = true;
return javaClassDependencies;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Dependency> findTransitiveDependenciesFrom(JavaClass javaClass) {
ImmutableSet.Builder<Dependency> transitiveDependencies = ImmutableSet.builder();
Set<JavaClass> 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<Dependency> transitiveDependencies, Set<JavaClass> analyzedClasses) {
analyzedClasses.add(javaClass); // currently being analyzed
Set<JavaClass> 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@
import static com.tngtech.archunit.testutil.ReflectionTestUtils.field;
import static com.tngtech.archunit.testutil.ReflectionTestUtils.method;
import static com.tngtech.archunit.testutil.TestUtils.namesOf;
import static com.tngtech.java.junit.dataprovider.DataProviders.$;
import static com.tngtech.java.junit.dataprovider.DataProviders.$$;
import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assume.assumeTrue;
Expand Down Expand Up @@ -242,6 +244,7 @@ public void imports_simple_class_details() throws Exception {
ImportedClasses classes = classesIn("testexamples/simpleimport");
JavaClass javaClass = classes.get(ClassToImportOne.class);

assertThat(javaClass.isFullyImported()).isTrue();
assertThat(javaClass.getName()).as("full name").isEqualTo(ClassToImportOne.class.getName());
assertThat(javaClass.getSimpleName()).as("simple name").isEqualTo(ClassToImportOne.class.getSimpleName());
assertThat(javaClass.getPackageName()).as("package name").isEqualTo(ClassToImportOne.class.getPackage().getName());
Expand Down Expand Up @@ -1711,6 +1714,49 @@ public void resolve_missing_dependencies_from_classpath_can_be_toogled() throws
assertThat(clazz.getSuperClass().get().getMethods()).isEmpty();
}

@DataProvider
public static Object[][] classes_not_fully_imported() {
class Element {
}
@SuppressWarnings("unused")
class DependsOnArray {
Element[] array;
}
ArchConfiguration.get().setResolveMissingDependenciesFromClassPath(true);
JavaClass resolvedFromClasspath = new ClassFileImporter().importClasses(DependsOnArray.class)
.get(DependsOnArray.class).getField("array").getRawType().getComponentType();

ArchConfiguration.get().setResolveMissingDependenciesFromClassPath(false);
JavaClass stub = new ClassFileImporter().importClasses(DependsOnArray.class)
.get(DependsOnArray.class).getField("array").getRawType().getComponentType();

return $$(
$("Resolved from classpath", resolvedFromClasspath),
$("Stub class", stub)
);
}

@Test
@UseDataProvider("classes_not_fully_imported")
public void classes_not_fully_imported_have_flag_fullyImported_false_and_empty_dependencies(@SuppressWarnings("unused") String description, JavaClass notFullyImported) {
assertThat(notFullyImported.isFullyImported()).isFalse();
assertThat(notFullyImported.getDirectDependenciesFromSelf()).isEmpty();
assertThat(notFullyImported.getDirectDependenciesToSelf()).isEmpty();
assertThat(notFullyImported.getFieldAccessesToSelf()).isEmpty();
assertThat(notFullyImported.getMethodCallsToSelf()).isEmpty();
assertThat(notFullyImported.getConstructorCallsToSelf()).isEmpty();
assertThat(notFullyImported.getAccessesToSelf()).isEmpty();
assertThat(notFullyImported.getFieldsWithTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getMethodsWithParameterTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getMethodsWithReturnTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getMethodThrowsDeclarationsWithTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getConstructorsWithParameterTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getConstructorsWithThrowsDeclarationTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getAnnotationsWithTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getAnnotationsWithParameterTypeOfSelf()).isEmpty();
assertThat(notFullyImported.getInstanceofChecksWithTypeOfSelf()).isEmpty();
}

@Test
public void import_is_resilient_against_broken_class_files() throws Exception {
Class<?> expectedClass = getClass();
Expand Down

0 comments on commit fd57c1f

Please sign in to comment.