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}