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