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 }