001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2022 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.List;
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    /** List of simple and fully-qualified {@link Override Override} annotation names. */
047    private static final List<String> OVERRIDE_ANNOTATIONS = List.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, List<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                DetailAST identNode = annotationNode.findFirstToken(TokenTypes.IDENT);
127                if (identNode == null) {
128                    identNode = annotationNode.findFirstToken(TokenTypes.DOT)
129                            .findFirstToken(TokenTypes.IDENT);
130                }
131
132                return annotations.contains(identNode.getText());
133            });
134            result = firstMatchingAnnotation != null;
135        }
136
137        return result;
138    }
139
140    /**
141     * Checks if the AST is annotated with {@code Override} or
142     * {@code java.lang.Override} annotation.
143     *
144     * @param ast the current node
145     * @return {@code true} if the AST contains Override annotation
146     * @throws IllegalArgumentException when ast is null
147     */
148    public static boolean hasOverrideAnnotation(DetailAST ast) {
149        return containsAnnotation(ast, OVERRIDE_ANNOTATIONS);
150    }
151
152    /**
153     * Gets the AST that holds a series of annotations for the
154     * potentially annotated AST.  Returns {@code null}
155     * if the passed in AST does not have an Annotation Holder.
156     *
157     * @param ast the current node
158     * @return the Annotation Holder
159     * @throws IllegalArgumentException when ast is null
160     */
161    public static DetailAST getAnnotationHolder(DetailAST ast) {
162        if (ast == null) {
163            throw new IllegalArgumentException(THE_AST_IS_NULL);
164        }
165
166        final DetailAST annotationHolder;
167
168        if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF
169            || ast.getType() == TokenTypes.PACKAGE_DEF) {
170            annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS);
171        }
172        else {
173            annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS);
174        }
175
176        return annotationHolder;
177    }
178
179    /**
180     * Checks if the AST is annotated with the passed in annotation
181     * and returns the AST representing that annotation.
182     *
183     * <p>
184     * This method will not look for imports or package
185     * statements to detect the passed in annotation.
186     * </p>
187     *
188     * <p>
189     * To check if an AST contains a passed in annotation
190     * taking into account fully-qualified names
191     * (ex: java.lang.Override, Override)
192     * this method will need to be called twice. Once for each
193     * name given.
194     * </p>
195     *
196     * @param ast the current node
197     * @param annotation the annotation name to check for
198     * @return the AST representing that annotation
199     * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank
200     */
201    public static DetailAST getAnnotation(final DetailAST ast,
202        String annotation) {
203        if (ast == null) {
204            throw new IllegalArgumentException(THE_AST_IS_NULL);
205        }
206
207        if (annotation == null) {
208            throw new IllegalArgumentException("the annotation is null");
209        }
210
211        if (CommonUtil.isBlank(annotation)) {
212            throw new IllegalArgumentException(
213                    "the annotation is empty or spaces");
214        }
215
216        return findFirstAnnotation(ast, annotationNode -> {
217            final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT);
218            final String name =
219                    FullIdent.createFullIdent(firstChild.getNextSibling()).getText();
220            return annotation.equals(name);
221        });
222    }
223
224    /**
225     * Checks if the given AST is annotated with at least one annotation that
226     * matches the given predicate and returns the AST representing the first
227     * matching annotation.
228     *
229     * <p>
230     * This method will not look for imports or package
231     * statements to detect the passed in annotation.
232     * </p>
233     *
234     * @param ast the current node
235     * @param predicate The predicate which decides if an annotation matches
236     * @return the AST representing that annotation
237     */
238    private static DetailAST findFirstAnnotation(final DetailAST ast,
239                                                 Predicate<DetailAST> predicate) {
240        final DetailAST holder = getAnnotationHolder(ast);
241        DetailAST result = null;
242        for (DetailAST child = holder.getFirstChild();
243            child != null; child = child.getNextSibling()) {
244            if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) {
245                result = child;
246                break;
247            }
248        }
249
250        return result;
251    }
252
253}