View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.site;
21  
22  import java.beans.Introspector;
23  import java.lang.reflect.Field;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.TreeSet;
29  import java.util.regex.Pattern;
30  
31  import org.apache.maven.doxia.macro.MacroExecutionException;
32  
33  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
34  import com.puppycrawl.tools.checkstyle.api.DetailAST;
35  import com.puppycrawl.tools.checkstyle.api.DetailNode;
36  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
37  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
38  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
39  import com.puppycrawl.tools.checkstyle.utils.BlockCommentPosition;
40  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
41  
42  /**
43   * Class for scraping class javadoc and all property setter javadocs from the
44   * given checkstyle module.
45   */
46  @FileStatefulCheck
47  public class ClassAndPropertiesSettersJavadocScraper extends AbstractJavadocCheck {
48  
49      /** Name of the module being scraped. */
50      private static String moduleName = "";
51  
52      /** The instance of the module. */
53      private static Object moduleInstance = new Object();
54  
55      /** The properties of the module. */
56      private static Set<String> properties = Set.of();
57  
58      /** Map of property names to their setter javadoc nodes. */
59      private final Map<String, DetailNode> setterNodes = new HashMap<>();
60  
61      /**
62       * Initialize the scraper. Clears static context and sets the module name.
63       *
64       * @param newModuleName the module name.
65       * @param instance the module instance.
66       * @param propertiesSet the set of properties to document.
67       */
68      public static void initialize(String newModuleName, Object instance,
69                                    Set<String> propertiesSet) {
70          JavadocScraperResultUtil.clearData();
71          moduleName = newModuleName;
72          moduleInstance = instance;
73          if (propertiesSet == null) {
74              properties = Set.of();
75          }
76          else {
77              properties = Collections.unmodifiableSet(new TreeSet<>(propertiesSet));
78          }
79      }
80  
81      @Override
82      public int[] getDefaultJavadocTokens() {
83          return new int[] {
84              JavadocCommentsTokenTypes.JAVADOC_CONTENT,
85          };
86      }
87  
88      @Override
89      public void visitJavadocToken(DetailNode ast) {
90          final DetailAST blockCommentAst = getBlockCommentAst();
91  
92          if (BlockCommentPosition.isOnMethod(blockCommentAst)) {
93              handleMethodComment(ast, blockCommentAst);
94          }
95          else if (BlockCommentPosition.isOnField(blockCommentAst)) {
96              handleFieldComment(ast, blockCommentAst);
97          }
98          else if (BlockCommentPosition.isOnClass(blockCommentAst)) {
99              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 }