View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
23  
24  import java.util.Locale;
25  import java.util.Optional;
26  import java.util.Set;
27  
28  import org.junit.jupiter.api.Test;
29  
30  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
32  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
33  import com.puppycrawl.tools.checkstyle.utils.ModuleReflectionUtil;
34  import com.tngtech.archunit.base.DescribedPredicate;
35  import com.tngtech.archunit.core.domain.JavaClass;
36  import com.tngtech.archunit.core.domain.JavaClasses;
37  import com.tngtech.archunit.core.domain.JavaType;
38  import com.tngtech.archunit.core.domain.properties.HasName;
39  import com.tngtech.archunit.core.importer.ClassFileImporter;
40  import com.tngtech.archunit.core.importer.ImportOption;
41  import com.tngtech.archunit.lang.ArchCondition;
42  import com.tngtech.archunit.lang.ArchRule;
43  import com.tngtech.archunit.lang.ConditionEvents;
44  import com.tngtech.archunit.lang.SimpleConditionEvent;
45  
46  public class ArchUnitSuperClassTest {
47  
48      /**
49       * Classes not abiding to {@link #testChecksShouldHaveAllowedAbstractClassAsSuperclass()} rule.
50       */
51      private static final Set<String> SUPPRESSED_CLASSES = Set.of(
52          "com.puppycrawl.tools.checkstyle.checks.coding.SuperCloneCheck",
53          "com.puppycrawl.tools.checkstyle.checks.coding.SuperFinalizeCheck",
54          "com.puppycrawl.tools.checkstyle.checks.header.HeaderCheck",
55          "com.puppycrawl.tools.checkstyle.checks.header.RegexpHeaderCheck",
56          "com.puppycrawl.tools.checkstyle.checks.header.MultiFileRegexpHeaderCheck",
57          "com.puppycrawl.tools.checkstyle.checks.metrics.ClassDataAbstractionCouplingCheck",
58          "com.puppycrawl.tools.checkstyle.checks.metrics.ClassFanOutComplexityCheck",
59          "com.puppycrawl.tools.checkstyle.checks.naming.AbstractAccessControlNameCheck",
60          "com.puppycrawl.tools.checkstyle.checks.naming.CatchParameterNameCheck",
61          "com.puppycrawl.tools.checkstyle.checks.naming.ClassTypeParameterNameCheck",
62          "com.puppycrawl.tools.checkstyle.checks.naming.ConstantNameCheck",
63          "com.puppycrawl.tools.checkstyle.checks.naming.IllegalIdentifierNameCheck",
64          "com.puppycrawl.tools.checkstyle.checks.naming.InterfaceTypeParameterNameCheck",
65          "com.puppycrawl.tools.checkstyle.checks.naming.LambdaParameterNameCheck",
66          "com.puppycrawl.tools.checkstyle.checks.naming.LocalFinalVariableNameCheck",
67          "com.puppycrawl.tools.checkstyle.checks.naming.LocalVariableNameCheck",
68          "com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck",
69          "com.puppycrawl.tools.checkstyle.checks.naming.MethodNameCheck",
70          "com.puppycrawl.tools.checkstyle.checks.naming.MethodTypeParameterNameCheck",
71          "com.puppycrawl.tools.checkstyle.checks.naming.ParameterNameCheck",
72          "com.puppycrawl.tools.checkstyle.checks.naming.PatternVariableNameCheck",
73          "com.puppycrawl.tools.checkstyle.checks.naming.RecordComponentNameCheck",
74          "com.puppycrawl.tools.checkstyle.checks.naming.RecordTypeParameterNameCheck",
75          "com.puppycrawl.tools.checkstyle.checks.naming.StaticVariableNameCheck",
76          "com.puppycrawl.tools.checkstyle.checks.naming.TypeNameCheck",
77          "com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck",
78          "com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck"
79      );
80  
81      /**
82       * ArchCondition checking that a class is the direct subclass of a particular class.
83       *
84       * @param superclass the superclass
85       * @return ArchCondition checking that a class is the direct subclass of a particular class
86       */
87      private static ArchCondition<JavaClass> beDirectSubclassOf(Class<?> superclass) {
88          return new SuperclassArchCondition(superclass);
89      }
90  
91      /**
92       * Tests that all checks have {@link AbstractCheck} or {@link AbstractFileSetCheck} or
93       * {@link AbstractJavadocCheck} as their super class.
94       */
95      @Test
96      public void testChecksShouldHaveAllowedAbstractClassAsSuperclass() {
97          final JavaClasses checksPackage = new ClassFileImporter()
98              .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
99              .importPackages("com.puppycrawl.tools.checkstyle")
100             .that(new DescribedPredicate<>("are checkstyle modules") {
101                 @Override
102                 public boolean test(JavaClass input) {
103                     final Class<?> clazz = input.reflect();
104                     return ModuleReflectionUtil.isCheckstyleModule(clazz)
105                         && (ModuleReflectionUtil.isCheckstyleTreeWalkerCheck(clazz)
106                             || ModuleReflectionUtil.isFileSetModule(clazz));
107                 }
108             });
109 
110         final ArchCondition<JavaClass> beSuppressedClass = new SuppressionArchCondition<>(
111             SUPPRESSED_CLASSES, "be suppressed");
112 
113         final ArchRule checksShouldHaveAllowedAbstractClassAsSuper = classes()
114             .should(beDirectSubclassOf(AbstractCheck.class)
115                         .or(beDirectSubclassOf(AbstractFileSetCheck.class))
116                         .or(beDirectSubclassOf(AbstractJavadocCheck.class)))
117             .orShould(beSuppressedClass);
118 
119         checksShouldHaveAllowedAbstractClassAsSuper.check(checksPackage);
120     }
121 
122     /**
123      * ArchCondition checking that a given class extends the expected superclass.
124      */
125     private static final class SuperclassArchCondition extends ArchCondition<JavaClass> {
126 
127         private final Class<?> expectedSuperclass;
128 
129         private SuperclassArchCondition(Class<?> expectedSuperclass) {
130             super("be subclass of " + expectedSuperclass.getSimpleName());
131             this.expectedSuperclass = expectedSuperclass;
132         }
133 
134         @Override
135         public void check(JavaClass item, ConditionEvents events) {
136             final Optional<JavaType> superclassOptional = item.getSuperclass();
137             if (superclassOptional.isPresent()) {
138                 final JavaClass superclass = superclassOptional.get().toErasure();
139                 if (!superclass.isEquivalentTo(expectedSuperclass)) {
140                     final String format = "<%s> is subclass of <%s> instead of <%s>";
141                     final String message = String
142                         .format(Locale.ROOT, format, item.getFullName(), superclass.getFullName(),
143                                 expectedSuperclass.getName());
144                     events.add(SimpleConditionEvent.violated(item, message));
145                 }
146             }
147         }
148     }
149 
150     /**
151      * ArchCondition checking if a type or a member is present in the suppression list.
152      */
153     private static final class SuppressionArchCondition<T extends HasName.AndFullName>
154         extends ArchCondition<T> {
155 
156         private final Set<String> suppressions;
157 
158         private SuppressionArchCondition(Set<String> suppressions, String description) {
159             super(description);
160             this.suppressions = suppressions;
161         }
162 
163         @Override
164         public void check(HasName.AndFullName item, ConditionEvents events) {
165             if (!suppressions.contains(item.getFullName())) {
166                 final String message = String.format(
167                     Locale.ROOT, "should %s or resolved.", getDescription());
168                 events.add(SimpleConditionEvent.violated(item, message));
169             }
170         }
171     }
172 }