Skip to content

Commit 43a475a

Browse files
authored
add rule to prevent dependencies on upper packages TNG#352
To keep packages splittable into separate artifacts in a clean way it can make sense to forbid dependencies on any parent package, as that prevents the possibility to cleanly separate packages on the same sub-package level.
2 parents 2cbaab8 + e860986 commit 43a475a

File tree

10 files changed

+166
-7
lines changed

10 files changed

+166
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.tngtech.archunit.exampletest.junit4;
2+
3+
import com.tngtech.archunit.example.layers.ClassViolatingCodingRules;
4+
import com.tngtech.archunit.junit.AnalyzeClasses;
5+
import com.tngtech.archunit.junit.ArchTest;
6+
import com.tngtech.archunit.junit.ArchUnitRunner;
7+
import com.tngtech.archunit.lang.ArchRule;
8+
import org.junit.experimental.categories.Category;
9+
import org.junit.runner.RunWith;
10+
11+
import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES;
12+
13+
@Category(Example.class)
14+
@RunWith(ArchUnitRunner.class)
15+
@AnalyzeClasses(packagesOf = ClassViolatingCodingRules.class)
16+
public class DependencyRulesTest {
17+
18+
@ArchTest
19+
static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.tngtech.archunit.exampletest.junit5;
2+
3+
import com.tngtech.archunit.example.layers.ClassViolatingCodingRules;
4+
import com.tngtech.archunit.junit.AnalyzeClasses;
5+
import com.tngtech.archunit.junit.ArchTag;
6+
import com.tngtech.archunit.junit.ArchTest;
7+
import com.tngtech.archunit.lang.ArchRule;
8+
9+
import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES;
10+
11+
@ArchTag("example")
12+
@AnalyzeClasses(packagesOf = ClassViolatingCodingRules.class)
13+
public class DependencyRulesTest {
14+
15+
@ArchTest
16+
static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES;
17+
}

archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/AbstractController.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
* For demo purpose only, see {@link InheritedControllerImpl}
77
*/
88
public abstract class AbstractController {
9-
9+
public abstract static class AbstractInnerControl {
10+
}
1011
}

archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/web/InheritedControllerImpl.java

+2
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
import com.tngtech.archunit.example.layers.AbstractController;
44

55
public class InheritedControllerImpl extends AbstractController {
6+
public static class ChildControl extends AbstractInnerControl {
7+
}
68
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.tngtech.archunit.exampletest;
2+
3+
import com.tngtech.archunit.core.domain.JavaClasses;
4+
import com.tngtech.archunit.core.importer.ClassFileImporter;
5+
import com.tngtech.archunit.example.layers.ClassViolatingCodingRules;
6+
import org.junit.Test;
7+
import org.junit.experimental.categories.Category;
8+
9+
import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES;
10+
11+
@Category(Example.class)
12+
public class DependencyRulesTest {
13+
14+
private final JavaClasses classes = new ClassFileImporter().importPackagesOf(ClassViolatingCodingRules.class);
15+
16+
@Test
17+
public void no_accesses_to_upper_package() {
18+
NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES.check(classes);
19+
}
20+
}

archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java

+32
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import com.tngtech.archunit.example.layers.persistence.WrongSecurityCheck;
8888
import com.tngtech.archunit.example.layers.persistence.first.InWrongPackageDao;
8989
import com.tngtech.archunit.example.layers.persistence.first.dao.EntityInWrongPackage;
90+
import com.tngtech.archunit.example.layers.persistence.first.dao.SomeDao;
9091
import com.tngtech.archunit.example.layers.persistence.first.dao.jpa.SomeJpa;
9192
import com.tngtech.archunit.example.layers.persistence.layerviolation.DaoCallingService;
9293
import com.tngtech.archunit.example.layers.persistence.second.dao.OtherDao;
@@ -582,6 +583,37 @@ Stream<DynamicTest> DaoRulesTest() {
582583
.toDynamicTests();
583584
}
584585

586+
@TestFactory
587+
Stream<DynamicTest> DependencyRulesTest() {
588+
return ExpectedTestFailures
589+
.forTests(
590+
com.tngtech.archunit.exampletest.DependencyRulesTest.class,
591+
com.tngtech.archunit.exampletest.junit4.DependencyRulesTest.class,
592+
com.tngtech.archunit.exampletest.junit5.DependencyRulesTest.class)
593+
594+
.ofRule("no classes should depend on upper packages, because that might prevent packages on that level from being split into separate artifacts in a clean way")
595+
.by(inheritanceFrom(UseCaseTwoController.class).extending(AbstractController.class))
596+
.by(callFromConstructor(UseCaseTwoController.class).toConstructor(AbstractController.class).inLine(6).asDependency())
597+
.by(inheritanceFrom(InheritedControllerImpl.class).extending(AbstractController.class))
598+
.by(callFromConstructor(InheritedControllerImpl.class).toConstructor(AbstractController.class).inLine(5).asDependency())
599+
.by(inheritanceFrom(InheritedControllerImpl.ChildControl.class).extending(AbstractController.AbstractInnerControl.class))
600+
.by(callFromConstructor(InheritedControllerImpl.ChildControl.class).toConstructor(AbstractController.AbstractInnerControl.class).inLine(6).asDependency())
601+
.by(callFromMethod(DaoCallingService.class, "violateLayerRulesTrickily").toConstructor(SomeMediator.class, ServiceViolatingLayerRules.class).inLine(18).asDependency())
602+
.by(callFromMethod(DaoCallingService.class, "violateLayerRulesTrickily").toMethod(SomeMediator.class, "violateLayerRulesIndirectly").inLine(18).asDependency())
603+
.by(annotatedClass(WronglyAnnotated.class).annotatedWith(MyController.class))
604+
.by(inheritanceFrom(VeryCentralCore.class).implementing(SomeOtherBusinessInterface.class))
605+
.by(inheritanceFrom(SomeJpa.class).implementing(SomeDao.class))
606+
.by(inheritanceFrom(OtherJpa.class).implementing(OtherDao.class))
607+
.by(annotatedClass(ServiceViolatingDaoRules.class).annotatedWith(MyService.class))
608+
.by(annotatedClass(ServiceViolatingLayerRules.class).annotatedWith(MyService.class))
609+
.by(inheritanceFrom(ServiceImplementation.class).implementing(ServiceInterface.class))
610+
.by(annotatedClass(ServiceImplementation.class).annotatedWith(MyService.class))
611+
.by(annotatedClass(WronglyNamedSvc.class).annotatedWith(MyService.class))
612+
.by(annotatedClass(AnnotatedController.class).annotatedWith(MyController.class))
613+
614+
.toDynamicTests();
615+
}
616+
585617
@TestFactory
586618
Stream<DynamicTest> FrozenRulesTest() {
587619
return ExpectedTestFailures

archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void associateLines(LineAssociation association) {
3131

3232
private String getExpectedMessage() {
3333
String expectedDescription = origin.getExpectedDescription() + " " + target.getExpectedDescription();
34-
String expectedLocation = String.format("(%s.java:%d)", origin.getDeclaringClass().getSimpleName(), lineNumber);
34+
String expectedLocation = String.format("(%s.java:%d)", origin.getLocationClass().getSimpleName(), lineNumber);
3535
return expectedDescription + " in " + expectedLocation;
3636
}
3737

archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public boolean correspondsTo(Object object) {
6464
}
6565

6666
private static String getDependencyPattern(String originName, String dependencyTypePattern, String targetName, int lineNumber) {
67-
return String.format(".*%s.*%s.*%s.*\\.java:%d.*", quote(originName), dependencyTypePattern, quote(targetName), lineNumber);
67+
return String.format(".*%s[^$]*%s[^$]*%s.*\\.java:%d.*", quote(originName), dependencyTypePattern, quote(targetName), lineNumber);
6868
}
6969

7070
public static class InheritanceCreator {

archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMember.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ abstract class ExpectedMember {
3232
}
3333
}
3434

35-
String lineMessage(int number) {
36-
return String.format("(%s.java:%d)", clazz.getSimpleName(), number);
37-
}
38-
3935
List<String> getParams() {
4036
return params;
4137
}
@@ -48,6 +44,14 @@ Class<?> getDeclaringClass() {
4844
return clazz;
4945
}
5046

47+
Class<?> getLocationClass() {
48+
Class<?> result = clazz;
49+
while (result.getEnclosingClass() != null) {
50+
result = result.getEnclosingClass();
51+
}
52+
return result;
53+
}
54+
5155
abstract String getExpectedDescription();
5256

5357
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2014-2020 TNG Technology Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.tngtech.archunit.library;
17+
18+
import com.tngtech.archunit.PublicAPI;
19+
import com.tngtech.archunit.core.domain.Dependency;
20+
import com.tngtech.archunit.core.domain.JavaClass;
21+
import com.tngtech.archunit.lang.ArchCondition;
22+
import com.tngtech.archunit.lang.ArchRule;
23+
import com.tngtech.archunit.lang.ConditionEvents;
24+
import com.tngtech.archunit.lang.SimpleConditionEvent;
25+
26+
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
27+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
28+
29+
@PublicAPI(usage = ACCESS)
30+
public final class DependencyRules {
31+
private DependencyRules() {
32+
}
33+
34+
@PublicAPI(usage = ACCESS)
35+
public static final ArchRule NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES =
36+
noClasses().should(dependOnUpperPackages())
37+
.because("that might prevent packages on that level from being split into separate artifacts in a clean way");
38+
39+
@PublicAPI(usage = ACCESS)
40+
public static ArchCondition<JavaClass> dependOnUpperPackages() {
41+
return new DependOnUpperPackagesCondition();
42+
}
43+
44+
private static class DependOnUpperPackagesCondition extends ArchCondition<JavaClass> {
45+
DependOnUpperPackagesCondition() {
46+
super("depend on upper packages");
47+
}
48+
49+
@Override
50+
public void check(final JavaClass clazz, final ConditionEvents events) {
51+
for (Dependency dependency : clazz.getDirectDependenciesFromSelf()) {
52+
boolean dependencyOnUpperPackage = isDependencyOnUpperPackage(dependency.getOriginClass(), dependency.getTargetClass());
53+
events.add(new SimpleConditionEvent(dependency, dependencyOnUpperPackage, dependency.getDescription()));
54+
}
55+
}
56+
57+
private boolean isDependencyOnUpperPackage(JavaClass origin, JavaClass target) {
58+
String originPackageName = origin.getPackageName();
59+
String targetSubPackagePrefix = target.getPackageName() + ".";
60+
return originPackageName.startsWith(targetSubPackagePrefix);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)