Skip to content

add method JavaClass.getTransitiveDependenciesFromSelf #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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