001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.modifier;
021
022import java.util.Arrays;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.Set;
027
028import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.FullIdent;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
034import com.puppycrawl.tools.checkstyle.utils.NullUtil;
035
036/**
037 * <div>
038 * Checks that elements annotated with specified annotations
039 * have only allowed visibility modifiers.
040 * </div>
041 *
042 * <p>
043 * This check enforces consistency between annotation presence and
044 * declared visibility. If a configured annotation is found on a target
045 * element, its visibility modifier must match one of the allowed values.
046 * </p>
047 *
048 * @since 13.4.0
049 */
050@FileStatefulCheck
051public class AnnotatedMethodVisibilityModifierCheck extends AbstractCheck {
052
053    /**
054     * Message key for violation.
055     */
056    public static final String MSG_KEY = "annotated.visibility.modifier";
057
058    /**
059     * "protected" visibility string.
060     */
061    private static final String PROTECTED = "protected";
062
063    /**
064     * "public" visibility string.
065     */
066    private static final String PUBLIC = "public";
067
068    /**
069     * "private" visibility string.
070     */
071    private static final String PRIVATE = "private";
072
073    /**
074     * "package-private" visibility string.
075     */
076    private static final String PACKAGE_PRIVATE = "package-private";
077
078    /**
079     * Dot.
080     */
081    private static final char DOT = '.';
082
083    /**
084     * Configured annotation canonical names.
085     */
086    private final Set<String> annotations = new HashSet<>(
087            Set.of("com.google.common.annotations.VisibleForTesting"));
088
089    /**
090     * Allowed visibility values.
091     * Acceptable values: public, protected, package-private, private.
092     */
093    private final Set<String> visibility = new HashSet<>(
094            Set.of(PROTECTED, PACKAGE_PRIVATE));
095
096    /**
097     * Set of non star imports.
098     */
099    private final Map<String, String> importedAnnotations = new HashMap<>();
100
101    /**
102     * Set of star imports.
103     */
104    private final Set<String> starImports = new HashSet<>();
105
106    /**
107     * Current package.
108     */
109    private String currentPackage;
110
111    /**
112     * Setter for annotation canonical names.
113     *
114     * @param values comma-separated fully qualified annotation names
115     * @since 13.4.0
116     */
117    public void setAnnotations(String... values) {
118        annotations.clear();
119        annotations.addAll(Arrays.asList(values));
120    }
121
122    /**
123     * Setter for allowed visibility modifiers.
124     * Allowed values:
125     * public, protected, private, package-private
126     *
127     * @param values comma-separated visibility names
128     * @since 13.4.0
129     */
130    public void setVisibility(String... values) {
131        visibility.clear();
132        visibility.addAll(Arrays.asList(values));
133    }
134
135    @Override
136    public void beginTree(DetailAST rootAST) {
137        importedAnnotations.clear();
138        starImports.clear();
139    }
140
141    @Override
142    public int[] getDefaultTokens() {
143        return getAcceptableTokens();
144    }
145
146    @Override
147    public int[] getRequiredTokens() {
148        return new int[] {
149            // annotation name resolution
150            TokenTypes.PACKAGE_DEF,
151            TokenTypes.IMPORT,
152        };
153    }
154
155    @Override
156    public int[] getAcceptableTokens() {
157        return new int[] {
158            // annotation name resolution
159            TokenTypes.PACKAGE_DEF,
160            TokenTypes.IMPORT,
161            // tokens that can have annotations
162            TokenTypes.CLASS_DEF,
163            TokenTypes.INTERFACE_DEF,
164            TokenTypes.ENUM_DEF,
165            TokenTypes.RECORD_DEF,
166            TokenTypes.METHOD_DEF,
167            TokenTypes.CTOR_DEF,
168            TokenTypes.VARIABLE_DEF,
169            TokenTypes.ANNOTATION_DEF,
170        };
171    }
172
173    @Override
174    public void visitToken(DetailAST ast) {
175        switch (ast.getType()) {
176            case TokenTypes.PACKAGE_DEF -> handlePackage(ast);
177            case TokenTypes.IMPORT -> handleImport(ast);
178            default -> checkAnnotatedVisibility(ast);
179        }
180    }
181
182    /**
183     * Handles package declarations and stores the current package name.
184     *
185     * @param ast package definition node
186     */
187    private void handlePackage(DetailAST ast) {
188        currentPackage =
189                FullIdent.createFullIdent(ast.getLastChild().getPreviousSibling()).getText();
190    }
191
192    /**
193     * Processes import statements and records imported annotations.
194     *
195     * @param ast import node
196     */
197    private void handleImport(DetailAST ast) {
198        final String importText = FullIdent.createFullIdentBelow(ast).getText();
199        if (importText.endsWith(".*")) {
200            starImports.add(importText.substring(0, importText.length() - 2));
201        }
202        else {
203            final int lastDot = importText.lastIndexOf(DOT);
204            final String simple = importText.substring(lastDot + 1);
205            importedAnnotations.put(simple, importText);
206        }
207    }
208
209    /**
210     * Checks the visibility of annotated elements.
211     *
212     * @param ast AST node to inspect
213     */
214    private void checkAnnotatedVisibility(DetailAST ast) {
215        if (ast.getParent().getType() == TokenTypes.OBJBLOCK
216                && hasConfiguredAnnotation(ast)) {
217            final VisibilityInfo info = getVisibilityInfo(ast);
218            if (!visibility.contains(info.visibility())) {
219                log(info.node(), MSG_KEY, info.visibility());
220            }
221        }
222    }
223
224    /**
225     * Determines whether the AST node contains a configured annotation.
226     *
227     * @param ast AST node to inspect
228     * @return true if the annotation is present
229     */
230    private boolean hasConfiguredAnnotation(DetailAST ast) {
231        boolean result = false;
232        final DetailAST modifiers =
233                NullUtil.notNull(ast.findFirstToken(TokenTypes.MODIFIERS));
234        DetailAST child = modifiers.getFirstChild();
235        while (child != null) {
236            final String annotationText =
237                    AnnotationUtil.getAnnotationFullIdent(child);
238            final String resolved = resolveAnnotation(annotationText);
239            if (annotations.contains(resolved)) {
240                result = true;
241            }
242            child = child.getNextSibling();
243        }
244        return result;
245    }
246
247    /**
248     * Resolves the fully qualified name of an annotation.
249     *
250     * @param name annotation name
251     * @return resolved canonical name
252     */
253    private String resolveAnnotation(String name) {
254        String result = name;
255        final String pkgCandidate = currentPackage + DOT + name;
256        final String importCandidate = importedAnnotations.get(name);
257        final String javaLangCandidate = "java.lang." + name;
258        if (annotations.contains(pkgCandidate)) {
259            result = pkgCandidate;
260        }
261        else if (importCandidate != null) {
262            result = importCandidate;
263        }
264        else if (annotations.contains(javaLangCandidate)) {
265            result = javaLangCandidate;
266        }
267        else {
268            for (String starImport : starImports) {
269                final String starCandidate = starImport + DOT + name;
270                if (annotations.contains(starCandidate)) {
271                    result = starCandidate;
272                }
273            }
274        }
275        return result;
276    }
277
278    /**
279     * Extracts visibility information from the AST node.
280     *
281     * @param ast node to inspect
282     * @return visibility information record
283     */
284    private static VisibilityInfo getVisibilityInfo(DetailAST ast) {
285        final DetailAST modifiers =
286                NullUtil.notNull(ast.findFirstToken(TokenTypes.MODIFIERS));
287        String visibility = PACKAGE_PRIVATE;
288        DetailAST node = NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT));
289        final DetailAST publicModifier = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC);
290        final DetailAST protectedModifier = modifiers.findFirstToken(TokenTypes.LITERAL_PROTECTED);
291        final DetailAST privateModifier = modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE);
292        if (publicModifier != null) {
293            visibility = PUBLIC;
294            node = publicModifier;
295        }
296        else if (protectedModifier != null) {
297            visibility = PROTECTED;
298            node = protectedModifier;
299        }
300        else if (privateModifier != null) {
301            visibility = PRIVATE;
302            node = privateModifier;
303        }
304        else if (isInsideInterface(ast)) {
305            visibility = PUBLIC;
306        }
307        return new VisibilityInfo(visibility, node);
308    }
309
310    /**
311     * Determines whether the AST node is inside an interface.
312     *
313     * @param ast node to inspect
314     * @return true if inside interface
315     */
316    private static boolean isInsideInterface(DetailAST ast) {
317        boolean result = false;
318        DetailAST parent = ast.getParent();
319        while (parent != null) {
320            if (parent.getType() == TokenTypes.INTERFACE_DEF) {
321                result = true;
322            }
323            parent = parent.getParent();
324        }
325        return result;
326    }
327
328    /**
329     * Record holding visibility information.
330     *
331     * @param visibility visibility value
332     * @param node AST node where violation should be reported
333     */
334    private record VisibilityInfo(String visibility, DetailAST node) {
335
336    }
337
338}