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.javadoc.WriteTagCheck.tagRegExp",
154 "com.puppycrawl.tools.checkstyle.checks.naming.AbstractNameCheck.format",
155 "com.puppycrawl.tools.checkstyle.checks.whitespace.AbstractParenPadCheck.option"
156 );
157
158
159
160
161
162 private static final Set<String> SUPPRESSED_CLASSES_FOR_STATELESS_CHECK_RULE = Set.of(
163 "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck",
164 "com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocMethodCheck",
165 "com.puppycrawl.tools.checkstyle.checks.metrics.ClassDataAbstractionCouplingCheck",
166 "com.puppycrawl.tools.checkstyle.checks.metrics.ClassFanOutComplexityCheck",
167 "com.puppycrawl.tools.checkstyle.checks.naming.CatchParameterNameCheck",
168 "com.puppycrawl.tools.checkstyle.checks.naming.ClassTypeParameterNameCheck",
169 "com.puppycrawl.tools.checkstyle.checks.naming.ConstantNameCheck",
170 "com.puppycrawl.tools.checkstyle.checks.naming.InterfaceTypeParameterNameCheck",
171 "com.puppycrawl.tools.checkstyle.checks.naming.LambdaParameterNameCheck",
172 "com.puppycrawl.tools.checkstyle.checks.naming.LocalFinalVariableNameCheck",
173 "com.puppycrawl.tools.checkstyle.checks.naming.LocalVariableNameCheck",
174 "com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck",
175 "com.puppycrawl.tools.checkstyle.checks.naming.MethodNameCheck",
176 "com.puppycrawl.tools.checkstyle.checks.naming.MethodTypeParameterNameCheck",
177 "com.puppycrawl.tools.checkstyle.checks.naming.ParameterNameCheck",
178 "com.puppycrawl.tools.checkstyle.checks.naming.PatternVariableNameCheck",
179 "com.puppycrawl.tools.checkstyle.checks.naming.RecordComponentNameCheck",
180 "com.puppycrawl.tools.checkstyle.checks.naming.RecordTypeParameterNameCheck",
181 "com.puppycrawl.tools.checkstyle.checks.naming.StaticVariableNameCheck",
182 "com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck",
183 "com.puppycrawl.tools.checkstyle.checks.naming.TypeNameCheck"
184 );
185
186
187
188
189 private static final Set<String> SUPPRESSED_CLASSES_FOR_STATEFUL_CHECK_RULE = Set.of(
190 "com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck"
191 );
192
193 private static final JavaClasses CHECKSTYLE_CHECKS = new ClassFileImporter()
194 .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
195 .importPackages("com.puppycrawl.tools.checkstyle")
196 .that(new DescribedPredicate<>("are checkstyle modules") {
197 @Override
198 public boolean test(JavaClass input) {
199 final Class<?> clazz = input.reflect();
200 return ModuleReflectionUtil.isCheckstyleModule(clazz)
201 && (ModuleReflectionUtil.isCheckstyleTreeWalkerCheck(clazz)
202 || ModuleReflectionUtil.isFileSetModule(clazz));
203 }
204 });
205
206
207
208
209 private static final ArchCondition<JavaField> BE_IMMUTABLE = new ImmutableFieldArchCondition();
210
211
212
213
214 private static final DescribedPredicate<JavaClass> IMMUTABLE_FIELDS =
215 new ImmutableFieldsPredicate();
216
217
218
219
220 private static final Map<String, ModuleDetails> MODULE_DETAILS_MAP =
221 XmlMetaReader.readAllModulesIncludingThirdPartyIfAny().stream()
222 .collect(Collectors.toUnmodifiableMap(ModuleDetails::getFullQualifiedName,
223 Function.identity()));
224
225
226
227
228 @Test
229 public void testUtilClassesImmutability() {
230 final JavaClasses utilClasses = new ClassFileImporter()
231 .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
232 .importPackages("com.puppycrawl.tools.checkstyle.utils",
233 "com.puppycrawl.tools.checkstyle.checks.javadoc.utils");
234
235 final ArchCondition<JavaField> beSuppressedField = new SuppressionArchCondition<>(
236 SUPPRESSED_FIELDS_IN_UTIL_CLASSES, "be suppressed");
237
238 final ArchRule fieldsInUtilClassesShouldBeImmutable = fields()
239 .that()
240 .areDeclaredInClassesThat()
241 .haveSimpleNameEndingWith("Util")
242 .should(BE_IMMUTABLE)
243 .andShould()
244 .beFinal()
245 .andShould()
246 .beStatic()
247 .orShould(beSuppressedField);
248
249 fieldsInUtilClassesShouldBeImmutable.check(utilClasses);
250 }
251
252
253
254
255 @Test
256 public void testFieldsInStatelessChecksShouldBeImmutable() {
257 final DescribedPredicate<JavaField> moduleProperties = new ModulePropertyPredicate();
258
259 final ArchCondition<JavaField> beSuppressedField = new SuppressionArchCondition<>(
260 SUPPRESSED_FIELDS_IN_MODULES, "be suppressed");
261
262 final ArchRule fieldsInStatelessChecksShouldBeImmutable = fields()
263 .that()
264 .areDeclaredInClassesThat()
265 .areAnnotatedWith(StatelessCheck.class)
266 .and(are(not(moduleProperties)))
267 .should(BE_IMMUTABLE)
268 .andShould()
269 .beFinal()
270 .orShould(beSuppressedField);
271
272 fieldsInStatelessChecksShouldBeImmutable.check(CHECKSTYLE_CHECKS);
273 }
274
275
276
277
278 @Test
279 public void testClassesWithImmutableFieldsShouldBeStateless() {
280 final ArchCondition<JavaClass> beSuppressedClass = new SuppressionArchCondition<>(
281 SUPPRESSED_CLASSES_FOR_STATELESS_CHECK_RULE, "be suppressed");
282
283 final ArchRule classesWithImmutableFieldsShouldBeStateless = classes()
284 .that(have(IMMUTABLE_FIELDS))
285 .and()
286 .doNotHaveModifier(JavaModifier.ABSTRACT)
287 .should()
288 .beAnnotatedWith(StatelessCheck.class)
289 .orShould(beSuppressedClass);
290
291 classesWithImmutableFieldsShouldBeStateless.check(CHECKSTYLE_CHECKS);
292 }
293
294
295
296
297
298 @Test
299 public void testClassesWithMutableFieldsShouldBeStateful() {
300 final ArchCondition<JavaClass> beSuppressedClass = new SuppressionArchCondition<>(
301 SUPPRESSED_CLASSES_FOR_STATEFUL_CHECK_RULE, "be suppressed");
302
303 final ArchRule classesWithMutableFieldsShouldBeStateful = classes()
304 .that(doNot(have(IMMUTABLE_FIELDS)))
305 .and()
306 .doNotHaveModifier(JavaModifier.ABSTRACT)
307 .should()
308 .beAnnotatedWith(FileStatefulCheck.class)
309 .orShould()
310 .beAnnotatedWith(GlobalStatefulCheck.class)
311 .orShould(beSuppressedClass);
312
313 classesWithMutableFieldsShouldBeStateful.check(CHECKSTYLE_CHECKS);
314 }
315
316
317
318
319 private static final class ImmutableFieldArchCondition extends ArchCondition<JavaField> {
320 private ImmutableFieldArchCondition() {
321 super("be among immutable types");
322 }
323
324
325
326
327
328
329
330 private static boolean isRawTypeImmutable(JavaField javaField) {
331 final JavaClass rawType = javaField.getRawType();
332 final String rawTypeName = rawType.getName();
333 return PRIMITIVE_TYPES.contains(rawTypeName)
334 || IMMUTABLE_TYPES.contains(rawTypeName);
335 }
336
337
338
339
340
341
342
343 private static boolean isEnumConstantOrEmptyArray(JavaField javaField) {
344 final JavaClass rawType = javaField.getRawType();
345 return rawType.isEnum()
346 || ZERO_SIZE_ARRAY_FIELDS.contains(javaField.getFullName());
347 }
348
349
350
351
352
353
354
355
356
357
358
359 private static boolean isParameterizedTypeImmutable(JavaField javaField) {
360 boolean isParameterizedTypeImmutable = false;
361 final JavaType javaType = javaField.getType();
362
363 if (javaType instanceof JavaParameterizedType) {
364 final JavaParameterizedType parameterizedType = (JavaParameterizedType) javaType;
365 isParameterizedTypeImmutable = parameterizedType.getActualTypeArguments().stream()
366 .allMatch(actualTypeArgument -> {
367 return IMMUTABLE_TYPES.contains(actualTypeArgument.toErasure().getName());
368 });
369 }
370 return isParameterizedTypeImmutable;
371 }
372
373 @Override
374 public void check(JavaField item, ConditionEvents events) {
375 if (!isRawTypeImmutable(item)
376 && !isEnumConstantOrEmptyArray(item)
377 && !isParameterizedTypeImmutable(item)) {
378 final String message = String
379 .format(Locale.ROOT, "Field <%s> should %s in %s",
380 item.getFullName(), getDescription(),
381 item.getSourceCodeLocation());
382 events.add(SimpleConditionEvent.violated(item, message));
383 }
384 }
385 }
386
387
388
389
390 private static final class ModulePropertyPredicate extends DescribedPredicate<JavaField> {
391
392 private ModulePropertyPredicate() {
393 super("module properties");
394 }
395
396
397
398
399
400
401
402 private static boolean isModuleProperty(JavaField javaField) {
403 boolean result = false;
404 final JavaClass containingClass = javaField.getOwner();
405 final ModuleDetails moduleDetails = MODULE_DETAILS_MAP.get(
406 containingClass.getFullName());
407 if (moduleDetails != null) {
408 final List<ModulePropertyDetails> properties = moduleDetails.getProperties();
409 result = properties.stream()
410 .map(ModulePropertyDetails::getName)
411 .anyMatch(moduleName -> moduleName.equals(javaField.getName()));
412 }
413 return result;
414 }
415
416 @Override
417 public boolean test(JavaField input) {
418 return isModuleProperty(input);
419 }
420 }
421
422
423
424
425 private static final class ImmutableFieldsPredicate extends DescribedPredicate<JavaClass> {
426 private ImmutableFieldsPredicate() {
427 super("immutable fields");
428 }
429
430 @Override
431 public boolean test(JavaClass input) {
432 final Set<JavaField> fields = input.getFields();
433 return fields.stream()
434 .filter(javaField -> {
435 return !ModulePropertyPredicate.isModuleProperty(javaField)
436 && !SUPPRESSED_FIELDS_IN_MODULES.contains(javaField.getFullName());
437 })
438 .allMatch(javaField -> {
439 final Set<JavaModifier> javaFieldModifiers = javaField.getModifiers();
440 return javaFieldModifiers.contains(JavaModifier.FINAL)
441 && (ImmutableFieldArchCondition.isRawTypeImmutable(javaField)
442 || ImmutableFieldArchCondition.isEnumConstantOrEmptyArray(javaField)
443 || ImmutableFieldArchCondition.isParameterizedTypeImmutable(javaField));
444 });
445 }
446 }
447
448
449
450
451 private static final class SuppressionArchCondition<T extends HasName.AndFullName>
452 extends ArchCondition<T> {
453
454 private final Set<String> suppressions;
455
456 private SuppressionArchCondition(Set<String> suppressions, String description) {
457 super(description);
458 this.suppressions = suppressions;
459 }
460
461 @Override
462 public void check(HasName.AndFullName item, ConditionEvents events) {
463 if (!suppressions.contains(item.getFullName())) {
464 final String message = String.format(
465 Locale.ROOT, "should %s or resolved.", getDescription());
466 events.add(SimpleConditionEvent.violated(item, message));
467 }
468 }
469 }
470 }