001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.site; 021 022import java.beans.Introspector; 023import java.lang.reflect.Field; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.Map; 027import java.util.Set; 028import java.util.TreeSet; 029import java.util.regex.Pattern; 030 031import org.apache.maven.doxia.macro.MacroExecutionException; 032 033import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 034import com.puppycrawl.tools.checkstyle.api.DetailAST; 035import com.puppycrawl.tools.checkstyle.api.DetailNode; 036import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 037import com.puppycrawl.tools.checkstyle.api.TokenTypes; 038import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck; 039import com.puppycrawl.tools.checkstyle.utils.BlockCommentPosition; 040import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 041 042/** 043 * Class for scraping class javadoc and all property setter javadocs from the 044 * given checkstyle module. 045 */ 046@FileStatefulCheck 047public class ClassAndPropertiesSettersJavadocScraper extends AbstractJavadocCheck { 048 049 /** Name of the module being scraped. */ 050 private static String moduleName = ""; 051 052 /** The instance of the module. */ 053 private static Object moduleInstance = new Object(); 054 055 /** The properties of the module. */ 056 private static Set<String> properties = Set.of(); 057 058 /** Map of property names to their setter javadoc nodes. */ 059 private final Map<String, DetailNode> setterNodes = new HashMap<>(); 060 061 /** 062 * Creates a new {@code ClassAndPropertiesSettersJavadocScraper} instance. 063 */ 064 public ClassAndPropertiesSettersJavadocScraper() { 065 // no code by default 066 } 067 068 /** 069 * Initialize the scraper. Clears static context and sets the module name. 070 * 071 * @param newModuleName the module name. 072 * @param instance the module instance. 073 * @param propertiesSet the set of properties to document. 074 */ 075 public static void initialize(String newModuleName, Object instance, 076 Set<String> propertiesSet) { 077 JavadocScraperResultUtil.clearData(); 078 moduleName = newModuleName; 079 moduleInstance = instance; 080 if (propertiesSet == null) { 081 properties = Set.of(); 082 } 083 else { 084 properties = Collections.unmodifiableSet(new TreeSet<>(propertiesSet)); 085 } 086 } 087 088 @Override 089 public int[] getDefaultJavadocTokens() { 090 return new int[] { 091 JavadocCommentsTokenTypes.JAVADOC_CONTENT, 092 }; 093 } 094 095 @Override 096 public void visitJavadocToken(DetailNode ast) { 097 final DetailAST blockCommentAst = getBlockCommentAst(); 098 099 if (BlockCommentPosition.isOnMethod(blockCommentAst)) { 100 handleMethodComment(ast, blockCommentAst); 101 } 102 else if (BlockCommentPosition.isOnField(blockCommentAst)) { 103 handleFieldComment(ast, blockCommentAst); 104 } 105 else if (BlockCommentPosition.isOnClass(blockCommentAst)) { 106 handleClassComment(ast, blockCommentAst); 107 } 108 } 109 110 /** 111 * Processes method Javadoc. If the method is a setter for a property of the 112 * module being scraped, the Javadoc node is stored. 113 * 114 * @param ast the Javadoc node. 115 * @param blockCommentAst the block comment AST. 116 */ 117 private void handleMethodComment(DetailNode ast, DetailAST blockCommentAst) { 118 final DetailAST methodDef = getParentAst(blockCommentAst, TokenTypes.METHOD_DEF); 119 120 if (methodDef != null 121 && isSetterMethod(methodDef) 122 && isMethodOfScrapedModule(methodDef)) { 123 final String methodName = TokenUtil.getIdent(methodDef).getText(); 124 final String propertyName = getPropertyName(methodName); 125 setterNodes.put(propertyName, ast); 126 } 127 } 128 129 /** 130 * Processes field Javadoc. If the field is a known property of the module 131 * being scraped, the Javadoc node is stored. 132 * 133 * @param ast the Javadoc node. 134 * @param blockCommentAst the block comment AST. 135 */ 136 private void handleFieldComment(DetailNode ast, DetailAST blockCommentAst) { 137 final DetailAST fieldDef = getParentAst(blockCommentAst, TokenTypes.VARIABLE_DEF); 138 139 if (fieldDef != null && isMethodOfScrapedModule(fieldDef)) { 140 final String fieldName = TokenUtil.getIdent(fieldDef).getText(); 141 if (isKnownProperty(fieldName)) { 142 setterNodes.put(fieldName, ast); 143 } 144 } 145 } 146 147 /** 148 * Checks if the field name is a known property that should be documented. 149 * 150 * @param fieldName the name of the field. 151 * @return true if it is a known property, false otherwise. 152 */ 153 private static boolean isKnownProperty(String fieldName) { 154 final boolean result; 155 if (properties.isEmpty()) { 156 result = SiteUtil.VIOLATE_EXECUTION_ON_NON_TIGHT_HTML.equals(fieldName); 157 } 158 else { 159 result = properties.contains(fieldName); 160 } 161 return result; 162 } 163 164 /** 165 * Processes class Javadoc. Extracts module metadata such as 'since' version, 166 * description, and notes. 167 * 168 * @param ast the Javadoc node. 169 * @param blockCommentAst the block comment AST. 170 */ 171 private static void handleClassComment(DetailNode ast, DetailAST blockCommentAst) { 172 final DetailAST classDef = getParentAst(blockCommentAst, TokenTypes.CLASS_DEF); 173 if (classDef != null) { 174 final String className = TokenUtil.getIdent(classDef).getText(); 175 176 final boolean isModuleNameNotEmpty = moduleName != null && !moduleName.isEmpty(); 177 178 final boolean isSameClass = className.equals(moduleName); 179 180 final boolean isModuleInstanceValid = moduleInstance != null 181 && moduleInstance.getClass() != Object.class; 182 183 if (isModuleNameNotEmpty && isSameClass && isModuleInstanceValid) { 184 185 final String moduleSinceVersion = 186 ModuleJavadocParsingUtil.getModuleSinceVersion(ast); 187 JavadocScraperResultUtil.setModuleSinceVersion(moduleSinceVersion); 188 189 final String moduleDescription = 190 ModuleJavadocParsingUtil.getModuleDescription(ast); 191 JavadocScraperResultUtil.setModuleDescription(moduleDescription); 192 193 final String moduleNotes = 194 ModuleJavadocParsingUtil.getModuleNotes(ast); 195 JavadocScraperResultUtil.setModuleNotes(moduleNotes); 196 } 197 } 198 } 199 200 @Override 201 public void finishTree(DetailAST rootAST) { 202 final Set<String> propsToProcess; 203 if (properties.isEmpty()) { 204 propsToProcess = setterNodes.keySet(); 205 } 206 else { 207 propsToProcess = properties; 208 } 209 210 for (String property : propsToProcess) { 211 final boolean isRealInstance = moduleInstance != null 212 && moduleInstance.getClass() != Object.class; 213 if (isRealInstance && !setterNodes.containsKey(property)) { 214 continue; 215 } 216 try { 217 final PropertyDetails details = createPropertyDetails(property); 218 JavadocScraperResultUtil.putPropertyDetails(property, details); 219 } 220 catch (MacroExecutionException ignored) { 221 // Property details cannot be created for this property, skip it. 222 } 223 } 224 } 225 226 /** 227 * Checks if the given method is a method of the module being scraped. Traverses 228 * parent nodes until it finds the class definition and checks if the class name 229 * is the same as the module name. We want to avoid scraping javadocs from 230 * inner classes. 231 * 232 * @param methodDef the method definition. 233 * @return true if the method is a method of the given module, false otherwise. 234 */ 235 private static boolean isMethodOfScrapedModule(DetailAST methodDef) { 236 final DetailAST classDef = getParentAst(methodDef, TokenTypes.CLASS_DEF); 237 boolean isMethodOfModule = false; 238 if (classDef != null) { 239 final String className = TokenUtil.getIdent(classDef).getText(); 240 isMethodOfModule = className.equals(moduleName); 241 } 242 return isMethodOfModule; 243 } 244 245 /** 246 * Get the parent node of the given type. Traverses up the tree until it finds 247 * the given type. 248 * 249 * @param ast the node to start traversing from. 250 * @param type the type of the parent node to find. 251 * @return the parent node of the given type, or null if not found. 252 */ 253 private static DetailAST getParentAst(DetailAST ast, int type) { 254 DetailAST node = ast.getParent(); 255 while (node != null && node.getType() != type) { 256 node = node.getParent(); 257 } 258 return node; 259 } 260 261 /** 262 * Get the property name from the setter method name. For example, getPropertyName("setFoo") 263 * returns "foo". This method removes the "set" prefix and decapitalizes the first letter 264 * of the property name. 265 * 266 * @param setterName the setter method name. 267 * @return the property name. 268 */ 269 private static String getPropertyName(String setterName) { 270 return Introspector.decapitalize(setterName.substring("set".length())); 271 } 272 273 /** 274 * Returns whether an AST represents a setter method. 275 * 276 * @param ast the AST to check with 277 * @return whether the AST represents a setter method 278 */ 279 private static boolean isSetterMethod(DetailAST ast) { 280 boolean setterMethod = false; 281 282 if (ast.getType() == TokenTypes.METHOD_DEF) { 283 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 284 final String name = type.getNextSibling().getText(); 285 final Pattern setterPattern = Pattern.compile("^set[A-Z].*"); 286 287 setterMethod = setterPattern.matcher(name).matches(); 288 } 289 return setterMethod; 290 } 291 292 /** 293 * Creates a PropertyDetails object for the given property. 294 * 295 * @param propertyName the name of the property. 296 * @return the PropertyDetails object. 297 * @throws MacroExecutionException if an error occurs 298 */ 299 private PropertyDetails createPropertyDetails(String propertyName) 300 throws MacroExecutionException { 301 final DetailNode setterJavadoc = setterNodes.get(propertyName); 302 final Class<?> instanceClass; 303 if (moduleInstance == null) { 304 instanceClass = Object.class; 305 } 306 else { 307 instanceClass = moduleInstance.getClass(); 308 } 309 310 final String description = SiteUtil.getPropertyDescriptionForXdoc(propertyName, 311 setterJavadoc, moduleName); 312 final String moduleSinceVersion = JavadocScraperResultUtil.getModuleSinceVersion(); 313 final String since = SiteUtil.getPropertySinceVersion(moduleSinceVersion, 314 setterJavadoc); 315 316 final PropertyDetails.Builder builder = new PropertyDetails.Builder() 317 .name(propertyName) 318 .description(description) 319 .sinceVersion(since); 320 321 final boolean isDefaultInstance = moduleInstance == null 322 || moduleInstance.getClass() == Object.class; 323 324 final PropertyDetails result; 325 if (isDefaultInstance) { 326 result = builder.build(); 327 } 328 else { 329 final Field field = SiteUtil.getField(instanceClass, propertyName); 330 result = SiteUtil.constructPropertyDetails(builder, moduleInstance, field, 331 propertyName, moduleName); 332 } 333 return result; 334 } 335 336}