001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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 final DetailAST holder = getAnnotationHolder(ast);
092 return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null;
093 }
094
095 /**
096 * Checks if the given AST element is annotated with any of the specified annotations.
097 *
098 * <p>
099 * 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}