001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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        if (ast == null) {
092            throw new IllegalArgumentException(THE_AST_IS_NULL);
093        }
094        final DetailAST holder = getAnnotationHolder(ast);
095        return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null;
096    }
097
098    /**
099     * Checks if the given AST element is annotated with any of the specified annotations.
100     *
101     * <p>
102     * This method accepts both simple and fully-qualified names,
103     * e.g. "Override" will match both java.lang.Override and Override.
104     * </p>
105     *
106     * @param ast The type or method definition.
107     * @param annotations A collection of annotations to look for.
108     * @return {@code true} if the given AST element is annotated with
109     *                      at least one of the specified annotations;
110     *                      {@code false} otherwise.
111     * @throws IllegalArgumentException when ast or annotations are null
112     */
113    public static boolean containsAnnotation(DetailAST ast, Set<String> annotations) {
114        if (ast == null) {
115            throw new IllegalArgumentException(THE_AST_IS_NULL);
116        }
117
118        if (annotations == null) {
119            throw new IllegalArgumentException("annotations cannot be null");
120        }
121
122        boolean result = false;
123
124        if (!annotations.isEmpty()) {
125            final DetailAST firstMatchingAnnotation = findFirstAnnotation(ast, annotationNode -> {
126                final String annotationFullIdent = getAnnotationFullIdent(annotationNode);
127                return annotations.contains(annotationFullIdent);
128            });
129            result = firstMatchingAnnotation != null;
130        }
131
132        return result;
133    }
134
135    /**
136     * Gets the full ident text of the annotation AST.
137     *
138     * @param annotationNode The annotation AST.
139     * @return The full ident text.
140     */
141    private static String getAnnotationFullIdent(DetailAST annotationNode) {
142        final DetailAST identNode = annotationNode.findFirstToken(TokenTypes.IDENT);
143        final String annotationString;
144
145        // If no `IDENT` is found, then we have a `DOT` -> more than 1 qualifier
146        if (identNode == null) {
147            final DetailAST dotNode = annotationNode.findFirstToken(TokenTypes.DOT);
148            annotationString = FullIdent.createFullIdent(dotNode).getText();
149        }
150        else {
151            annotationString = identNode.getText();
152        }
153
154        return annotationString;
155    }
156
157    /**
158     * Checks if the AST is annotated with {@code Override} or
159     * {@code java.lang.Override} annotation.
160     *
161     * @param ast the current node
162     * @return {@code true} if the AST contains Override annotation
163     * @throws IllegalArgumentException when ast is null
164     */
165    public static boolean hasOverrideAnnotation(DetailAST ast) {
166        return containsAnnotation(ast, OVERRIDE_ANNOTATIONS);
167    }
168
169    /**
170     * Gets the AST that holds a series of annotations for the
171     * potentially annotated AST.  Returns {@code null}
172     * if the passed in AST does not have an Annotation Holder.
173     *
174     * @param ast the current node
175     * @return the Annotation Holder
176     * @throws IllegalArgumentException when ast is null
177     */
178    public static DetailAST getAnnotationHolder(DetailAST ast) {
179        if (ast == null) {
180            throw new IllegalArgumentException(THE_AST_IS_NULL);
181        }
182
183        final DetailAST annotationHolder;
184
185        if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF
186            || ast.getType() == TokenTypes.PACKAGE_DEF) {
187            annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS);
188        }
189        else {
190            annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS);
191        }
192
193        return annotationHolder;
194    }
195
196    /**
197     * Checks if the AST is annotated with the passed in annotation
198     * and returns the AST representing that annotation.
199     *
200     * <p>
201     * This method will not look for imports or package
202     * statements to detect the passed in annotation.
203     * </p>
204     *
205     * <p>
206     * To check if an AST contains a passed in annotation
207     * taking into account fully-qualified names
208     * (ex: java.lang.Override, Override)
209     * this method will need to be called twice. Once for each
210     * name given.
211     * </p>
212     *
213     * @param ast the current node
214     * @param annotation the annotation name to check for
215     * @return the AST representing that annotation
216     * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank
217     */
218    public static DetailAST getAnnotation(final DetailAST ast,
219        String annotation) {
220        if (ast == null) {
221            throw new IllegalArgumentException(THE_AST_IS_NULL);
222        }
223
224        if (annotation == null) {
225            throw new IllegalArgumentException("the annotation is null");
226        }
227
228        if (CommonUtil.isBlank(annotation)) {
229            throw new IllegalArgumentException(
230                    "the annotation is empty or spaces");
231        }
232
233        return findFirstAnnotation(ast, annotationNode -> {
234            final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT);
235            final String name =
236                    FullIdent.createFullIdent(firstChild.getNextSibling()).getText();
237            return annotation.equals(name);
238        });
239    }
240
241    /**
242     * Checks if the given AST is annotated with at least one annotation that
243     * matches the given predicate and returns the AST representing the first
244     * matching annotation.
245     *
246     * <p>
247     * This method will not look for imports or package
248     * statements to detect the passed in annotation.
249     * </p>
250     *
251     * @param ast the current node
252     * @param predicate The predicate which decides if an annotation matches
253     * @return the AST representing that annotation
254     */
255    private static DetailAST findFirstAnnotation(final DetailAST ast,
256                                                 Predicate<DetailAST> predicate) {
257        final DetailAST holder = getAnnotationHolder(ast);
258        DetailAST result = null;
259        for (DetailAST child = holder.getFirstChild();
260            child != null; child = child.getNextSibling()) {
261            if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) {
262                result = child;
263                break;
264            }
265        }
266
267        return result;
268    }
269
270}