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.checks.design;
21
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.regex.Pattern;
28 import java.util.stream.Collectors;
29
30 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
32 import com.puppycrawl.tools.checkstyle.api.DetailAST;
33 import com.puppycrawl.tools.checkstyle.api.FullIdent;
34 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
36 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
37 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121 @FileStatefulCheck
122 public class VisibilityModifierCheck
123 extends AbstractCheck {
124
125
126
127
128
129 public static final String MSG_KEY = "variable.notPrivate";
130
131
132 private static final Set<String> DEFAULT_IMMUTABLE_TYPES = Set.of(
133 "java.lang.String",
134 "java.lang.Integer",
135 "java.lang.Byte",
136 "java.lang.Character",
137 "java.lang.Short",
138 "java.lang.Boolean",
139 "java.lang.Long",
140 "java.lang.Double",
141 "java.lang.Float",
142 "java.lang.StackTraceElement",
143 "java.math.BigInteger",
144 "java.math.BigDecimal",
145 "java.io.File",
146 "java.util.Locale",
147 "java.util.UUID",
148 "java.net.URL",
149 "java.net.URI",
150 "java.net.Inet4Address",
151 "java.net.Inet6Address",
152 "java.net.InetSocketAddress"
153 );
154
155
156 private static final Set<String> DEFAULT_IGNORE_ANNOTATIONS = Set.of(
157 "org.junit.Rule",
158 "org.junit.ClassRule",
159 "com.google.common.annotations.VisibleForTesting"
160 );
161
162
163 private static final String PUBLIC_ACCESS_MODIFIER = "public";
164
165
166 private static final String PRIVATE_ACCESS_MODIFIER = "private";
167
168
169 private static final String PROTECTED_ACCESS_MODIFIER = "protected";
170
171
172 private static final String PACKAGE_ACCESS_MODIFIER = "package";
173
174
175 private static final String STATIC_KEYWORD = "static";
176
177
178 private static final String FINAL_KEYWORD = "final";
179
180
181 private static final String[] EXPLICIT_MODS = {
182 PUBLIC_ACCESS_MODIFIER,
183 PRIVATE_ACCESS_MODIFIER,
184 PROTECTED_ACCESS_MODIFIER,
185 };
186
187
188
189
190 private Pattern publicMemberPattern = Pattern.compile("^serialVersionUID$");
191
192
193 private Set<String> ignoreAnnotationShortNames;
194
195
196 private Set<String> immutableClassShortNames;
197
198
199
200
201
202 private Set<String> ignoreAnnotationCanonicalNames = DEFAULT_IGNORE_ANNOTATIONS;
203
204
205 private boolean protectedAllowed;
206
207
208 private boolean packageAllowed;
209
210
211 private boolean allowPublicImmutableFields;
212
213
214 private boolean allowPublicFinalFields;
215
216
217 private Set<String> immutableClassCanonicalNames = DEFAULT_IMMUTABLE_TYPES;
218
219
220
221
222
223
224
225
226 public void setIgnoreAnnotationCanonicalNames(String... annotationNames) {
227 ignoreAnnotationCanonicalNames = Set.of(annotationNames);
228 }
229
230
231
232
233
234
235
236 public void setProtectedAllowed(boolean protectedAllowed) {
237 this.protectedAllowed = protectedAllowed;
238 }
239
240
241
242
243
244
245
246 public void setPackageAllowed(boolean packageAllowed) {
247 this.packageAllowed = packageAllowed;
248 }
249
250
251
252
253
254
255
256
257 public void setPublicMemberPattern(Pattern pattern) {
258 publicMemberPattern = pattern;
259 }
260
261
262
263
264
265
266
267 public void setAllowPublicImmutableFields(boolean allow) {
268 allowPublicImmutableFields = allow;
269 }
270
271
272
273
274
275
276
277 public void setAllowPublicFinalFields(boolean allow) {
278 allowPublicFinalFields = allow;
279 }
280
281
282
283
284
285
286
287 public void setImmutableClassCanonicalNames(String... classNames) {
288 immutableClassCanonicalNames = Set.of(classNames);
289 }
290
291 @Override
292 public int[] getDefaultTokens() {
293 return getRequiredTokens();
294 }
295
296 @Override
297 public int[] getAcceptableTokens() {
298 return getRequiredTokens();
299 }
300
301 @Override
302 public int[] getRequiredTokens() {
303 return new int[] {
304 TokenTypes.VARIABLE_DEF,
305 TokenTypes.IMPORT,
306 };
307 }
308
309 @Override
310 public void beginTree(DetailAST rootAst) {
311 immutableClassShortNames = getClassShortNames(immutableClassCanonicalNames);
312 ignoreAnnotationShortNames = getClassShortNames(ignoreAnnotationCanonicalNames);
313 }
314
315 @Override
316 public void visitToken(DetailAST ast) {
317 switch (ast.getType()) {
318 case TokenTypes.VARIABLE_DEF -> {
319 if (!isAnonymousClassVariable(ast)) {
320 visitVariableDef(ast);
321 }
322 }
323 case TokenTypes.IMPORT -> visitImport(ast);
324 default -> {
325 final String exceptionMsg = "Unexpected token type: " + ast.getText();
326 throw new IllegalArgumentException(exceptionMsg);
327 }
328 }
329 }
330
331
332
333
334
335
336
337 private static boolean isAnonymousClassVariable(DetailAST variableDef) {
338 return variableDef.getParent().getType() != TokenTypes.OBJBLOCK;
339 }
340
341
342
343
344
345
346
347 private void visitVariableDef(DetailAST variableDef) {
348 final boolean inInterfaceOrAnnotationBlock =
349 ScopeUtil.isInInterfaceOrAnnotationBlock(variableDef);
350
351 if (!inInterfaceOrAnnotationBlock && !hasIgnoreAnnotation(variableDef)) {
352 final DetailAST varNameAST = variableDef.findFirstToken(TokenTypes.TYPE)
353 .getNextSibling();
354 final String varName = varNameAST.getText();
355 if (!hasProperAccessModifier(variableDef, varName)) {
356 log(varNameAST, MSG_KEY, varName);
357 }
358 }
359 }
360
361
362
363
364
365
366
367 private boolean hasIgnoreAnnotation(DetailAST variableDef) {
368 final DetailAST firstIgnoreAnnotation =
369 findMatchingAnnotation(variableDef);
370 return firstIgnoreAnnotation != null;
371 }
372
373
374
375
376
377
378
379
380 private void visitImport(DetailAST importAst) {
381 if (!isStarImport(importAst)) {
382 final String canonicalName = getCanonicalName(importAst);
383 final String shortName = getClassShortName(canonicalName);
384
385
386
387
388 if (!immutableClassCanonicalNames.contains(canonicalName)) {
389 immutableClassShortNames.remove(shortName);
390 }
391 if (!ignoreAnnotationCanonicalNames.contains(canonicalName)) {
392 ignoreAnnotationShortNames.remove(shortName);
393 }
394 }
395 }
396
397
398
399
400
401
402
403
404
405
406
407
408
409 private static boolean isStarImport(DetailAST importAst) {
410 boolean result = false;
411 DetailAST toVisit = importAst;
412 while (toVisit != null) {
413 toVisit = getNextSubTreeNode(toVisit, importAst);
414 if (toVisit != null && toVisit.getType() == TokenTypes.STAR) {
415 result = true;
416 break;
417 }
418 }
419 return result;
420 }
421
422
423
424
425
426
427
428
429 private boolean hasProperAccessModifier(DetailAST variableDef, String variableName) {
430 boolean result = true;
431
432 final String variableScope = getVisibilityScope(variableDef);
433
434 if (!PRIVATE_ACCESS_MODIFIER.equals(variableScope)) {
435 result =
436 isStaticFinalVariable(variableDef)
437 || packageAllowed && PACKAGE_ACCESS_MODIFIER.equals(variableScope)
438 || protectedAllowed && PROTECTED_ACCESS_MODIFIER.equals(variableScope)
439 || isIgnoredPublicMember(variableName, variableScope)
440 || isAllowedPublicField(variableDef);
441 }
442
443 return result;
444 }
445
446
447
448
449
450
451
452 private static boolean isStaticFinalVariable(DetailAST variableDef) {
453 final Set<String> modifiers = getModifiers(variableDef);
454 return modifiers.contains(STATIC_KEYWORD)
455 && modifiers.contains(FINAL_KEYWORD);
456 }
457
458
459
460
461
462
463
464
465 private boolean isIgnoredPublicMember(String variableName, String variableScope) {
466 return PUBLIC_ACCESS_MODIFIER.equals(variableScope)
467 && publicMemberPattern.matcher(variableName).find();
468 }
469
470
471
472
473
474
475
476 private boolean isAllowedPublicField(DetailAST variableDef) {
477 return allowPublicFinalFields && isFinalField(variableDef)
478 || allowPublicImmutableFields && isImmutableFieldDefinedInFinalClass(variableDef);
479 }
480
481
482
483
484
485
486
487 private boolean isImmutableFieldDefinedInFinalClass(DetailAST variableDef) {
488 final DetailAST classDef = variableDef.getParent().getParent();
489 final Set<String> classModifiers = getModifiers(classDef);
490 return (classModifiers.contains(FINAL_KEYWORD) || classDef.getType() == TokenTypes.ENUM_DEF)
491 && isImmutableField(variableDef);
492 }
493
494
495
496
497
498
499
500 private static Set<String> getModifiers(DetailAST defAST) {
501 final DetailAST modifiersAST = defAST.findFirstToken(TokenTypes.MODIFIERS);
502 final Set<String> modifiersSet = new HashSet<>();
503 if (modifiersAST != null) {
504 DetailAST modifier = modifiersAST.getFirstChild();
505 while (modifier != null) {
506 modifiersSet.add(modifier.getText());
507 modifier = modifier.getNextSibling();
508 }
509 }
510 return modifiersSet;
511 }
512
513
514
515
516
517
518
519 private static String getVisibilityScope(DetailAST variableDef) {
520 final Set<String> modifiers = getModifiers(variableDef);
521 String accessModifier = PACKAGE_ACCESS_MODIFIER;
522 for (final String modifier : EXPLICIT_MODS) {
523 if (modifiers.contains(modifier)) {
524 accessModifier = modifier;
525 break;
526 }
527 }
528 return accessModifier;
529 }
530
531
532
533
534
535
536
537
538
539
540
541 private boolean isImmutableField(DetailAST variableDef) {
542 boolean result = false;
543 if (isFinalField(variableDef)) {
544 final DetailAST type = variableDef.findFirstToken(TokenTypes.TYPE);
545 final boolean isCanonicalName = isCanonicalName(type);
546 final String typeName = getCanonicalName(type);
547 if (immutableClassShortNames.contains(typeName)
548 || isCanonicalName && immutableClassCanonicalNames.contains(typeName)) {
549 final DetailAST typeArgs = getGenericTypeArgs(type, isCanonicalName);
550
551 if (typeArgs == null) {
552 result = true;
553 }
554 else {
555 final List<String> argsClassNames = getTypeArgsClassNames(typeArgs);
556 result = areImmutableTypeArguments(argsClassNames);
557 }
558 }
559 else {
560 result = !isCanonicalName && isPrimitive(type);
561 }
562 }
563 return result;
564 }
565
566
567
568
569
570
571
572 private static boolean isCanonicalName(DetailAST type) {
573 return type.getFirstChild().getType() == TokenTypes.DOT;
574 }
575
576
577
578
579
580
581
582
583 private static DetailAST getGenericTypeArgs(DetailAST type, boolean isCanonicalName) {
584 final DetailAST typeArgs;
585 if (isCanonicalName) {
586
587 typeArgs = type.getFirstChild().findFirstToken(TokenTypes.TYPE_ARGUMENTS);
588 }
589 else {
590 typeArgs = type.findFirstToken(TokenTypes.TYPE_ARGUMENTS);
591 }
592 return typeArgs;
593 }
594
595
596
597
598
599
600
601 private static List<String> getTypeArgsClassNames(DetailAST typeArgs) {
602 final List<String> typeClassNames = new ArrayList<>();
603 DetailAST type = typeArgs.findFirstToken(TokenTypes.TYPE_ARGUMENT);
604 DetailAST sibling;
605 do {
606 final String typeName = getCanonicalName(type);
607 typeClassNames.add(typeName);
608 sibling = type.getNextSibling();
609 type = sibling.getNextSibling();
610 } while (sibling.getType() == TokenTypes.COMMA);
611 return typeClassNames;
612 }
613
614
615
616
617
618
619
620
621
622 private boolean areImmutableTypeArguments(Collection<String> typeArgsClassNames) {
623 return typeArgsClassNames.stream().noneMatch(
624 typeName -> {
625 return !immutableClassShortNames.contains(typeName)
626 && !immutableClassCanonicalNames.contains(typeName);
627 });
628 }
629
630
631
632
633
634
635
636 private static boolean isFinalField(DetailAST variableDef) {
637 final DetailAST modifiers = variableDef.findFirstToken(TokenTypes.MODIFIERS);
638 return modifiers.findFirstToken(TokenTypes.FINAL) != null;
639 }
640
641
642
643
644
645
646
647
648
649
650
651 private static boolean isPrimitive(DetailAST type) {
652 return type.getFirstChild().getType() != TokenTypes.IDENT;
653 }
654
655
656
657
658
659
660
661 private static String getCanonicalName(DetailAST type) {
662 final StringBuilder canonicalNameBuilder = new StringBuilder(256);
663 DetailAST toVisit = type;
664 while (toVisit != null) {
665 toVisit = getNextSubTreeNode(toVisit, type);
666 if (toVisit != null && toVisit.getType() == TokenTypes.IDENT) {
667 if (!canonicalNameBuilder.isEmpty()) {
668 canonicalNameBuilder.append('.');
669 }
670 canonicalNameBuilder.append(toVisit.getText());
671 final DetailAST nextSubTreeNode = getNextSubTreeNode(toVisit, type);
672 if (nextSubTreeNode != null
673 && nextSubTreeNode.getType() == TokenTypes.TYPE_ARGUMENTS) {
674 break;
675 }
676 }
677 }
678 return canonicalNameBuilder.toString();
679 }
680
681
682
683
684
685
686
687
688
689
690 private static DetailAST
691 getNextSubTreeNode(DetailAST currentNodeAst, DetailAST subTreeRootAst) {
692 DetailAST currentNode = currentNodeAst;
693 DetailAST toVisitAst = currentNode.getFirstChild();
694 while (toVisitAst == null) {
695 toVisitAst = currentNode.getNextSibling();
696 if (currentNode.getParent().getColumnNo() == subTreeRootAst.getColumnNo()) {
697 break;
698 }
699 currentNode = currentNode.getParent();
700 }
701 return toVisitAst;
702 }
703
704
705
706
707
708
709
710 private static Set<String> getClassShortNames(Set<String> canonicalClassNames) {
711 return canonicalClassNames.stream()
712 .map(CommonUtil::baseClassName)
713 .collect(Collectors.toCollection(HashSet::new));
714 }
715
716
717
718
719
720
721
722 private static String getClassShortName(String canonicalClassName) {
723 return canonicalClassName
724 .substring(canonicalClassName.lastIndexOf('.') + 1);
725 }
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750 private DetailAST findMatchingAnnotation(DetailAST variableDef) {
751 DetailAST matchingAnnotation = null;
752
753 final DetailAST holder = AnnotationUtil.getAnnotationHolder(variableDef);
754
755 for (DetailAST child = holder.getFirstChild();
756 child != null; child = child.getNextSibling()) {
757 if (child.getType() == TokenTypes.ANNOTATION) {
758 final DetailAST ast = child.getFirstChild();
759 final String name =
760 FullIdent.createFullIdent(ast.getNextSibling()).getText();
761 if (ignoreAnnotationCanonicalNames.contains(name)
762 || ignoreAnnotationShortNames.contains(name)) {
763 matchingAnnotation = child;
764 break;
765 }
766 }
767 }
768
769 return matchingAnnotation;
770 }
771
772 }