001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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}