001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 the passed in annotation.
060     *
061     * <p>
062     * This method will not look for imports or package
063     * statements to detect the passed in annotation.
064     * </p>
065     *
066     * <p>
067     * To check if an AST contains a passed in annotation
068     * taking into account fully-qualified names
069     * (ex: java.lang.Override, Override)
070     * this method will need to be called twice. Once for each
071     * name given.
072     * </p>
073     *
074     * @param ast the current node
075     * @param annotation the annotation name to check for
076     * @return true if contains the annotation
077     */
078    public static boolean containsAnnotation(final DetailAST ast,
079        String annotation) {
080        return getAnnotation(ast, annotation) != null;
081    }
082
083    /**
084     * Checks if the AST is annotated with any annotation.
085     *
086     * @param ast the current node
087     * @return {@code true} if the AST contains at least one annotation
088     * @throws IllegalArgumentException when ast is null
089     */
090    public static boolean containsAnnotation(final DetailAST ast) {
091        final DetailAST holder = getAnnotationHolder(ast);
092        return holder != null && holder.findFirstToken(TokenTypes.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    private 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            annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS);
178        }
179        else {
180            annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS);
181        }
182
183        return annotationHolder;
184    }
185
186    /**
187     * Checks if the AST is annotated with the passed in annotation
188     * and returns the AST representing that annotation.
189     *
190     * <p>
191     * This method will not look for imports or package
192     * statements to detect the passed in annotation.
193     * </p>
194     *
195     * <p>
196     * To check if an AST contains a passed in annotation
197     * taking into account fully-qualified names
198     * (ex: java.lang.Override, Override)
199     * this method will need to be called twice. Once for each
200     * name given.
201     * </p>
202     *
203     * @param ast the current node
204     * @param annotation the annotation name to check for
205     * @return the AST representing that annotation
206     * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank
207     */
208    public static DetailAST getAnnotation(final DetailAST ast,
209        String annotation) {
210        if (ast == null) {
211            throw new IllegalArgumentException(THE_AST_IS_NULL);
212        }
213
214        if (annotation == null) {
215            throw new IllegalArgumentException("the annotation is null");
216        }
217
218        if (CommonUtil.isBlank(annotation)) {
219            throw new IllegalArgumentException(
220                    "the annotation is empty or spaces");
221        }
222
223        return findFirstAnnotation(ast, annotationNode -> {
224            final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT);
225            final String name =
226                    FullIdent.createFullIdent(firstChild.getNextSibling()).getText();
227            return annotation.equals(name);
228        });
229    }
230
231    /**
232     * Checks if the given AST is annotated with at least one annotation that
233     * matches the given predicate and returns the AST representing the first
234     * matching annotation.
235     *
236     * <p>
237     * This method will not look for imports or package
238     * statements to detect the passed in annotation.
239     * </p>
240     *
241     * @param ast the current node
242     * @param predicate The predicate which decides if an annotation matches
243     * @return the AST representing that annotation
244     */
245    private static DetailAST findFirstAnnotation(final DetailAST ast,
246                                                 Predicate<DetailAST> predicate) {
247        final DetailAST holder = getAnnotationHolder(ast);
248        DetailAST result = null;
249        for (DetailAST child = holder.getFirstChild();
250            child != null; child = child.getNextSibling()) {
251            if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) {
252                result = child;
253                break;
254            }
255        }
256
257        return result;
258    }
259
260}