diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/AnyTransitiveDependencyCondition.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/AnyTransitiveDependencyCondition.java new file mode 100644 index 0000000000..7a4547c9f9 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/AnyTransitiveDependencyCondition.java @@ -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 { + + private final DescribedPredicate conditionPredicate; + private final Set allClasses = new HashSet<>(); + + public AnyTransitiveDependencyCondition(DescribedPredicate conditionPredicate) { + super("transitively depend on any classes that " + conditionPredicate.getDescription()); + + this.conditionPredicate = checkNotNull(conditionPredicate); + } + + @Override + public void init(Collection 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 getDependencyPathToMatchingClasses(JavaClass clazz) { + LinkedList transitivePath = new LinkedList<>(); + if (matchingTransitiveDependencies(clazz, new HashSet<>(), transitivePath)) { + transitivePath.add(clazz); + return transitivePath; + } + return null; + } + + private boolean matchingTransitiveDependencies( + JavaClass clazz, + HashSet analyzedClasses, + LinkedList transitivePath + ) { + if (conditionPredicate.test(clazz)) { + return true; + } + + analyzedClasses.add(clazz); + + Optional 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 getDirectDependencyTargets(JavaClass item) { + return item.getDirectDependenciesFromSelf().stream().map(Dependency::getTargetClass).map(JavaClass::getBaseComponentType).distinct(); + } + + private static ConditionEvent createTransitivePathFoundEvent(JavaClass clazz, LinkedList 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 Predicate not(Predicate target) { + Objects.requireNonNull(target); + return (Predicate) target.negate(); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java index 1bebc7e9d0..9c7a12925e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java @@ -309,6 +309,11 @@ public static ArchCondition transitivelyDependOnClassesThat(final Des GET_TRANSITIVE_DEPENDENCIES_FROM_SELF); } + @PublicAPI(usage = ACCESS) + public static ArchCondition transitivelyDependOnAnyClassesThat(final DescribedPredicate predicate) { + return new AnyTransitiveDependencyCondition(predicate); + } + @PublicAPI(usage = ACCESS) public static ArchCondition onlyDependOnClassesThat(final DescribedPredicate predicate) { return new AllDependenciesCondition( diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesShouldInternal.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesShouldInternal.java index d0b87f5ad3..848176ee99 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesShouldInternal.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesShouldInternal.java @@ -524,6 +524,16 @@ public ClassesShouldConjunction onlyDependOnClassesThat(DescribedPredicate transitivelyDependOnAnyClassesThat() { + return new ClassesThatInternal<>(predicate -> addCondition(ArchConditions.transitivelyDependOnAnyClassesThat(predicate))); + } + + @Override + public ClassesShouldConjunction transitivelyDependOnAnyClassesThat(DescribedPredicate predicate) { + return addCondition(ArchConditions.transitivelyDependOnAnyClassesThat(predicate)); + } + @Override public ClassesThat transitivelyDependOnClassesThat() { return new ClassesThatInternal<>(predicate -> addCondition(ArchConditions.transitivelyDependOnClassesThat(predicate))); diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java index 853699cd6c..04314a608d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java @@ -1015,6 +1015,21 @@ public interface ClassesShould { @PublicAPI(usage = ACCESS) ClassesShouldConjunction onlyDependOnClassesThat(DescribedPredicate predicate); + /** + * TODO + * @return + */ + @PublicAPI(usage = ACCESS) + ClassesThat transitivelyDependOnAnyClassesThat(); + + /** + * TODO + * @param predicate + * @return + */ + @PublicAPI(usage = ACCESS) + ClassesShouldConjunction transitivelyDependOnAnyClassesThat(DescribedPredicate predicate); + /** * Asserts that all classes selected by this rule transitively depend on certain classes.
* NOTE: This usually makes more sense the negated way, e.g. diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java index de43a0c458..d62879b6b2 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java @@ -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; @@ -393,7 +393,8 @@ public void containAnyMembersThat(ClassesThat 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 @@ -403,7 +404,8 @@ public void containAnyFieldsThat(ClassesThat 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 @@ -413,7 +415,8 @@ public void containAnyCodeUnitsThat(ClassesThat 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 @@ -423,7 +426,8 @@ public void containAnyMethodsThat(ClassesThat 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 @@ -434,7 +438,8 @@ public void containAnyConstructorsThat(ClassesThat 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 @@ -446,7 +451,8 @@ public void containAnyStaticInitializersThat(ClassesThat 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; + Class transitivelyDependentClass3 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass3.class; + JavaClasses classes = new ClassFileImporter().importClasses( + testClass1, + testClass2, + directlyDependentClass1, + directlyDependentClass2, + transitivelyDependentClass1, + transitivelyDependentClass2, + transitivelyDependentClass3 + ); + + ClassesShould noClassesShould = noClasses().that().haveSimpleNameStartingWith("TestClass").should(); + ArchRule rule = viaPredicate + ? noClassesShould.transitivelyDependOnAnyClassesThat(have(simpleNameStartingWith("TransitivelyDependentClass"))) + : noClassesShould.transitivelyDependOnAnyClassesThat().haveSimpleNameStartingWith("TransitivelyDependentClass"); + + assertThatRule(rule).checking(classes) + .hasViolations(3) + .hasViolationMatching(String.format(".*<%s>.* accesses .*<%s>.* which transitively accesses .*<(?:%s|%s)> <- .*", + quote(testClass1.getName()), + quote(directlyDependentClass2.getName()), + quote(transitivelyDependentClass1.getName()), + quote(transitivelyDependentClass2.getName()) + )) + .hasViolationMatching(String.format(".*<%s>.* accesses .*<%s>.* which transitively accesses .*<%s> <- <%s>.*", + quote(testClass1.getName()), + quote(directlyDependentClass1.getName()), + quote(transitivelyDependentClass1.getName()), + quote(directlyDependentClass1.getName()) + )) + .hasViolationMatching(String.format(".*<%s>.* accesses .*<%s>.*", + quote(testClass1.getName()), + quote(transitivelyDependentClass3.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 transitivelyDependentClass1 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass1.class; + Class transitivelyDependentClass2 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass2.class; + Class transitivelyDependentClass3 = TransitivelyDependOnClassesThatTestCases.TransitivelyDependentClass3.class; JavaClasses classes = new ClassFileImporter().importClasses( - testClass, directlyDependentClass1, directlyDependentClass2, transitivelyDependentClass + testClass1, + testClass2, + directlyDependentClass1, + directlyDependentClass2, + transitivelyDependentClass1, + transitivelyDependentClass2, + transitivelyDependentClass3 ); - 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(have(simpleNameStartingWith("TransitivelyDependentClass"))) + : noClassesShould.transitivelyDependOnClassesThat().haveSimpleNameStartingWith("TransitivelyDependentClass"); assertThatRule(rule).checking(classes) - .hasViolations(2) + .hasViolations(8) + .hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*", + quote(directlyDependentClass1.getName()), "transitiveDependency1", quote(transitivelyDependentClass1.getName()) + )) + .hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*", + quote(directlyDependentClass2.getName()), "transitiveDependency2", quote(transitivelyDependentClass2.getName()) + )) .hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*", - quote(directlyDependentClass1.getName()), "transitiveDependency1", quote(transitivelyDependentClass.getName()) + quote(transitivelyDependentClass2.getName()), "transitiveDependency1", quote(transitivelyDependentClass1.getName()) )) .hasViolationMatching(String.format(".*%s\\.%s.* has type .*%s.*", - quote(directlyDependentClass2.getName()), "transitiveDependency2", quote(transitivelyDependentClass.getName()) + quote(testClass1.getName()), "directDependency3", quote(transitivelyDependentClass3.getName()) )); }