1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.internal;
21
22 import static com.google.common.truth.Truth.assertWithMessage;
23 import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
24
25 import java.util.HashSet;
26 import java.util.Locale;
27 import java.util.Optional;
28 import java.util.Set;
29
30 import org.junit.jupiter.api.Test;
31
32 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
33 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
34 import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
35 import com.puppycrawl.tools.checkstyle.utils.ModuleReflectionUtil;
36 import com.tngtech.archunit.base.DescribedPredicate;
37 import com.tngtech.archunit.core.domain.JavaClass;
38 import com.tngtech.archunit.core.domain.JavaClasses;
39 import com.tngtech.archunit.core.domain.JavaType;
40 import com.tngtech.archunit.core.domain.properties.HasName;
41 import com.tngtech.archunit.core.importer.ClassFileImporter;
42 import com.tngtech.archunit.core.importer.ImportOption;
43 import com.tngtech.archunit.lang.ArchCondition;
44 import com.tngtech.archunit.lang.ArchRule;
45 import com.tngtech.archunit.lang.ConditionEvents;
46 import com.tngtech.archunit.lang.SimpleConditionEvent;
47
48 public class ArchUnitSuperClassTest {
49
50
51
52
53 private static final Set<String> SUPPRESSED_CLASSES = Set.of(
54 "com.puppycrawl.tools.checkstyle.checks.coding.SuperCloneCheck",
55 "com.puppycrawl.tools.checkstyle.checks.coding.SuperFinalizeCheck",
56 "com.puppycrawl.tools.checkstyle.checks.header.HeaderCheck",
57 "com.puppycrawl.tools.checkstyle.checks.header.RegexpHeaderCheck",
58 "com.puppycrawl.tools.checkstyle.checks.header.MultiFileRegexpHeaderCheck",
59 "com.puppycrawl.tools.checkstyle.checks.metrics.ClassDataAbstractionCouplingCheck",
60 "com.puppycrawl.tools.checkstyle.checks.metrics.ClassFanOutComplexityCheck",
61 "com.puppycrawl.tools.checkstyle.checks.naming.CatchParameterNameCheck",
62 "com.puppycrawl.tools.checkstyle.checks.naming.ClassTypeParameterNameCheck",
63 "com.puppycrawl.tools.checkstyle.checks.naming.ConstantNameCheck",
64 "com.puppycrawl.tools.checkstyle.checks.naming.IllegalIdentifierNameCheck",
65 "com.puppycrawl.tools.checkstyle.checks.naming.InterfaceTypeParameterNameCheck",
66 "com.puppycrawl.tools.checkstyle.checks.naming.LambdaParameterNameCheck",
67 "com.puppycrawl.tools.checkstyle.checks.naming.LocalFinalVariableNameCheck",
68 "com.puppycrawl.tools.checkstyle.checks.naming.LocalVariableNameCheck",
69 "com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck",
70 "com.puppycrawl.tools.checkstyle.checks.naming.MethodNameCheck",
71 "com.puppycrawl.tools.checkstyle.checks.naming.MethodTypeParameterNameCheck",
72 "com.puppycrawl.tools.checkstyle.checks.naming.ParameterNameCheck",
73 "com.puppycrawl.tools.checkstyle.checks.naming.PatternVariableNameCheck",
74 "com.puppycrawl.tools.checkstyle.checks.naming.RecordComponentNameCheck",
75 "com.puppycrawl.tools.checkstyle.checks.naming.RecordTypeParameterNameCheck",
76 "com.puppycrawl.tools.checkstyle.checks.naming.StaticVariableNameCheck",
77 "com.puppycrawl.tools.checkstyle.checks.naming.TypeNameCheck",
78 "com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck",
79 "com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck"
80 );
81
82
83
84
85
86
87
88 private static ArchCondition<JavaClass> beDirectSubclassOf(Class<?> superclass) {
89 return new SuperclassArchCondition(superclass);
90 }
91
92
93
94
95
96 @Test
97 public void testChecksShouldHaveAllowedAbstractClassAsSuperclass() {
98 final JavaClasses checksPackage = new ClassFileImporter()
99 .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
100 .importPackages("com.puppycrawl.tools.checkstyle")
101 .that(new DescribedPredicate<>("are checkstyle modules") {
102 @Override
103 public boolean test(JavaClass input) {
104 final Class<?> clazz = input.reflect();
105 return ModuleReflectionUtil.isCheckstyleModule(clazz)
106 && (ModuleReflectionUtil.isCheckstyleTreeWalkerCheck(clazz)
107 || ModuleReflectionUtil.isFileSetModule(clazz));
108 }
109 });
110
111 final SuppressionArchCondition<JavaClass> beSuppressedClass =
112 new SuppressionArchCondition<>(SUPPRESSED_CLASSES, "be suppressed");
113
114 final ArchRule checksShouldHaveAllowedAbstractClassAsSuper = classes()
115 .should(beDirectSubclassOf(AbstractCheck.class)
116 .or(beDirectSubclassOf(AbstractFileSetCheck.class))
117 .or(beDirectSubclassOf(AbstractJavadocCheck.class)))
118 .orShould(beSuppressedClass);
119
120 checksShouldHaveAllowedAbstractClassAsSuper.check(checksPackage);
121
122 assertWithMessage("Outdated suppressions (can be removed)")
123 .that(beSuppressedClass.suppressions)
124 .containsExactlyElementsIn(beSuppressedClass.usedSuppressions);
125 }
126
127
128
129
130 private static final class SuperclassArchCondition extends ArchCondition<JavaClass> {
131
132 private final Class<?> expectedSuperclass;
133
134 private SuperclassArchCondition(Class<?> expectedSuperclass) {
135 super("be subclass of " + expectedSuperclass.getSimpleName());
136 this.expectedSuperclass = expectedSuperclass;
137 }
138
139 @Override
140 public void check(JavaClass item, ConditionEvents events) {
141 final Optional<JavaType> superclassOptional = item.getSuperclass();
142 if (superclassOptional.isPresent()) {
143 final JavaClass superclass = superclassOptional.get().toErasure();
144 if (!superclass.isEquivalentTo(expectedSuperclass)) {
145 final String message = String.format(Locale.ROOT,
146 "<%s> is subclass of <%s> instead of <%s>",
147 item.getFullName(),
148 superclass.getFullName(),
149 expectedSuperclass.getName());
150 events.add(SimpleConditionEvent.violated(item, message));
151 }
152 }
153 }
154 }
155
156
157
158
159 private static final class SuppressionArchCondition<T extends HasName.AndFullName>
160 extends ArchCondition<T> {
161
162 private final Set<String> suppressions;
163 private final Set<String> usedSuppressions;
164
165 private SuppressionArchCondition(Set<String> suppressions, String description) {
166 super(description);
167 this.suppressions = suppressions;
168 usedSuppressions = new HashSet<>();
169 }
170
171 @Override
172 public void check(HasName.AndFullName item, ConditionEvents events) {
173 final String fullName = item.getFullName();
174 if (suppressions.contains(fullName)) {
175 usedSuppressions.add(fullName);
176 }
177 else {
178 final String message = String.format(
179 Locale.ROOT, "should %s or resolved.", getDescription());
180 events.add(SimpleConditionEvent.violated(item, message));
181 }
182 }
183 }
184
185 }