Skip to content

Commit

Permalink
add arch condition to check for any transitive dependency
Browse files Browse the repository at this point in the history
Issue: #780
Signed-off-by: e.solutions <17569373+Pfoerd@users.noreply.github.com>
on-behalf-of: @e-esolutions-GmbH <info@esolutions.de>
  • Loading branch information
Pfoerd committed Jun 23, 2022
1 parent 347dc45 commit 77831df
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.tngtech.archunit.lang.conditions;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.common.collect.ImmutableList;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvent;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getLast;
import static java.util.stream.Collectors.joining;

public class AnyTransitiveDependencyCondition extends ArchCondition<JavaClass> {

private final DescribedPredicate<? super JavaClass> conditionPredicate;
private final TransitiveDependencyPath transitiveDependencyPath = new TransitiveDependencyPath();
private Collection<JavaClass> allClasses;

public AnyTransitiveDependencyCondition(DescribedPredicate<? super JavaClass> conditionPredicate) {
super("transitively depend on any classes that " + conditionPredicate.getDescription());

this.conditionPredicate = checkNotNull(conditionPredicate);
}

@Override
public void init(Collection<JavaClass> allObjectsToTest) {
this.allClasses = allObjectsToTest;
}

@Override
public void check(JavaClass javaClass, ConditionEvents events) {
boolean hasTransitiveDependency = false;
for (JavaClass dependency : getDirectDependencies(javaClass)) {
if (!allClasses.contains(dependency)) {
List<JavaClass> dependencyPath = transitiveDependencyPath.findFirstPathToTransitiveDependency(dependency);
if (!dependencyPath.isEmpty()) {
events.add(newTransitivePathFoundEvent(javaClass, dependencyPath));
hasTransitiveDependency = true;
}
}
}
if (!hasTransitiveDependency) {
events.add(noTransitivePathFoundEvent(javaClass));
}
}

private static ConditionEvent newTransitivePathFoundEvent(JavaClass selected, List<JavaClass> transitivePath) {
String message =
String.format("Class <%s> transitively depends on <%s>", selected.getFullName(), getLast(transitivePath).getFullName());

if (transitivePath.size() > 1) {
message += " by [" + transitivePath.stream().map(JavaClass::getName).collect(joining("->")) + "]";
}
message += " in " + selected.getSourceCodeLocation();
return SimpleConditionEvent.satisfied(selected, message);
}

private static ConditionEvent noTransitivePathFoundEvent(JavaClass selected) {
return SimpleConditionEvent.violated(selected,
String.format("Class <%s> not transitively depends on any matching class", selected.getFullName()));
}

private static Set<JavaClass> getDirectDependencies(JavaClass item) {
Set<JavaClass> directDependencies = new HashSet<>();
for (Dependency dependency : item.getDirectDependenciesFromSelf()) {
directDependencies.add(dependency.getTargetClass().getBaseComponentType());
}
return directDependencies;
}

private class TransitiveDependencyPath {
/**
* @return the first dependency path to a matching class or empty if there is none
*/
List<JavaClass> findFirstPathToTransitiveDependency(JavaClass clazz) {
ImmutableList.Builder<JavaClass> transitivePath = ImmutableList.builder();
addDependenciesToPathFrom(clazz, transitivePath, new HashSet<>(allClasses));
return transitivePath.build().reverse();
}

private boolean addDependenciesToPathFrom(
JavaClass clazz,
ImmutableList.Builder<JavaClass> dependencyPath,
Set<JavaClass> analyzedClasses
) {
if (conditionPredicate.test(clazz)) {
dependencyPath.add(clazz);
return true;
}

analyzedClasses.add(clazz);

for (JavaClass directDependency : getDirectDependencies(clazz)) {
if (!analyzedClasses.contains(directDependency)
&& addDependenciesToPathFrom(directDependency, dependencyPath, analyzedClasses)) {
dependencyPath.add(clazz);
return true;
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ public static ArchCondition<JavaClass> transitivelyDependOnClassesThat(final Des
GET_TRANSITIVE_DEPENDENCIES_FROM_SELF);
}

@PublicAPI(usage = ACCESS)
public static ArchCondition<JavaClass> transitivelyDependOnAnyClassesThat(final DescribedPredicate<? super JavaClass> predicate) {
return new AnyTransitiveDependencyCondition(predicate);
}

@PublicAPI(usage = ACCESS)
public static ArchCondition<JavaClass> onlyDependOnClassesThat(final DescribedPredicate<? super JavaClass> predicate) {
return new AllDependenciesCondition(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,16 @@ public ClassesShouldConjunction onlyDependOnClassesThat(DescribedPredicate<? sup
return addCondition(ArchConditions.onlyDependOnClassesThat(predicate));
}

@Override
public ClassesThat<ClassesShouldConjunction> transitivelyDependOnAnyClassesThat() {
return new ClassesThatInternal<>(predicate -> addCondition(ArchConditions.transitivelyDependOnAnyClassesThat(predicate)));
}

@Override
public ClassesShouldConjunction transitivelyDependOnAnyClassesThat(DescribedPredicate<? super JavaClass> predicate) {
return addCondition(ArchConditions.transitivelyDependOnAnyClassesThat(predicate));
}

@Override
public ClassesThat<ClassesShouldConjunction> transitivelyDependOnClassesThat() {
return new ClassesThatInternal<>(predicate -> addCondition(ArchConditions.transitivelyDependOnClassesThat(predicate)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,48 @@ public interface ClassesShould {
@PublicAPI(usage = ACCESS)
ClassesShouldConjunction onlyDependOnClassesThat(DescribedPredicate<? super JavaClass> predicate);

/**
* Asserts that all classes selected by this rule transitively depend on any matching classes.<br>
* This is a much more efficient variant of {@link #transitivelyDependOnClassesThat()} that can be used to detect transitive dependencies in a
* huge codebase or to classes in large 3rd-party libraries like the Android SDK.
* It focuses on detecting all <strong>direct</strong> dependencies of the selected classes that are themselves matched or have any
* transitive dependencies on matched classes. Thus, it doesn't discover all possible dependency paths but stops at the first match to be fast and
* resource-friendly.<br>
* NOTE: This usually makes more sense the negated way, e.g.
* <p>
* <pre><code>
* {@link ArchRuleDefinition#noClasses() noClasses()}.{@link GivenClasses#should() should()}.{@link #transitivelyDependOnAnyClassesThat()}.{@link ClassesThat#haveFullyQualifiedName(String) haveFullyQualifiedName(String)}
* </code></pre>
*
* NOTE: 'dependOn' catches wide variety of violations, e.g. having fields of type, having method parameters of type, extending type etc...
*
* @return A syntax element that allows choosing to which classes a transitive dependency should exist
*/
@PublicAPI(usage = ACCESS)
ClassesThat<ClassesShouldConjunction> transitivelyDependOnAnyClassesThat();


/**
* Asserts that all classes selected by this rule transitively depend on any matching classes.<br>
* This is a much more efficient variant of {@link #transitivelyDependOnClassesThat()} that can be used to detect transitive dependencies in a
* huge codebase or to classes in large 3rd-party libraries like the Android SDK.
* It focuses on detecting all <strong>direct</strong> dependencies of the selected classes that are themselves matched or have any
* transitive dependencies on matched classes. Thus, it doesn't discover all possible dependency paths but stops at the first match to be fast and
* resource-friendly.<br>
* NOTE: This usually makes more sense the negated way, e.g.
* <p>
* <pre><code>
* {@link ArchRuleDefinition#noClasses() noClasses()}.{@link GivenClasses#should() should()}.{@link #transitivelyDependOnAnyClassesThat(DescribedPredicate) transitivelyDependOnAnyClassesThat(myPredicate)}
* </code></pre>
*
* NOTE: 'dependOn' catches wide variety of violations, e.g. having fields of type, having method parameters of type, extending type etc...
*
* @param predicate Determines which {@link JavaClass JavaClasses} match the dependency target
* @return A syntax element that can either be used as working rule, or to continue specifying a more complex rule
*/
@PublicAPI(usage = ACCESS)
ClassesShouldConjunction transitivelyDependOnAnyClassesThat(DescribedPredicate<? super JavaClass> predicate);

/**
* Asserts that all classes selected by this rule transitively depend on certain classes.<br>
* NOTE: This usually makes more sense the negated way, e.g.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.properties.HasName;
import com.tngtech.archunit.core.domain.properties.HasType;
Expand All @@ -36,7 +37,6 @@
import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAME;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE;
import static com.tngtech.archunit.lang.conditions.ArchConditions.fullyQualifiedName;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.are;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.have;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
Expand Down Expand Up @@ -1708,49 +1708,139 @@ public void onlyDependOnClassesThat_reports_all_dependencies() {

private static class TransitivelyDependOnClassesThatTestCases {
@SuppressWarnings("unused")
static class TestClass {
static class TestClass1 {
DirectlyDependentClass1 directDependency1;
DirectlyDependentClass2 directDependency2;
DirectlyDependentClass3 directDependency3;
}

@SuppressWarnings("unused")
static class TestClass2 {
TestClass1 testClass1;
}

@SuppressWarnings("unused")
static class DirectlyDependentClass1 {
TransitivelyDependentClass transitiveDependency1;
Level1TransitivelyDependentClass1 transitiveDependency1;
}

@SuppressWarnings("unused")
static class DirectlyDependentClass2{
static class DirectlyDependentClass2 {
DirectlyDependentClass1 otherDependency;
TransitivelyDependentClass transitiveDependency2;
Level2TransitivelyDependentClass2 transitiveDependency2;
}

static class DirectlyDependentClass3 {
}

@SuppressWarnings("unused")
static class Level1TransitivelyDependentClass1 {
Level2TransitivelyDependentClass1 transitiveDependency1;
}

static class TransitivelyDependentClass {
static class Level2TransitivelyDependentClass1 {
}

@SuppressWarnings("unused")
static class Level2TransitivelyDependentClass2 {
Level2TransitivelyDependentClass1 transitiveDependency1;
}
}

@Test
@DataProvider(value = {"true", "false"})
public void transitivelyDependOnAnyClassesThat_reports_all_direct_dependencies_with_any_transitive_dependency(boolean viaPredicate) {
Class<?> testClass1 = TransitivelyDependOnClassesThatTestCases.TestClass1.class;
Class<?> testClass2 = TransitivelyDependOnClassesThatTestCases.TestClass2.class;
Class<?> directlyDependentClass1 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass1.class;
Class<?> directlyDependentClass2 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass2.class;
Class<?> directlyDependentClass3 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass3.class;
Class<?> level1TransitivelyDependentClass1 = TransitivelyDependOnClassesThatTestCases.Level1TransitivelyDependentClass1.class;
Class<?> level2TransitivelyDependentClass1 = TransitivelyDependOnClassesThatTestCases.Level2TransitivelyDependentClass1.class;
Class<?> level2TransitivelyDependentClass2 = TransitivelyDependOnClassesThatTestCases.Level2TransitivelyDependentClass2.class;
Class<?>[] matchingTransitivelyDependentClasses =
new Class<?>[]{level2TransitivelyDependentClass1, level2TransitivelyDependentClass2, directlyDependentClass3};

JavaClasses classes = new ClassFileImporter().importClasses(
testClass1,
testClass2,
directlyDependentClass1,
directlyDependentClass2,
directlyDependentClass3,
level1TransitivelyDependentClass1,
level2TransitivelyDependentClass1,
level2TransitivelyDependentClass2
);

ClassesShould noClassesShould = noClasses().that().haveSimpleNameStartingWith("TestClass").should();
ArchRule rule = viaPredicate
? noClassesShould.transitivelyDependOnAnyClassesThat(Predicates.belongToAnyOf(matchingTransitivelyDependentClasses))
: noClassesShould.transitivelyDependOnAnyClassesThat().belongToAnyOf(matchingTransitivelyDependentClasses);

assertThatRule(rule).checking(classes)
.hasViolations(3)
.hasViolationMatching(String.format(".*<%s> transitively depends on <(?:%s|%s)> by \\[%s->.*\\] in .*",
quote(testClass1.getName()),
quote(level2TransitivelyDependentClass1.getName()),
quote(level2TransitivelyDependentClass2.getName()),
quote(directlyDependentClass2.getName())
))
.hasViolationMatching(String.format(".*<%s> transitively depends on <%s> by \\[%s->%s->%s\\] in .*",
quote(testClass1.getName()),
quote(level2TransitivelyDependentClass1.getName()),
quote(directlyDependentClass1.getName()),
quote(level1TransitivelyDependentClass1.getName()),
quote(level2TransitivelyDependentClass1.getName())
))
.hasViolationMatching(String.format(".*<%s> transitively depends on <%s> in .*",
quote(testClass1.getName()),
quote(directlyDependentClass3.getName())
));
}

@Test
@DataProvider(value = {"true", "false"})
public void transitivelyDependOnClassesThat_reports_all_transitive_dependencies(boolean viaPredicate) {
Class<?> testClass = TransitivelyDependOnClassesThatTestCases.TestClass.class;
Class<?> testClass1 = TransitivelyDependOnClassesThatTestCases.TestClass1.class;
Class<?> testClass2 = TransitivelyDependOnClassesThatTestCases.TestClass2.class;
Class<?> directlyDependentClass1 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass1.class;
Class<?> directlyDependentClass2 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass2.class;
Class<?> transitivelyDependentClass = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass.class;
Class<?> directlyDependentClass3 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass3.class;
Class<?> level1TransitivelyDependentClass1 = TransitivelyDependOnClassesThatTestCases.Level1TransitivelyDependentClass1.class;
Class<?> level2TransitivelyDependentClass1 = TransitivelyDependOnClassesThatTestCases.Level2TransitivelyDependentClass1.class;
Class<?> level2TransitivelyDependentClass2 = TransitivelyDependOnClassesThatTestCases.Level2TransitivelyDependentClass2.class;
Class<?>[] matchingTransitivelyDependentClasses =
new Class<?>[]{level2TransitivelyDependentClass1, level2TransitivelyDependentClass2, directlyDependentClass3};

JavaClasses classes = new ClassFileImporter().importClasses(
testClass, directlyDependentClass1, directlyDependentClass2, transitivelyDependentClass
testClass1,
testClass2,
directlyDependentClass1,
directlyDependentClass2,
directlyDependentClass3,
level1TransitivelyDependentClass1,
level2TransitivelyDependentClass1,
level2TransitivelyDependentClass2
);

ClassesShould noClassesShould = noClasses().that().haveFullyQualifiedName(testClass.getName()).should();
ClassesShould noClassesShould = noClasses().that().haveSimpleNameStartingWith("TestClass").should();
ArchRule rule = viaPredicate
? noClassesShould.transitivelyDependOnClassesThat(have(fullyQualifiedName(transitivelyDependentClass.getName())))
: noClassesShould.transitivelyDependOnClassesThat().haveFullyQualifiedName(transitivelyDependentClass.getName());
? noClassesShould.transitivelyDependOnClassesThat(Predicates.belongToAnyOf(matchingTransitivelyDependentClasses))
: noClassesShould.transitivelyDependOnClassesThat().belongToAnyOf(matchingTransitivelyDependentClasses);

assertThatRule(rule).checking(classes)
.hasViolations(2)
.hasViolations(8)
.hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*",
quote(level1TransitivelyDependentClass1.getName()), "transitiveDependency1", quote(level2TransitivelyDependentClass1.getName())
))
.hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*",
quote(directlyDependentClass2.getName()), "transitiveDependency2", quote(level2TransitivelyDependentClass2.getName())
))
.hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*",
quote(directlyDependentClass1.getName()), "transitiveDependency1", quote(transitivelyDependentClass.getName())
quote(level2TransitivelyDependentClass2.getName()), "transitiveDependency1", quote(level2TransitivelyDependentClass1.getName())
))
.hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*",
quote(directlyDependentClass2.getName()), "transitiveDependency2", quote(transitivelyDependentClass.getName())
quote(testClass1.getName()), "directDependency3", quote(directlyDependentClass3.getName())
));
}

Expand Down

0 comments on commit 77831df

Please sign in to comment.