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 .haveNameNotContaining("$")
265 .and()
266 .areDeclaredInClassesThat()
267 .areAnnotatedWith(StatelessCheck.class)
268 .and(are(not(moduleProperties)))
269 .should(BE_IMMUTABLE)
270 .andShould()
271 .beFinal()
272 .orShould(beSuppressedField);
273
274 fieldsInStatelessChecksShouldBeImmutable.check(CHECKSTYLE_CHECKS);
275 }
276
277
278
279
280 @Test
281 public void testClassesWithImmutableFieldsShouldBeStateless() {
282 final ArchCondition<JavaClass> beSuppressedClass = new SuppressionArchCondition<>(
283 SUPPRESSED_CLASSES_FOR_STATELESS_CHECK_RULE, "be suppressed");
284
285 final ArchRule classesWithImmutableFieldsShouldBeStateless = classes()
286 .that(have(IMMUTABLE_FIELDS))
287 .and()
288 .doNotHaveModifier(JavaModifier.ABSTRACT)
289 .should()
290 .beAnnotatedWith(StatelessCheck.class)
291 .orShould(beSuppressedClass);
292
293 classesWithImmutableFieldsShouldBeStateless.check(CHECKSTYLE_CHECKS);
294 }
295
296
297
298
299
300 @Test
301 public void testClassesWithMutableFieldsShouldBeStateful() {
302 final ArchCondition<JavaClass> beSuppressedClass = new SuppressionArchCondition<>(
303 SUPPRESSED_CLASSES_FOR_STATEFUL_CHECK_RULE, "be suppressed");
304
305 final ArchRule classesWithMutableFieldsShouldBeStateful = classes()
306 .that(doNot(have(IMMUTABLE_FIELDS)))
307 .and()
308 .doNotHaveModifier(JavaModifier.ABSTRACT)
309 .should()
310 .beAnnotatedWith(FileStatefulCheck.class)
311 .orShould()
312 .beAnnotatedWith(GlobalStatefulCheck.class)
313 .orShould(beSuppressedClass);
314
315 classesWithMutableFieldsShouldBeStateful.check(CHECKSTYLE_CHECKS);
316 }
317
318
319
320
321 private static final class ImmutableFieldArchCondition extends ArchCondition<JavaField> {
322 private ImmutableFieldArchCondition() {
323 super("be among immutable types");
324 }
325
326
327
328
329
330
331
332 private static boolean isRawTypeImmutable(JavaField javaField) {
333 final JavaClass rawType = javaField.getRawType();
334 final String rawTypeName = rawType.getName();
335 return PRIMITIVE_TYPES.contains(rawTypeName)
336 || IMMUTABLE_TYPES.contains(rawTypeName);
337 }
338
339
340
341
342
343
344
345 private static boolean isEnumConstantOrEmptyArray(JavaField javaField) {
346 final JavaClass rawType = javaField.getRawType();
347 return rawType.isEnum()
348 || ZERO_SIZE_ARRAY_FIELDS.contains(javaField.getFullName());
349 }
350
351
352
353
354
355
356
357
358
359
360
361 private static boolean isParameterizedTypeImmutable(JavaField javaField) {
362 boolean isParameterizedTypeImmutable = false;
363 final JavaType javaType = javaField.getType();
364
365 if (javaType instanceof JavaParameterizedType) {
366 final JavaParameterizedType parameterizedType = (JavaParameterizedType) javaType;
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 }