View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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       * Classes not abiding to {@link #testChecksShouldHaveAllowedAbstractClassAsSuperclass()} rule.
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       * ArchCondition checking that a class is the direct subclass of a particular class.
84       *
85       * @param superclass the superclass
86       * @return ArchCondition checking that a class is the direct subclass of a particular class
87       */
88      private static ArchCondition<JavaClass> beDirectSubclassOf(Class<?> superclass) {
89          return new SuperclassArchCondition(superclass);
90      }
91  
92      /**
93       * Tests that all checks have {@link AbstractCheck} or {@link AbstractFileSetCheck} or
94       * {@link AbstractJavadocCheck} as their super class.
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      * ArchCondition checking that a given class extends the expected superclass.
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      * ArchCondition checking if a type or a member is present in the suppression list.
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 }