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: TNG#780
Signed-off-by: e.solutions <info@esolutions.de>
on-behalf-of: @e-esolutions-GmbH <info@esolutions.de>
  • Loading branch information
Pfoerd committed Jun 7, 2022
1 parent 6862363 commit e9e76d4
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2014-2022 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.lang.conditions;

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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;

public class AnyTransitiveDependencyCondition extends ArchCondition<JavaClass> {

private final DescribedPredicate<? super JavaClass> conditionPredicate;
private final Set<JavaClass> allClasses = new HashSet<>();

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) {
allClasses.addAll(allObjectsToTest);
}

@Override
public void check(JavaClass item, ConditionEvents events) {
getDirectDependencyTargets(item)
.filter(not(allClasses::contains))
.map(this::getDependencyPathToMatchingClasses)
.filter(Objects::nonNull)
.map(dependencyPath -> createTransitivePathFoundEvent(item, dependencyPath))
.forEach(events::add);
}

/**
* @return the dependency path to a matching class including the source class or null if there is none
*/
private LinkedList<JavaClass> getDependencyPathToMatchingClasses(JavaClass clazz) {
LinkedList<JavaClass> transitivePath = new LinkedList<>();
if (matchingTransitiveDependencies(clazz, new HashSet<>(), transitivePath)) {
transitivePath.add(clazz);
return transitivePath;
}
return null;
}

private boolean matchingTransitiveDependencies(
JavaClass clazz,
HashSet<JavaClass> analyzedClasses,
LinkedList<JavaClass> transitivePath
) {
if (conditionPredicate.test(clazz)) {
return true;
}

analyzedClasses.add(clazz);

Optional<JavaClass> firstMatchingTransitiveDependency = getDirectDependencyTargets(clazz)
.filter(not(allClasses::contains))
.filter(not(analyzedClasses::contains))
.filter(it -> matchingTransitiveDependencies(it, analyzedClasses, transitivePath))
.findFirst();

firstMatchingTransitiveDependency.ifPresent(transitivePath::add);
return firstMatchingTransitiveDependency.isPresent();
}

private static Stream<JavaClass> getDirectDependencyTargets(JavaClass item) {
return item.getDirectDependenciesFromSelf().stream().map(Dependency::getTargetClass).map(JavaClass::getBaseComponentType).distinct();
}

private static ConditionEvent createTransitivePathFoundEvent(JavaClass clazz, LinkedList<JavaClass> dependencyPath) {
StringBuilder messageBuilder =
new StringBuilder("Class <" + clazz.getFullName() + "> accesses <" + dependencyPath.getLast().getFullName() + ">");
if (dependencyPath.size() > 1) {
messageBuilder
.append(" which transitively accesses e.g. ")
.append(dependencyPath.subList(0, dependencyPath.size()).stream()
.map(it -> String.format("<%s>", it.getFullName()))
.collect(Collectors.joining(" <- ")));

}
return SimpleConditionEvent.satisfied(clazz, messageBuilder.toString());
}

@SuppressWarnings("unchecked")
private static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>) target.negate();
}
}
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,21 @@ public interface ClassesShould {
@PublicAPI(usage = ACCESS)
ClassesShouldConjunction onlyDependOnClassesThat(DescribedPredicate<? super JavaClass> predicate);

/**
* TODO
* @return
*/
@PublicAPI(usage = ACCESS)
ClassesThat<ClassesShouldConjunction> transitivelyDependOnAnyClassesThat();

/**
* TODO
* @param predicate
* @return
*/
@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 @@ -31,12 +31,12 @@
import static com.tngtech.archunit.base.DescribedPredicate.equalTo;
import static com.tngtech.archunit.base.DescribedPredicate.not;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableFrom;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleNameStartingWith;
import static com.tngtech.archunit.core.domain.JavaModifier.PRIVATE;
import static com.tngtech.archunit.core.domain.properties.HasName.AndFullName.Predicates.fullNameMatching;
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 @@ -393,7 +393,8 @@ public void containAnyMembersThat(ClassesThat<ClassesShouldConjunction> noClasse
.on(Data_of_containAnyMembersThat.OkayOrigin.class, Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.OkayTarget.class, Data_of_containAnyMembersThat.ViolatingTarget.class);

assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class, Data_of_containAnyMembersThat.ViolatingTarget.class);
assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.ViolatingTarget.class);
}

@Test
Expand All @@ -403,7 +404,8 @@ public void containAnyFieldsThat(ClassesThat<ClassesShouldConjunction> noClasses
.on(Data_of_containAnyMembersThat.OkayOrigin.class, Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.OkayTarget.class, Data_of_containAnyMembersThat.ViolatingTarget.class);

assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class, Data_of_containAnyMembersThat.ViolatingTarget.class);
assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.ViolatingTarget.class);
}

@Test
Expand All @@ -413,7 +415,8 @@ public void containAnyCodeUnitsThat(ClassesThat<ClassesShouldConjunction> noClas
.on(Data_of_containAnyMembersThat.OkayOrigin.class, Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.OkayTarget.class, Data_of_containAnyMembersThat.ViolatingTarget.class);

assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class, Data_of_containAnyMembersThat.ViolatingTarget.class);
assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.ViolatingTarget.class);
}

@Test
Expand All @@ -423,7 +426,8 @@ public void containAnyMethodsThat(ClassesThat<ClassesShouldConjunction> noClasse
.on(Data_of_containAnyMembersThat.OkayOrigin.class, Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.OkayTarget.class, Data_of_containAnyMembersThat.ViolatingTarget.class);

assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class, Data_of_containAnyMembersThat.ViolatingTarget.class);
assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.ViolatingTarget.class);
}

@Test
Expand All @@ -434,7 +438,8 @@ public void containAnyConstructorsThat(ClassesThat<ClassesShouldConjunction> noC
.on(Data_of_containAnyMembersThat.OkayOrigin.class, Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.OkayTarget.class, Data_of_containAnyMembersThat.ViolatingTarget.class);

assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class, Data_of_containAnyMembersThat.ViolatingTarget.class);
assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.ViolatingTarget.class);
}

@Test
Expand All @@ -446,7 +451,8 @@ public void containAnyStaticInitializersThat(ClassesThat<ClassesShouldConjunctio
.on(Data_of_containAnyMembersThat.OkayOrigin.class, Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.OkayTarget.class, Data_of_containAnyMembersThat.ViolatingTarget.class);

assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class, Data_of_containAnyMembersThat.ViolatingTarget.class);
assertThatTypes(classes).matchInAnyOrder(Data_of_containAnyMembersThat.ViolatingOrigin.class,
Data_of_containAnyMembersThat.ViolatingTarget.class);
}

@Test
Expand Down Expand Up @@ -1708,49 +1714,98 @@ public void onlyDependOnClassesThat_reports_all_dependencies() {

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

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

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

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

static class TransitivelyDependentClass {
static class TransitivelyDependentClass1 {
}

@SuppressWarnings("unused")
static class TransitivelyDependentClass2 {
TransitivelyDependentClass1 transitiveDependency1;
}
}

@Test
@DataProvider(value = {"true", "false"})
public void transitivelyDependOnClassesThat_reports_all_transitive_dependencies(boolean viaPredicate) {
Class<?> testClass = TransitivelyDependOnClassesThatTestCases.TestClass.class;
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<?> transitivelyDependentClass = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass.class;
Class<?> transitivelyDependentClass1 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass1.class;
Class<?> transitivelyDependentClass2 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass2.class;
JavaClasses classes = new ClassFileImporter().importClasses(
testClass, directlyDependentClass1, directlyDependentClass2, transitivelyDependentClass
testClass1, testClass2, directlyDependentClass1, directlyDependentClass2, transitivelyDependentClass1, transitivelyDependentClass2
);

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.transitivelyDependOnAnyClassesThat(have(simpleNameStartingWith("TransitivelyDependentClass")))
: noClassesShould.transitivelyDependOnAnyClassesThat().haveSimpleNameStartingWith("TransitivelyDependentClass");

assertThatRule(rule).checking(classes)
.hasViolations(2)
.hasViolationMatching(String.format(".*<%s>.* accesses .*<%s>.* transitively accesses .*<(?:%s|%s)> <- .*",
quote(testClass1.getName()),
quote(directlyDependentClass2.getName()),
quote(transitivelyDependentClass1.getName()),
quote(transitivelyDependentClass2.getName())
))
.hasViolationMatching(String.format(".*<%s>.* accesses .*<%s>.* transitively accesses .*<%s> <- <%s>.*",
quote(testClass1.getName()),
quote(directlyDependentClass1.getName()),
quote(transitivelyDependentClass1.getName()),
quote(directlyDependentClass1.getName())
));
}

@Test
@DataProvider(value = {"true", "false"})
public void transitivelyDependOnClassesThat_reports_all_transitive_dependencies(boolean viaPredicate) {
Class<?> testClass1 = TransitivelyDependOnClassesThatTestCases.TestClass1.class;
Class<?> testClass2 = TransitivelyDependOnClassesThatTestCases.TestClass2.class;
Class<?> directlyDependentClass1 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass1.class;
Class<?> directlyDependentClass2 = TransitivelyDependOnClassesThatTestCases.DirectlyDependentClass2.class;
Class<?> transitivelyDependentClass1 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass1.class;
Class<?> transitivelyDependentClass2 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass2.class;
JavaClasses classes = new ClassFileImporter().importClasses(
testClass1, testClass2, directlyDependentClass1, directlyDependentClass2, transitivelyDependentClass1, transitivelyDependentClass2
);

ClassesShould noClassesShould = noClasses().that().haveSimpleNameStartingWith("TestClass").should();
ArchRule rule = viaPredicate
? noClassesShould.transitivelyDependOnClassesThat(have(simpleNameStartingWith("TransitivelyDependentClass")))
: noClassesShould.transitivelyDependOnClassesThat().haveSimpleNameStartingWith("TransitivelyDependentClass");

assertThatRule(rule).checking(classes)
.hasViolations(6)
.hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*",
quote(directlyDependentClass1.getName()), "transitiveDependency1", quote(transitivelyDependentClass1.getName())
))
.hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*",
quote(directlyDependentClass1.getName()), "transitiveDependency1", quote(transitivelyDependentClass.getName())
quote(directlyDependentClass2.getName()), "transitiveDependency2", quote(transitivelyDependentClass2.getName())
))
.hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*",
quote(directlyDependentClass2.getName()), "transitiveDependency2", quote(transitivelyDependentClass.getName())
quote(transitivelyDependentClass2.getName()), "transitiveDependency1", quote(transitivelyDependentClass1.getName())
));
}

Expand Down

0 comments on commit e9e76d4

Please sign in to comment.