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}