View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.utils;
21  
22  import java.util.Set;
23  import java.util.function.Predicate;
24  
25  import com.puppycrawl.tools.checkstyle.api.DetailAST;
26  import com.puppycrawl.tools.checkstyle.api.FullIdent;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  
29  /**
30   * Contains utility methods designed to work with annotations.
31   *
32   */
33  public final class AnnotationUtil {
34  
35      /**
36       * Common message.
37       */
38      private static final String THE_AST_IS_NULL = "the ast is null";
39  
40      /** {@link Override Override} annotation name. */
41      private static final String OVERRIDE = "Override";
42  
43      /** Fully-qualified {@link Override Override} annotation name. */
44      private static final String FQ_OVERRIDE = "java.lang." + OVERRIDE;
45  
46      /** Simple and fully-qualified {@link Override Override} annotation names. */
47      private static final Set<String> OVERRIDE_ANNOTATIONS = Set.of(OVERRIDE, FQ_OVERRIDE);
48  
49      /**
50       * Private utility constructor.
51       *
52       * @throws UnsupportedOperationException if called
53       */
54      private AnnotationUtil() {
55          throw new UnsupportedOperationException("do not instantiate.");
56      }
57  
58      /**
59       * Checks if the AST is annotated with the passed in annotation.
60       *
61       * <p>
62       * This method will not look for imports or package
63       * statements to detect the passed in annotation.
64       * </p>
65       *
66       * <p>
67       * To check if an AST contains a passed in annotation
68       * taking into account fully-qualified names
69       * (ex: java.lang.Override, Override)
70       * this method will need to be called twice. Once for each
71       * name given.
72       * </p>
73       *
74       * @param ast the current node
75       * @param annotation the annotation name to check for
76       * @return true if contains the annotation
77       */
78      public static boolean containsAnnotation(final DetailAST ast,
79          String annotation) {
80          return getAnnotation(ast, annotation) != null;
81      }
82  
83      /**
84       * Checks if the AST is annotated with any annotation.
85       *
86       * @param ast the current node
87       * @return {@code true} if the AST contains at least one annotation
88       * @throws IllegalArgumentException when ast is null
89       */
90      public static boolean containsAnnotation(final DetailAST ast) {
91          final DetailAST holder = getAnnotationHolder(ast);
92          return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null;
93      }
94  
95      /**
96       * Checks if the given AST element is annotated with any of the specified annotations.
97       *
98       * <p>
99       * 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 }