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