View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.modifier;
21  
22  import java.util.Arrays;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.FullIdent;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
34  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
35  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
36  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
37  
38  /**
39   * <div>
40   * Checks that elements annotated with specified annotations
41   * have only allowed visibility modifiers.
42   * </div>
43   *
44   * <p>
45   * This check enforces consistency between annotation presence and
46   * declared visibility. If a configured annotation is found on a target
47   * element, its visibility modifier must match one of the allowed values.
48   * </p>
49   *
50   * @since 13.9.0
51   */
52  @FileStatefulCheck
53  public class AnnotatedDeclarationVisibilityCheck extends AbstractCheck {
54  
55      /**
56       * Message key for violation.
57       */
58      public static final String MSG_KEY = "annotated.visibility.modifier";
59  
60      /**
61       * Dot.
62       */
63      private static final char DOT = '.';
64  
65      /**
66       * Configured annotation canonical names.
67       */
68      private final Set<String> annotations = new HashSet<>(
69              Set.of("com.google.common.annotations.VisibleForTesting"));
70  
71      /**
72       * Set of non star imports.
73       */
74      private final Map<String, String> importedAnnotations = new HashMap<>();
75  
76      /**
77       * Set of star imports.
78       */
79      private final Set<String> starImports = new HashSet<>();
80  
81      /**
82       * Allowed visibility values.
83       */
84      private AccessModifierOption[] visibility = {
85          AccessModifierOption.PROTECTED,
86          AccessModifierOption.PACKAGE,
87      };
88  
89      /**
90       * Current package.
91       */
92      private String currentPackage;
93  
94      /**
95       * Creates a new {@code AnnotatedDeclarationVisibilityCheck} instance.
96       */
97      public AnnotatedDeclarationVisibilityCheck() {
98          // no code by default
99      }
100 
101     /**
102      * Setter for annotation canonical names.
103      *
104      * @param values comma-separated fully qualified annotation names
105      * @since 13.9.0
106      */
107     public void setAnnotations(String... values) {
108         annotations.clear();
109         annotations.addAll(Arrays.asList(values));
110     }
111 
112     /**
113      * Setter for allowed visibility modifiers.
114      * Allowed values:
115      * public, protected, package, private.
116      *
117      * @param values allowed visibility values
118      * @since 13.9.0
119      */
120     public void setVisibility(AccessModifierOption... values) {
121         visibility = Arrays.copyOf(values, values.length);
122     }
123 
124     @Override
125     public void beginTree(DetailAST rootAST) {
126         currentPackage = "";
127         importedAnnotations.clear();
128         starImports.clear();
129     }
130 
131     @Override
132     public int[] getDefaultTokens() {
133         return getAcceptableTokens();
134     }
135 
136     @Override
137     public int[] getRequiredTokens() {
138         return new int[] {
139             // annotation name resolution
140             TokenTypes.PACKAGE_DEF,
141             TokenTypes.IMPORT,
142         };
143     }
144 
145     @Override
146     public int[] getAcceptableTokens() {
147         return new int[] {
148             // annotation name resolution
149             TokenTypes.PACKAGE_DEF,
150             TokenTypes.IMPORT,
151             // tokens that can have annotations
152             TokenTypes.CLASS_DEF,
153             TokenTypes.INTERFACE_DEF,
154             TokenTypes.ENUM_DEF,
155             TokenTypes.RECORD_DEF,
156             TokenTypes.METHOD_DEF,
157             TokenTypes.CTOR_DEF,
158             TokenTypes.VARIABLE_DEF,
159             TokenTypes.ANNOTATION_DEF,
160         };
161     }
162 
163     @Override
164     public void visitToken(DetailAST ast) {
165         switch (ast.getType()) {
166             case TokenTypes.PACKAGE_DEF -> handlePackage(ast);
167             case TokenTypes.IMPORT -> handleImport(ast);
168             default -> checkAnnotatedVisibility(ast);
169         }
170     }
171 
172     /**
173      * Handles package declarations and stores the current package name.
174      *
175      * @param ast package definition node
176      */
177     private void handlePackage(DetailAST ast) {
178         currentPackage =
179                 FullIdent.createFullIdent(ast.getLastChild().getPreviousSibling()).getText();
180     }
181 
182     /**
183      * Processes import statements and records imported annotations.
184      *
185      * @param ast import node
186      */
187     private void handleImport(DetailAST ast) {
188         final String importText = FullIdent.createFullIdentBelow(ast).getText();
189         if (importText.endsWith(".*")) {
190             starImports.add(importText.substring(0, importText.length() - 2));
191         }
192         else {
193             final int lastDot = importText.lastIndexOf(DOT);
194             final String simple = importText.substring(lastDot + 1);
195             importedAnnotations.put(simple, importText);
196         }
197     }
198 
199     /**
200      * Checks the visibility of annotated elements.
201      *
202      * @param ast AST node to inspect
203      */
204     private void checkAnnotatedVisibility(DetailAST ast) {
205         if ((ast.getType() != TokenTypes.VARIABLE_DEF
206                 || ast.getParent().getType() == TokenTypes.OBJBLOCK)
207                 && hasConfiguredAnnotation(ast)) {
208             final AccessModifierOption accessModifierOption =
209                 CheckUtil.getAccessModifierFromModifiersToken(ast);
210             if (!isAllowedVisibility(accessModifierOption)) {
211                 log(ast, MSG_KEY, getVisibilityName(accessModifierOption));
212             }
213         }
214     }
215 
216     /**
217      * Determines whether the AST node contains a configured annotation.
218      *
219      * @param ast AST node to inspect
220      * @return true if the annotation is present
221      */
222     private boolean hasConfiguredAnnotation(DetailAST ast) {
223         boolean result = false;
224         final DetailAST modifiers =
225                 NullUtil.notNull(ast.findFirstToken(TokenTypes.MODIFIERS));
226         DetailAST child = modifiers.getFirstChild();
227         while (child != null) {
228             final String annotationText =
229                     AnnotationUtil.getAnnotationFullIdent(child);
230             final String resolved = resolveAnnotation(annotationText);
231             if (annotations.contains(resolved)) {
232                 result = true;
233             }
234             child = child.getNextSibling();
235         }
236         return result;
237     }
238 
239     /**
240      * Resolves the fully qualified name of an annotation.
241      *
242      * @param name annotation name
243      * @return resolved canonical name
244      */
245     private String resolveAnnotation(String name) {
246         String result = name;
247         final String pkgCandidate = currentPackage + DOT + name;
248         final String importCandidate = importedAnnotations.get(name);
249         final String javaLangCandidate = "java.lang." + name;
250         if (annotations.contains(pkgCandidate)) {
251             result = pkgCandidate;
252         }
253         else if (importCandidate != null) {
254             result = importCandidate;
255         }
256         else if (annotations.contains(javaLangCandidate)) {
257             result = javaLangCandidate;
258         }
259         else {
260             for (String starImport : starImports) {
261                 final String starCandidate = starImport + DOT + name;
262                 if (annotations.contains(starCandidate)) {
263                     result = starCandidate;
264                 }
265             }
266         }
267         return result;
268     }
269 
270     /**
271      * Checks whether the given visibility value is allowed.
272      *
273      * @param accessModifier visibility value
274      * @return true if given visibility value is allowed
275      */
276     private boolean isAllowedVisibility(AccessModifierOption accessModifier) {
277         return Arrays.stream(visibility)
278                 .anyMatch(modifier -> modifier == accessModifier);
279     }
280 
281     /**
282      * Gets the visibility value to display in violation messages.
283      *
284      * @param accessModifier visibility value
285      * @return display name of the visibility value
286      */
287     private static String getVisibilityName(AccessModifierOption accessModifier) {
288         final String result;
289         if (accessModifier == AccessModifierOption.PACKAGE) {
290             result = "package-private";
291         }
292         else {
293             result = accessModifier.toString();
294         }
295         return result;
296     }
297 
298 }