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