1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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 any annotation.
60 *
61 * @param ast the current node
62 * @return {@code true} if the AST contains at least one annotation
63 * @throws IllegalArgumentException when ast is null
64 */
65 public static boolean containsAnnotation(final DetailAST ast) {
66 final DetailAST holder = getAnnotationHolder(ast);
67 return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null;
68 }
69
70 /**
71 * Checks if the AST is annotated with the passed in annotation.
72 *
73 * <p>
74 * This method will not look for imports or package
75 * statements to detect the passed in annotation.
76 * </p>
77 *
78 * <p>
79 * To check if an AST contains a passed in annotation
80 * taking into account fully-qualified names
81 * (ex: java.lang.Override, Override)
82 * this method will need to be called twice. Once for each
83 * name given.
84 * </p>
85 *
86 * @param ast the current node
87 * @param annotation the annotation name to check for
88 * @return true if contains the annotation
89 */
90 public static boolean containsAnnotation(final DetailAST ast,
91 String annotation) {
92 return getAnnotation(ast, 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 public 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 || ast.getType() == TokenTypes.MODULE_DEF) {
178 annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS);
179 }
180 else {
181 annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS);
182 }
183
184 return annotationHolder;
185 }
186
187 /**
188 * Checks if the AST is annotated with the passed in annotation
189 * and returns the AST representing that annotation.
190 *
191 * <p>
192 * This method will not look for imports or package
193 * statements to detect the passed in annotation.
194 * </p>
195 *
196 * <p>
197 * To check if an AST contains a passed in annotation
198 * taking into account fully-qualified names
199 * (ex: java.lang.Override, Override)
200 * this method will need to be called twice. Once for each
201 * name given.
202 * </p>
203 *
204 * @param ast the current node
205 * @param annotation the annotation name to check for
206 * @return the AST representing that annotation
207 * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank
208 */
209 public static DetailAST getAnnotation(final DetailAST ast,
210 String annotation) {
211 if (ast == null) {
212 throw new IllegalArgumentException(THE_AST_IS_NULL);
213 }
214
215 if (annotation == null) {
216 throw new IllegalArgumentException("the annotation is null");
217 }
218
219 if (CommonUtil.isBlank(annotation)) {
220 throw new IllegalArgumentException(
221 "the annotation is empty or spaces");
222 }
223
224 return findFirstAnnotation(ast, annotationNode -> {
225 final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT);
226 final String name =
227 FullIdent.createFullIdent(firstChild.getNextSibling()).getText();
228 return annotation.equals(name);
229 });
230 }
231
232 /**
233 * Checks if the given AST is annotated with at least one annotation that
234 * matches the given predicate and returns the AST representing the first
235 * matching annotation.
236 *
237 * <p>
238 * This method will not look for imports or package
239 * statements to detect the passed in annotation.
240 * </p>
241 *
242 * @param ast the current node
243 * @param predicate The predicate which decides if an annotation matches
244 * @return the AST representing that annotation
245 */
246 private static DetailAST findFirstAnnotation(final DetailAST ast,
247 Predicate<DetailAST> predicate) {
248 final DetailAST holder = getAnnotationHolder(ast);
249 DetailAST result = null;
250 for (DetailAST child = holder.getFirstChild();
251 child != null; child = child.getNextSibling()) {
252 if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) {
253 result = child;
254 break;
255 }
256 }
257
258 return result;
259 }
260
261 }