1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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
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
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
157
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
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
206
207 private static final ArchCondition<JavaField> BE_IMMUTABLE = new ImmutableFieldArchCondition();
208
209
210
211
212 private static final DescribedPredicate<JavaClass> IMMUTABLE_FIELDS =
213 new ImmutableFieldsPredicate();
214
215
216
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
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
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
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
310
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
338
339 private static final class ImmutableFieldArchCondition extends ArchCondition<JavaField> {
340 private ImmutableFieldArchCondition() {
341 super("be among immutable types");
342 }
343
344
345
346
347
348
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
359
360
361
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
371
372
373
374
375
376
377
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
408
409 private static final class ModulePropertyPredicate extends DescribedPredicate<JavaField> {
410
411 private ModulePropertyPredicate() {
412 super("module properties");
413 }
414
415
416
417
418
419
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
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
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 }