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.checks.naming.AccessModifierOption;
034import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
035import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
036import com.puppycrawl.tools.checkstyle.utils.NullUtil;
037
038/**
039 * <div>
040 * Checks that elements annotated with specified annotations
041 * have only allowed visibility modifiers.
042 * </div>
043 *
044 * <p>
045 * This check enforces consistency between annotation presence and
046 * declared visibility. If a configured annotation is found on a target
047 * element, its visibility modifier must match one of the allowed values.
048 * </p>
049 *
050 * @since 13.9.0
051 */
052@FileStatefulCheck
053public class AnnotatedDeclarationVisibilityCheck extends AbstractCheck {
054
055    /**
056     * Message key for violation.
057     */
058    public static final String MSG_KEY = "annotated.visibility.modifier";
059
060    /**
061     * Dot.
062     */
063    private static final char DOT = '.';
064
065    /**
066     * Configured annotation canonical names.
067     */
068    private final Set<String> annotations = new HashSet<>(
069            Set.of("com.google.common.annotations.VisibleForTesting"));
070
071    /**
072     * Set of non star imports.
073     */
074    private final Map<String, String> importedAnnotations = new HashMap<>();
075
076    /**
077     * Set of star imports.
078     */
079    private final Set<String> starImports = new HashSet<>();
080
081    /**
082     * Allowed visibility values.
083     */
084    private AccessModifierOption[] visibility = {
085        AccessModifierOption.PROTECTED,
086        AccessModifierOption.PACKAGE,
087    };
088
089    /**
090     * Current package.
091     */
092    private String currentPackage;
093
094    /**
095     * Creates a new {@code AnnotatedDeclarationVisibilityCheck} instance.
096     */
097    public AnnotatedDeclarationVisibilityCheck() {
098        // no code by default
099    }
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}