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.tngtech.archunit.base.DescribedPredicate.doNot;
23  import static com.tngtech.archunit.base.DescribedPredicate.not;
24  import static com.tngtech.archunit.lang.conditions.ArchPredicates.are;
25  import static com.tngtech.archunit.lang.conditions.ArchPredicates.have;
26  import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
27  import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;
28  
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.function.Function;
34  import java.util.stream.Collectors;
35  
36  import org.junit.jupiter.api.Test;
37  
38  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
39  import com.puppycrawl.tools.checkstyle.GlobalStatefulCheck;
40  import com.puppycrawl.tools.checkstyle.StatelessCheck;
41  import com.puppycrawl.tools.checkstyle.meta.ModuleDetails;
42  import com.puppycrawl.tools.checkstyle.meta.ModulePropertyDetails;
43  import com.puppycrawl.tools.checkstyle.meta.XmlMetaReader;
44  import com.puppycrawl.tools.checkstyle.utils.ModuleReflectionUtil;
45  import com.tngtech.archunit.base.DescribedPredicate;
46  import com.tngtech.archunit.core.domain.JavaClass;
47  import com.tngtech.archunit.core.domain.JavaClasses;
48  import com.tngtech.archunit.core.domain.JavaField;
49  import com.tngtech.archunit.core.domain.JavaModifier;
50  import com.tngtech.archunit.core.domain.JavaParameterizedType;
51  import com.tngtech.archunit.core.domain.JavaType;
52  import com.tngtech.archunit.core.domain.properties.HasName;
53  import com.tngtech.archunit.core.importer.ClassFileImporter;
54  import com.tngtech.archunit.core.importer.ImportOption;
55  import com.tngtech.archunit.lang.ArchCondition;
56  import com.tngtech.archunit.lang.ArchRule;
57  import com.tngtech.archunit.lang.ConditionEvents;
58  import com.tngtech.archunit.lang.SimpleConditionEvent;
59  
60  public class ImmutabilityTest {
61  
62      /**
63       * Immutable types canonical names.
64       */
65      private static final Set<String> IMMUTABLE_TYPES = Set.of(
66          "java.lang.String",
67          "java.lang.Integer",
68          "java.lang.Byte",
69          "java.lang.Character",
70          "java.lang.Short",
71          "java.lang.Boolean",
72          "java.lang.Long",
73          "java.lang.Double",
74          "java.lang.Float",
75          "java.lang.StackTraceElement",
76          "java.math.BigInteger",
77          "java.math.BigDecimal",
78          "java.io.File",
79          "java.util.Locale",
80          "java.util.UUID",
81          "java.net.URL",
82          "java.net.URI",
83          "java.net.Inet4Address",
84          "java.net.Inet6Address",
85          "java.net.InetSocketAddress",
86          "java.util.regex.Pattern"
87      );
88  
89      /**
90       * Immutable primitive types.
91       */
92      private static final Set<String> PRIMITIVE_TYPES = Set.of(
93          "byte",
94          "short",
95          "int",
96          "long",
97          "float",
98          "double",
99          "char",
100         "boolean"
101     );
102 
103     /**
104      * List of fields that are a zero size array. They are immutable by definition.
105      */
106     private static final Set<String> ZERO_SIZE_ARRAY_FIELDS = Set.of(
107         "com.puppycrawl.tools.checkstyle.utils.CommonUtil.EMPTY_BIT_SET",
108         "com.puppycrawl.tools.checkstyle.utils.CommonUtil.EMPTY_BYTE_ARRAY",
109         "com.puppycrawl.tools.checkstyle.utils.CommonUtil.EMPTY_DOUBLE_ARRAY",
110         "com.puppycrawl.tools.checkstyle.utils.CommonUtil.EMPTY_INTEGER_OBJECT_ARRAY",
111         "com.puppycrawl.tools.checkstyle.utils.CommonUtil.EMPTY_INT_ARRAY",
112         "com.puppycrawl.tools.checkstyle.utils.CommonUtil.EMPTY_OBJECT_ARRAY",
113         "com.puppycrawl.tools.checkstyle.utils.CommonUtil.EMPTY_STRING_ARRAY"
114     );
115 
116     /**
117      * List of fields not following {@link #testUtilClassesImmutability()} rule.
118      */
119     private static final Set<String> SUPPRESSED_FIELDS_IN_UTIL_CLASSES = Set.of(
120         "com.puppycrawl.tools.checkstyle.utils.TokenUtil.TOKEN_IDS",
121         "com.puppycrawl.tools.checkstyle.utils.XpathUtil.TOKEN_TYPES_WITH_TEXT_ATTRIBUTE"
122     );
123 
124     /**
125      * List of fields not following {@link #testFieldsInStatelessChecksShouldBeImmutable()} rule.
126      */
127     private static final Set<String> SUPPRESSED_FIELDS_IN_MODULES = Set.of(
128         "com.puppycrawl.tools.checkstyle.checks.FinalParametersCheck.primitiveDataTypes",
129         "com.puppycrawl.tools.checkstyle.checks.SuppressWarningsHolder.ENTRIES",
130         "com.puppycrawl.tools.checkstyle.checks.annotation.MissingDeprecatedCheck.TYPES_HASH_SET",
131         "com.puppycrawl.tools.checkstyle.checks.coding.AvoidDoubleBraceInitializationCheck"
132             + ".HAS_MEMBERS",
133         "com.puppycrawl.tools.checkstyle.checks.coding.AvoidDoubleBraceInitializationCheck"
134             + ".IGNORED_TYPES",
135         "com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck"
136             + ".ALLOWED_ASSIGNMENT_CONTEXT",
137         "com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck"
138             + ".ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT",
139         "com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck.COMPARISON_TYPES",
140         "com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck.CONTROL_CONTEXT",
141         "com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck"
142             + ".LOOP_IDIOM_IGNORED_PARENTS",
143         "com.puppycrawl.tools.checkstyle.checks.coding.MatchXpathCheck.xpathExpression",
144         "com.puppycrawl.tools.checkstyle.checks.javadoc.AtclauseOrderCheck.DEFAULT_ORDER",
145         "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocBlockTagLocationCheck.DEFAULT_TAGS",
146         "com.puppycrawl.tools.checkstyle.checks.javadoc.SummaryJavadocCheck.ALLOWED_TYPES",
147         "com.puppycrawl.tools.checkstyle.checks.modifier.ModifierOrderCheck.JLS_ORDER",
148         "com.puppycrawl.tools.checkstyle.checks.modifier.RedundantModifierCheck"
149             + ".TOKENS_FOR_INTERFACE_MODIFIERS",
150         "com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck.detector",
151         "com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck.detector",
152         "com.puppycrawl.tools.checkstyle.checks.coding.IllegalTokenTextCheck.formatString",
153         "com.puppycrawl.tools.checkstyle.checks.coding.IllegalSymbolCheck.codePointRanges",
154         "com.puppycrawl.tools.checkstyle.checks.javadoc.WriteTagCheck.tagRegExp",
155         "com.puppycrawl.tools.checkstyle.checks.naming.AbstractNameCheck.format",
156         "com.puppycrawl.tools.checkstyle.checks.whitespace.AbstractParenPadCheck.option"
157     );
158 
159     /**
160      * List of classes not following
161      * {@link #testClassesWithImmutableFieldsShouldBeStateless()} rule.
162      */
163     private static final Set<String> SUPPRESSED_CLASSES_FOR_STATELESS_CHECK_RULE = Set.of(
164         "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck",
165         "com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocMethodCheck",
166         "com.puppycrawl.tools.checkstyle.checks.metrics.ClassDataAbstractionCouplingCheck",
167         "com.puppycrawl.tools.checkstyle.checks.metrics.ClassFanOutComplexityCheck",
168         "com.puppycrawl.tools.checkstyle.checks.naming.CatchParameterNameCheck",
169         "com.puppycrawl.tools.checkstyle.checks.naming.ClassTypeParameterNameCheck",
170         "com.puppycrawl.tools.checkstyle.checks.naming.ConstantNameCheck",
171         "com.puppycrawl.tools.checkstyle.checks.naming.InterfaceTypeParameterNameCheck",
172         "com.puppycrawl.tools.checkstyle.checks.naming.LambdaParameterNameCheck",
173         "com.puppycrawl.tools.checkstyle.checks.naming.LocalFinalVariableNameCheck",
174         "com.puppycrawl.tools.checkstyle.checks.naming.LocalVariableNameCheck",
175         "com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck",
176         "com.puppycrawl.tools.checkstyle.checks.naming.MethodNameCheck",
177         "com.puppycrawl.tools.checkstyle.checks.naming.MethodTypeParameterNameCheck",
178         "com.puppycrawl.tools.checkstyle.checks.naming.ParameterNameCheck",
179         "com.puppycrawl.tools.checkstyle.checks.naming.PatternVariableNameCheck",
180         "com.puppycrawl.tools.checkstyle.checks.naming.RecordComponentNameCheck",
181         "com.puppycrawl.tools.checkstyle.checks.naming.RecordTypeParameterNameCheck",
182         "com.puppycrawl.tools.checkstyle.checks.naming.StaticVariableNameCheck",
183         "com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck",
184         "com.puppycrawl.tools.checkstyle.checks.naming.TypeNameCheck"
185     );
186 
187     /**
188      * List of classes not following {@link #testClassesWithMutableFieldsShouldBeStateful()} rule.
189      */
190     private static final Set<String> SUPPRESSED_CLASSES_FOR_STATEFUL_CHECK_RULE = Set.of(
191         "com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck"
192     );
193 
194     private static final JavaClasses CHECKSTYLE_CHECKS = new ClassFileImporter()
195         .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
196         .importPackages("com.puppycrawl.tools.checkstyle")
197         .that(new DescribedPredicate<>("are checkstyle modules") {
198             @Override
199             public boolean test(JavaClass input) {
200                 final Class<?> clazz = input.reflect();
201                 return ModuleReflectionUtil.isCheckstyleModule(clazz)
202                     && (ModuleReflectionUtil.isCheckstyleTreeWalkerCheck(clazz)
203                         || ModuleReflectionUtil.isFileSetModule(clazz));
204             }
205         });
206 
207     /**
208      * ArchCondition for immutable fields.
209      */
210     private static final ArchCondition<JavaField> BE_IMMUTABLE = new ImmutableFieldArchCondition();
211 
212     /**
213      * DescribedPredicate defining condition for a class to have immutable fields.
214      */
215     private static final DescribedPredicate<JavaClass> IMMUTABLE_FIELDS =
216         new ImmutableFieldsPredicate();
217 
218     /**
219      * Map of module full name to module details.
220      */
221     private static final Map<String, ModuleDetails> MODULE_DETAILS_MAP =
222         XmlMetaReader.readAllModulesIncludingThirdPartyIfAny().stream()
223             .collect(Collectors.toUnmodifiableMap(ModuleDetails::getFullQualifiedName,
224                                       Function.identity()));
225 
226     /**
227      * Test to ensure that fields in util classes are immutable.
228      */
229     @Test
230     public void testUtilClassesImmutability() {
231         final JavaClasses utilClasses = new ClassFileImporter()
232             .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
233             .importPackages("com.puppycrawl.tools.checkstyle.utils",
234                             "com.puppycrawl.tools.checkstyle.checks.javadoc.utils");
235 
236         final ArchCondition<JavaField> beSuppressedField = new SuppressionArchCondition<>(
237             SUPPRESSED_FIELDS_IN_UTIL_CLASSES, "be suppressed");
238 
239         final ArchRule fieldsInUtilClassesShouldBeImmutable = fields()
240             .that()
241             .areDeclaredInClassesThat()
242             .haveSimpleNameEndingWith("Util")
243             .should(BE_IMMUTABLE)
244             .andShould()
245             .beFinal()
246             .andShould()
247             .beStatic()
248             .orShould(beSuppressedField);
249 
250         fieldsInUtilClassesShouldBeImmutable.check(utilClasses);
251     }
252 
253     /**
254      * Test to ensure modules annotated with {@link StatelessCheck} contain immutable fields.
255      */
256     @Test
257     public void testFieldsInStatelessChecksShouldBeImmutable() {
258         final DescribedPredicate<JavaField> moduleProperties = new ModulePropertyPredicate();
259 
260         final ArchCondition<JavaField> beSuppressedField = new SuppressionArchCondition<>(
261             SUPPRESSED_FIELDS_IN_MODULES, "be suppressed");
262 
263         final ArchRule fieldsInStatelessChecksShouldBeImmutable = fields()
264             .that()
265             .haveNameNotContaining("$")
266             .and()
267             .areDeclaredInClassesThat()
268             .areAnnotatedWith(StatelessCheck.class)
269             .and(are(not(moduleProperties)))
270             .should(BE_IMMUTABLE)
271             .andShould()
272             .beFinal()
273             .orShould(beSuppressedField);
274 
275         fieldsInStatelessChecksShouldBeImmutable.check(CHECKSTYLE_CHECKS);
276     }
277 
278     /**
279      * Test to ensure classes with immutable fields are annotated with {@link StatelessCheck}.
280      */
281     @Test
282     public void testClassesWithImmutableFieldsShouldBeStateless() {
283         final ArchCondition<JavaClass> beSuppressedClass = new SuppressionArchCondition<>(
284             SUPPRESSED_CLASSES_FOR_STATELESS_CHECK_RULE, "be suppressed");
285 
286         final ArchRule classesWithImmutableFieldsShouldBeStateless = classes()
287             .that(have(IMMUTABLE_FIELDS))
288             .and()
289             .doNotHaveModifier(JavaModifier.ABSTRACT)
290             .should()
291             .beAnnotatedWith(StatelessCheck.class)
292             .orShould(beSuppressedClass);
293 
294         classesWithImmutableFieldsShouldBeStateless.check(CHECKSTYLE_CHECKS);
295     }
296 
297     /**
298      * Test to ensure classes with mutable fields are annotated with {@link FileStatefulCheck} or
299      * {@link GlobalStatefulCheck}.
300      */
301     @Test
302     public void testClassesWithMutableFieldsShouldBeStateful() {
303         final ArchCondition<JavaClass> beSuppressedClass = new SuppressionArchCondition<>(
304             SUPPRESSED_CLASSES_FOR_STATEFUL_CHECK_RULE, "be suppressed");
305 
306         final ArchRule classesWithMutableFieldsShouldBeStateful = classes()
307             .that(doNot(have(IMMUTABLE_FIELDS)))
308             .and()
309             .doNotHaveModifier(JavaModifier.ABSTRACT)
310             .should()
311             .beAnnotatedWith(FileStatefulCheck.class)
312             .orShould()
313             .beAnnotatedWith(GlobalStatefulCheck.class)
314             .orShould(beSuppressedClass);
315 
316         classesWithMutableFieldsShouldBeStateful.check(CHECKSTYLE_CHECKS);
317     }
318 
319     /**
320      * ArchCondition checking fields are immutable.
321      */
322     private static final class ImmutableFieldArchCondition extends ArchCondition<JavaField> {
323         private ImmutableFieldArchCondition() {
324             super("be among immutable types");
325         }
326 
327         /**
328          * Whether the raw type of the field is immutable.
329          *
330          * @param javaField java field to examine
331          * @return {@code true} if the raw type of field is immutable.
332          */
333         private static boolean isRawTypeImmutable(JavaField javaField) {
334             final JavaClass rawType = javaField.getRawType();
335             final String rawTypeName = rawType.getName();
336             return PRIMITIVE_TYPES.contains(rawTypeName)
337                 || IMMUTABLE_TYPES.contains(rawTypeName);
338         }
339 
340         /**
341          * Whether the field is an enum constant or an empty array.
342          *
343          * @param javaField java field to examine
344          * @return {@code true} if the field is an enum constant or an empty array
345          */
346         private static boolean isEnumConstantOrEmptyArray(JavaField javaField) {
347             final JavaClass rawType = javaField.getRawType();
348             return rawType.isEnum()
349                 || ZERO_SIZE_ARRAY_FIELDS.contains(javaField.getFullName());
350         }
351 
352         /**
353          * Whether the parameterized type of a field is immutable if it contains parameterized
354          * type. Parameterized type refers to the generic type of a field.
355          * {@code List<String>}, here the concrete type of parameterized field is
356          * {@code java.lang.String}.
357          *
358          * @param javaField java field to examine
359          * @return {@code true} if the parameterized type of a field is immutable
360          *         if it contains parameterized type
361          */
362         private static boolean isParameterizedTypeImmutable(JavaField javaField) {
363             boolean isParameterizedTypeImmutable = false;
364             final JavaType javaType = javaField.getType();
365 
366             if (javaType instanceof JavaParameterizedType parameterizedType) {
367                 isParameterizedTypeImmutable = parameterizedType.getActualTypeArguments().stream()
368                     .allMatch(actualTypeArgument -> {
369                         return IMMUTABLE_TYPES.contains(actualTypeArgument.toErasure().getName());
370                     });
371             }
372             return isParameterizedTypeImmutable;
373         }
374 
375         @Override
376         public void check(JavaField item, ConditionEvents events) {
377             if (!isRawTypeImmutable(item)
378                 && !isEnumConstantOrEmptyArray(item)
379                 && !isParameterizedTypeImmutable(item)) {
380                 final String message = String
381                     .format(Locale.ROOT, "Field <%s> should %s in %s",
382                             item.getFullName(), getDescription(),
383                             item.getSourceCodeLocation());
384                 events.add(SimpleConditionEvent.violated(item, message));
385             }
386         }
387     }
388 
389     /**
390      * DescribedPredicate defining condition for a field to be a module property.
391      */
392     private static final class ModulePropertyPredicate extends DescribedPredicate<JavaField> {
393 
394         private ModulePropertyPredicate() {
395             super("module properties");
396         }
397 
398         /**
399          * Whether a field is a module property or not.
400          *
401          * @param javaField field to check
402          * @return {@code true} if field is a module property
403          */
404         private static boolean isModuleProperty(JavaField javaField) {
405             boolean result = false;
406             final JavaClass containingClass = javaField.getOwner();
407             final ModuleDetails moduleDetails = MODULE_DETAILS_MAP.get(
408                 containingClass.getFullName());
409             if (moduleDetails != null) {
410                 final List<ModulePropertyDetails> properties = moduleDetails.getProperties();
411                 result = properties.stream()
412                     .map(ModulePropertyDetails::getName)
413                     .anyMatch(moduleName -> moduleName.equals(javaField.getName()));
414             }
415             return result;
416         }
417 
418         @Override
419         public boolean test(JavaField input) {
420             return isModuleProperty(input);
421         }
422     }
423 
424     /**
425      * DescribedPredicate defining condition for a class to have immutable fields.
426      */
427     private static final class ImmutableFieldsPredicate extends DescribedPredicate<JavaClass> {
428         private ImmutableFieldsPredicate() {
429             super("immutable fields");
430         }
431 
432         @Override
433         public boolean test(JavaClass input) {
434             final Set<JavaField> fields = input.getFields();
435             return fields.stream()
436                 .filter(javaField -> {
437                     return !ModulePropertyPredicate.isModuleProperty(javaField)
438                         && !javaField.getName().contains("$")
439                         && !SUPPRESSED_FIELDS_IN_MODULES.contains(javaField.getFullName());
440                 })
441                 .allMatch(javaField -> {
442                     final Set<JavaModifier> javaFieldModifiers = javaField.getModifiers();
443                     return javaFieldModifiers.contains(JavaModifier.FINAL)
444                         && (ImmutableFieldArchCondition.isRawTypeImmutable(javaField)
445                             || ImmutableFieldArchCondition.isEnumConstantOrEmptyArray(javaField)
446                             || ImmutableFieldArchCondition.isParameterizedTypeImmutable(javaField));
447                 });
448         }
449     }
450 
451     /**
452      * ArchCondition checking if a type or a member is present in the suppression list.
453      */
454     private static final class SuppressionArchCondition<T extends HasName.AndFullName>
455         extends ArchCondition<T> {
456 
457         private final Set<String> suppressions;
458 
459         private SuppressionArchCondition(Set<String> suppressions, String description) {
460             super(description);
461             this.suppressions = suppressions;
462         }
463 
464         @Override
465         public void check(HasName.AndFullName item, ConditionEvents events) {
466             if (!suppressions.contains(item.getFullName())) {
467                 final String message = String.format(
468                     Locale.ROOT, "should %s or resolved.", getDescription());
469                 events.add(SimpleConditionEvent.violated(item, message));
470             }
471         }
472     }
473 }