001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 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    /**
041     * Private utility constructor.
042     *
043     * @throws UnsupportedOperationException if called
044     */
045    private AnnotationUtil() {
046        throw new UnsupportedOperationException("do not instantiate.");
047    }
048
049    /**
050     * Checks if the AST is annotated with the passed in annotation.
051     *
052     * <p>
053     * This method will not look for imports or package
054     * statements to detect the passed in annotation.
055     * </p>
056     *
057     * <p>
058     * To check if an AST contains a passed in annotation
059     * taking into account fully-qualified names
060     * (ex: java.lang.Override, Override)
061     * this method will need to be called twice. Once for each
062     * name given.
063     * </p>
064     *
065     * @param ast the current node
066     * @param annotation the annotation name to check for
067     * @return true if contains the annotation
068     */
069    public static boolean containsAnnotation(final DetailAST ast,
070        String annotation) {
071        return getAnnotation(ast, annotation) != null;
072    }
073
074    /**
075     * Checks if the AST is annotated with any annotation.
076     *
077     * @param ast the current node
078     * @return {@code true} if the AST contains at least one annotation
079     * @throws IllegalArgumentException when ast is null
080     */
081    public static boolean containsAnnotation(final DetailAST ast) {
082        if (ast == null) {
083            throw new IllegalArgumentException(THE_AST_IS_NULL);
084        }
085        final DetailAST holder = getAnnotationHolder(ast);
086        return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null;
087    }
088
089    /**
090     * Checks if the given AST element is annotated with any of the specified annotations.
091     *
092     * <p>
093     * This method accepts both simple and fully-qualified names,
094     * e.g. "Override" will match both java.lang.Override and Override.
095     * </p>
096     *
097     * @param ast The type or method definition.
098     * @param annotations A collection of annotations to look for.
099     * @return {@code true} if the given AST element is annotated with
100     *                      at least one of the specified annotations;
101     *                      {@code false} otherwise.
102     * @throws IllegalArgumentException when ast or annotations are null
103     */
104    public static boolean containsAnnotation(DetailAST ast, List<String> annotations) {
105        if (ast == null) {
106            throw new IllegalArgumentException(THE_AST_IS_NULL);
107        }
108
109        if (annotations == null) {
110            throw new IllegalArgumentException("annotations cannot be null");
111        }
112
113        boolean result = false;
114
115        if (!annotations.isEmpty()) {
116            final DetailAST firstMatchingAnnotation = findFirstAnnotation(ast, annotationNode -> {
117                DetailAST identNode = annotationNode.findFirstToken(TokenTypes.IDENT);
118                if (identNode == null) {
119                    identNode = annotationNode.findFirstToken(TokenTypes.DOT)
120                            .findFirstToken(TokenTypes.IDENT);
121                }
122
123                return annotations.contains(identNode.getText());
124            });
125            result = firstMatchingAnnotation != null;
126        }
127
128        return result;
129    }
130
131    /**
132     * Gets the AST that holds a series of annotations for the
133     * potentially annotated AST.  Returns {@code null}
134     * if the passed in AST does not have an Annotation Holder.
135     *
136     * @param ast the current node
137     * @return the Annotation Holder
138     * @throws IllegalArgumentException when ast is null
139     */
140    public static DetailAST getAnnotationHolder(DetailAST ast) {
141        if (ast == null) {
142            throw new IllegalArgumentException(THE_AST_IS_NULL);
143        }
144
145        final DetailAST annotationHolder;
146
147        if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF
148            || ast.getType() == TokenTypes.PACKAGE_DEF) {
149            annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS);
150        }
151        else {
152            annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS);
153        }
154
155        return annotationHolder;
156    }
157
158    /**
159     * Checks if the AST is annotated with the passed in annotation
160     * and returns the AST representing that annotation.
161     *
162     * <p>
163     * This method will not look for imports or package
164     * statements to detect the passed in annotation.
165     * </p>
166     *
167     * <p>
168     * To check if an AST contains a passed in annotation
169     * taking into account fully-qualified names
170     * (ex: java.lang.Override, Override)
171     * this method will need to be called twice. Once for each
172     * name given.
173     * </p>
174     *
175     * @param ast the current node
176     * @param annotation the annotation name to check for
177     * @return the AST representing that annotation
178     * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank
179     */
180    public static DetailAST getAnnotation(final DetailAST ast,
181        String annotation) {
182        if (ast == null) {
183            throw new IllegalArgumentException(THE_AST_IS_NULL);
184        }
185
186        if (annotation == null) {
187            throw new IllegalArgumentException("the annotation is null");
188        }
189
190        if (CommonUtil.isBlank(annotation)) {
191            throw new IllegalArgumentException(
192                    "the annotation is empty or spaces");
193        }
194
195        return findFirstAnnotation(ast, annotationNode -> {
196            final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT);
197            final String name =
198                    FullIdent.createFullIdent(firstChild.getNextSibling()).getText();
199            return annotation.equals(name);
200        });
201    }
202
203    /**
204     * Checks if the given AST is annotated with at least one annotation that
205     * matches the given predicate and returns the AST representing the first
206     * matching annotation.
207     *
208     * <p>
209     * This method will not look for imports or package
210     * statements to detect the passed in annotation.
211     * </p>
212     *
213     * @param ast the current node
214     * @param predicate The predicate which decides if an annotation matches
215     * @return the AST representing that annotation
216     */
217    private static DetailAST findFirstAnnotation(final DetailAST ast,
218                                                 Predicate<DetailAST> predicate) {
219        final DetailAST holder = getAnnotationHolder(ast);
220        DetailAST result = null;
221        for (DetailAST child = holder.getFirstChild();
222            child != null; child = child.getNextSibling()) {
223            if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) {
224                result = child;
225                break;
226            }
227        }
228
229        return result;
230    }
231
232}