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.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
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
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
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
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
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
161
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
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
209
210 private static final ArchCondition<JavaField> BE_IMMUTABLE = new ImmutableFieldArchCondition();
211
212
213
214
215 private static final DescribedPredicate<JavaClass> IMMUTABLE_FIELDS =
216 new ImmutableFieldsPredicate();
217
218
219
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
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
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
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
299
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
321
322 private static final class ImmutableFieldArchCondition extends ArchCondition<JavaField> {
323 private ImmutableFieldArchCondition() {
324 super("be among immutable types");
325 }
326
327
328
329
330
331
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
342
343
344
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
354
355
356
357
358
359
360
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
391
392 private static final class ModulePropertyPredicate extends DescribedPredicate<JavaField> {
393
394 private ModulePropertyPredicate() {
395 super("module properties");
396 }
397
398
399
400
401
402
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
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
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 }