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.utils;
021
022import java.util.Set;
023import java.util.function.Predicate;
024
025import com.puppycrawl.tools.checkstyle.api.DetailAST;
026import com.puppycrawl.tools.checkstyle.api.FullIdent;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028
029/**
030 * Contains utility methods designed to work with annotations.
031 *
032 */
033public final class AnnotationUtil {
034
035    /**
036     * Common message.
037     */
038    private static final String THE_AST_IS_NULL = "the ast is null";
039
040    /** {@link Override Override} annotation name. */
041    private static final String OVERRIDE = "Override";
042
043    /** Fully-qualified {@link Override Override} annotation name. */
044    private static final String FQ_OVERRIDE = "java.lang." + OVERRIDE;
045
046    /** Simple and fully-qualified {@link Override Override} annotation names. */
047    private static final Set<String> OVERRIDE_ANNOTATIONS = Set.of(OVERRIDE, FQ_OVERRIDE);
048
049    /**
050     * Private utility constructor.
051     *
052     * @throws UnsupportedOperationException if called
053     */
054    private AnnotationUtil() {
055        throw new UnsupportedOperationException("do not instantiate.");
056    }
057
058    /**
059     * Checks if the AST is annotated with any annotation.
060     *
061     * @param ast the current node
062     * @return {@code true} if the AST contains at least one annotation
063     * @throws IllegalArgumentException when ast is null
064     */
065    public static boolean containsAnnotation(final DetailAST ast) {
066        final DetailAST holder = getAnnotationHolder(ast);
067        return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null;
068    }
069
070    /**
071     * Checks if the AST is annotated with the passed in annotation.
072     *
073     * <p>
074     * This method will not look for imports or package
075     * statements to detect the passed in annotation.
076     * </p>
077     *
078     * <p>
079     * To check if an AST contains a passed in annotation
080     * taking into account fully-qualified names
081     * (ex: java.lang.Override, Override)
082     * this method will need to be called twice. Once for each
083     * name given.
084     * </p>
085     *
086     * @param ast the current node
087     * @param annotation the annotation name to check for
088     * @return true if contains the annotation
089     */
090    public static boolean containsAnnotation(final DetailAST ast,
091        String annotation) {
092        return getAnnotation(ast, annotation) != null;
093    }
094
095    /**
096     * Checks if the given AST element is annotated with any of the specified annotations.
097     *
098     * <p>
099     * This method accepts both simple and fully-qualified names,
100     * e.g. "Override" will match both java.lang.Override and Override.
101     * </p>
102     *
103     * @param ast The type or method definition.
104     * @param annotations A collection of annotations to look for.
105     * @return {@code true} if the given AST element is annotated with
106     *                      at least one of the specified annotations;
107     *                      {@code false} otherwise.
108     * @throws IllegalArgumentException when ast or annotations are null
109     */
110    public static boolean containsAnnotation(DetailAST ast, Set<String> annotations) {
111        if (annotations == null) {
112            throw new IllegalArgumentException("annotations cannot be null");
113        }
114        boolean result = false;
115        if (!annotations.isEmpty()) {
116            final DetailAST firstMatchingAnnotation = findFirstAnnotation(ast, annotationNode -> {
117                final String annotationFullIdent = getAnnotationFullIdent(annotationNode);
118                return annotations.contains(annotationFullIdent);
119            });
120            result = firstMatchingAnnotation != null;
121        }
122        return result;
123    }
124
125    /**
126     * Gets the full ident text of the annotation AST.
127     *
128     * @param annotationNode The annotation AST.
129     * @return The full ident text.
130     */
131    public static String getAnnotationFullIdent(DetailAST annotationNode) {
132        final DetailAST identNode = annotationNode.findFirstToken(TokenTypes.IDENT);
133        final String annotationString;
134
135        // If no `IDENT` is found, then we have a `DOT` -> more than 1 qualifier
136        if (identNode == null) {
137            final DetailAST dotNode = annotationNode.findFirstToken(TokenTypes.DOT);
138            annotationString = FullIdent.createFullIdent(dotNode).getText();
139        }
140        else {
141            annotationString = identNode.getText();
142        }
143
144        return annotationString;
145    }
146
147    /**
148     * Checks if the AST is annotated with {@code Override} or
149     * {@code java.lang.Override} annotation.
150     *
151     * @param ast the current node
152     * @return {@code true} if the AST contains Override annotation
153     * @throws IllegalArgumentException when ast is null
154     */
155    public static boolean hasOverrideAnnotation(DetailAST ast) {
156        return containsAnnotation(ast, OVERRIDE_ANNOTATIONS);
157    }
158
159    /**
160     * Gets the AST that holds a series of annotations for the
161     * potentially annotated AST.  Returns {@code null}
162     * if the passed in AST does not have an Annotation Holder.
163     *
164     * @param ast the current node
165     * @return the Annotation Holder
166     * @throws IllegalArgumentException when ast is null
167     */
168    public static DetailAST getAnnotationHolder(DetailAST ast) {
169        if (ast == null) {
170            throw new IllegalArgumentException(THE_AST_IS_NULL);
171        }
172
173        final DetailAST annotationHolder;
174
175        if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF
176            || ast.getType() == TokenTypes.PACKAGE_DEF
177            || ast.getType() == TokenTypes.MODULE_DEF) {
178            annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS);
179        }
180        else {
181            annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS);
182        }
183
184        return annotationHolder;
185    }
186
187    /**
188     * Checks if the AST is annotated with the passed in annotation
189     * and returns the AST representing that annotation.
190     *
191     * <p>
192     * This method will not look for imports or package
193     * statements to detect the passed in annotation.
194     * </p>
195     *
196     * <p>
197     * To check if an AST contains a passed in annotation
198     * taking into account fully-qualified names
199     * (ex: java.lang.Override, Override)
200     * this method will need to be called twice. Once for each
201     * name given.
202     * </p>
203     *
204     * @param ast the current node
205     * @param annotation the annotation name to check for
206     * @return the AST representing that annotation
207     * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank
208     */
209    public static DetailAST getAnnotation(final DetailAST ast,
210        String annotation) {
211        if (ast == null) {
212            throw new IllegalArgumentException(THE_AST_IS_NULL);
213        }
214
215        if (annotation == null) {
216            throw new IllegalArgumentException("the annotation is null");
217        }
218
219        if (CommonUtil.isBlank(annotation)) {
220            throw new IllegalArgumentException(
221                    "the annotation is empty or spaces");
222        }
223
224        return findFirstAnnotation(ast, annotationNode -> {
225            final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT);
226            final String name =
227                    FullIdent.createFullIdent(firstChild.getNextSibling()).getText();
228            return annotation.equals(name);
229        });
230    }
231
232    /**
233     * Checks if the given AST is annotated with at least one annotation that
234     * matches the given predicate and returns the AST representing the first
235     * matching annotation.
236     *
237     * <p>
238     * This method will not look for imports or package
239     * statements to detect the passed in annotation.
240     * </p>
241     *
242     * @param ast the current node
243     * @param predicate The predicate which decides if an annotation matches
244     * @return the AST representing that annotation
245     */
246    private static DetailAST findFirstAnnotation(final DetailAST ast,
247                                                 Predicate<DetailAST> predicate) {
248        final DetailAST holder = getAnnotationHolder(ast);
249        DetailAST result = null;
250        for (DetailAST child = holder.getFirstChild();
251            child != null; child = child.getNextSibling()) {
252            if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) {
253                result = child;
254                break;
255            }
256        }
257
258        return result;
259    }
260
261}