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