001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2022 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 if (ast == null) { 092 throw new IllegalArgumentException(THE_AST_IS_NULL); 093 } 094 final DetailAST holder = getAnnotationHolder(ast); 095 return holder != null && holder.findFirstToken(TokenTypes.ANNOTATION) != null; 096 } 097 098 /** 099 * Checks if the given AST element is annotated with any of the specified annotations. 100 * 101 * <p> 102 * This method accepts both simple and fully-qualified names, 103 * e.g. "Override" will match both java.lang.Override and Override. 104 * </p> 105 * 106 * @param ast The type or method definition. 107 * @param annotations A collection of annotations to look for. 108 * @return {@code true} if the given AST element is annotated with 109 * at least one of the specified annotations; 110 * {@code false} otherwise. 111 * @throws IllegalArgumentException when ast or annotations are null 112 */ 113 public static boolean containsAnnotation(DetailAST ast, Set<String> annotations) { 114 if (ast == null) { 115 throw new IllegalArgumentException(THE_AST_IS_NULL); 116 } 117 118 if (annotations == null) { 119 throw new IllegalArgumentException("annotations cannot be null"); 120 } 121 122 boolean result = false; 123 124 if (!annotations.isEmpty()) { 125 final DetailAST firstMatchingAnnotation = findFirstAnnotation(ast, annotationNode -> { 126 final String annotationFullIdent = getAnnotationFullIdent(annotationNode); 127 return annotations.contains(annotationFullIdent); 128 }); 129 result = firstMatchingAnnotation != null; 130 } 131 132 return result; 133 } 134 135 /** 136 * Gets the full ident text of the annotation AST. 137 * 138 * @param annotationNode The annotation AST. 139 * @return The full ident text. 140 */ 141 private static String getAnnotationFullIdent(DetailAST annotationNode) { 142 final DetailAST identNode = annotationNode.findFirstToken(TokenTypes.IDENT); 143 final String annotationString; 144 145 // If no `IDENT` is found, then we have a `DOT` -> more than 1 qualifier 146 if (identNode == null) { 147 final DetailAST dotNode = annotationNode.findFirstToken(TokenTypes.DOT); 148 annotationString = FullIdent.createFullIdent(dotNode).getText(); 149 } 150 else { 151 annotationString = identNode.getText(); 152 } 153 154 return annotationString; 155 } 156 157 /** 158 * Checks if the AST is annotated with {@code Override} or 159 * {@code java.lang.Override} annotation. 160 * 161 * @param ast the current node 162 * @return {@code true} if the AST contains Override annotation 163 * @throws IllegalArgumentException when ast is null 164 */ 165 public static boolean hasOverrideAnnotation(DetailAST ast) { 166 return containsAnnotation(ast, OVERRIDE_ANNOTATIONS); 167 } 168 169 /** 170 * Gets the AST that holds a series of annotations for the 171 * potentially annotated AST. Returns {@code null} 172 * if the passed in AST does not have an Annotation Holder. 173 * 174 * @param ast the current node 175 * @return the Annotation Holder 176 * @throws IllegalArgumentException when ast is null 177 */ 178 public static DetailAST getAnnotationHolder(DetailAST ast) { 179 if (ast == null) { 180 throw new IllegalArgumentException(THE_AST_IS_NULL); 181 } 182 183 final DetailAST annotationHolder; 184 185 if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF 186 || ast.getType() == TokenTypes.PACKAGE_DEF) { 187 annotationHolder = ast.findFirstToken(TokenTypes.ANNOTATIONS); 188 } 189 else { 190 annotationHolder = ast.findFirstToken(TokenTypes.MODIFIERS); 191 } 192 193 return annotationHolder; 194 } 195 196 /** 197 * Checks if the AST is annotated with the passed in annotation 198 * and returns the AST representing that annotation. 199 * 200 * <p> 201 * This method will not look for imports or package 202 * statements to detect the passed in annotation. 203 * </p> 204 * 205 * <p> 206 * To check if an AST contains a passed in annotation 207 * taking into account fully-qualified names 208 * (ex: java.lang.Override, Override) 209 * this method will need to be called twice. Once for each 210 * name given. 211 * </p> 212 * 213 * @param ast the current node 214 * @param annotation the annotation name to check for 215 * @return the AST representing that annotation 216 * @throws IllegalArgumentException when ast or annotations are null; when annotation is blank 217 */ 218 public static DetailAST getAnnotation(final DetailAST ast, 219 String annotation) { 220 if (ast == null) { 221 throw new IllegalArgumentException(THE_AST_IS_NULL); 222 } 223 224 if (annotation == null) { 225 throw new IllegalArgumentException("the annotation is null"); 226 } 227 228 if (CommonUtil.isBlank(annotation)) { 229 throw new IllegalArgumentException( 230 "the annotation is empty or spaces"); 231 } 232 233 return findFirstAnnotation(ast, annotationNode -> { 234 final DetailAST firstChild = annotationNode.findFirstToken(TokenTypes.AT); 235 final String name = 236 FullIdent.createFullIdent(firstChild.getNextSibling()).getText(); 237 return annotation.equals(name); 238 }); 239 } 240 241 /** 242 * Checks if the given AST is annotated with at least one annotation that 243 * matches the given predicate and returns the AST representing the first 244 * matching annotation. 245 * 246 * <p> 247 * This method will not look for imports or package 248 * statements to detect the passed in annotation. 249 * </p> 250 * 251 * @param ast the current node 252 * @param predicate The predicate which decides if an annotation matches 253 * @return the AST representing that annotation 254 */ 255 private static DetailAST findFirstAnnotation(final DetailAST ast, 256 Predicate<DetailAST> predicate) { 257 final DetailAST holder = getAnnotationHolder(ast); 258 DetailAST result = null; 259 for (DetailAST child = holder.getFirstChild(); 260 child != null; child = child.getNextSibling()) { 261 if (child.getType() == TokenTypes.ANNOTATION && predicate.test(child)) { 262 result = child; 263 break; 264 } 265 } 266 267 return result; 268 } 269 270}