001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.PropertyDescriptor;
023import java.io.File;
024import java.io.IOException;
025import java.lang.module.ModuleDescriptor.Version;
026import java.lang.reflect.Array;
027import java.lang.reflect.Field;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.ParameterizedType;
030import java.net.URI;
031import java.nio.charset.StandardCharsets;
032import java.nio.file.Files;
033import java.nio.file.Path;
034import java.util.ArrayDeque;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.BitSet;
038import java.util.Collection;
039import java.util.Deque;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.LinkedHashMap;
043import java.util.List;
044import java.util.Locale;
045import java.util.Map;
046import java.util.Objects;
047import java.util.Optional;
048import java.util.Set;
049import java.util.TreeSet;
050import java.util.regex.Pattern;
051import java.util.stream.Collectors;
052import java.util.stream.IntStream;
053import java.util.stream.Stream;
054
055import javax.annotation.Nullable;
056
057import org.apache.commons.beanutils.PropertyUtils;
058import org.apache.maven.doxia.macro.MacroExecutionException;
059
060import com.google.common.collect.Lists;
061import com.puppycrawl.tools.checkstyle.Checker;
062import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
063import com.puppycrawl.tools.checkstyle.ModuleFactory;
064import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
065import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
066import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
067import com.puppycrawl.tools.checkstyle.TreeWalker;
068import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
069import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
070import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
071import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
072import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
073import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
074import com.puppycrawl.tools.checkstyle.api.DetailNode;
075import com.puppycrawl.tools.checkstyle.api.Filter;
076import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
077import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
078import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
079import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
080import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
081import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
082import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
083import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
084import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
085
086/**
087 * Utility class for site generation.
088 */
089public final class SiteUtil {
090
091    /** The string 'tokens'. */
092    public static final String TOKENS = "tokens";
093    /** The string 'javadocTokens'. */
094    public static final String JAVADOC_TOKENS = "javadocTokens";
095    /** The string '.'. */
096    public static final String DOT = ".";
097    /** The string ', '. */
098    public static final String COMMA_SPACE = ", ";
099    /** The string 'TokenTypes'. */
100    public static final String TOKEN_TYPES = "TokenTypes";
101    /** The path to the TokenTypes.html file. */
102    public static final String PATH_TO_TOKEN_TYPES =
103            "apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html";
104    /** The path to the JavadocTokenTypes.html file. */
105    public static final String PATH_TO_JAVADOC_TOKEN_TYPES =
106            "apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html";
107    /** The string of JavaDoc module marking 'Since version'. */
108    public static final String SINCE_VERSION = "Since version";
109    /** The url of the checkstyle website. */
110    private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/";
111    /** The string 'charset'. */
112    private static final String CHARSET = "charset";
113    /** The string '{}'. */
114    private static final String CURLY_BRACKETS = "{}";
115    /** The string 'fileExtensions'. */
116    private static final String FILE_EXTENSIONS = "fileExtensions";
117    /** The string 'checks'. */
118    private static final String CHECKS = "checks";
119    /** The string 'naming'. */
120    private static final String NAMING = "naming";
121    /** The string 'src'. */
122    private static final String SRC = "src";
123    /** The whitespace. */
124    private static final String WHITESPACE = " ";
125
126    /** Precompiled regex pattern to remove the "Setter to " prefix from strings. */
127    private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to ");
128
129    /** Class name and their corresponding parent module name. */
130    private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries(
131        Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()),
132        Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()),
133        Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()),
134        Map.entry(Filter.class, Checker.class.getSimpleName()),
135        Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName())
136    );
137
138    /** Set of properties that every check has. */
139    private static final Set<String> CHECK_PROPERTIES =
140            getProperties(AbstractCheck.class);
141
142    /** Set of properties that every Javadoc check has. */
143    private static final Set<String> JAVADOC_CHECK_PROPERTIES =
144            getProperties(AbstractJavadocCheck.class);
145
146    /** Set of properties that every FileSet check has. */
147    private static final Set<String> FILESET_PROPERTIES =
148            getProperties(AbstractFileSetCheck.class);
149
150    /**
151     * Check and property name.
152     */
153    private static final String HEADER_CHECK_HEADER = "HeaderCheck.header";
154
155    /**
156     * Check and property name.
157     */
158    private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header";
159
160    /** Set of properties that are undocumented. Those are internal properties. */
161    private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
162        "SuppressWithNearbyCommentFilter.fileContents",
163        "SuppressionCommentFilter.fileContents"
164    );
165
166    /** Properties that can not be gathered from class instance. */
167    private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
168        // static field (all upper case)
169        "SuppressWarningsHolder.aliasList",
170        // loads string into memory similar to file
171        HEADER_CHECK_HEADER,
172        REGEXP_HEADER_CHECK_HEADER,
173        // property is an int, but we cut off excess to accommodate old versions
174        "RedundantModifierCheck.jdkVersion",
175        // until https://github.com/checkstyle/checkstyle/issues/13376
176        "CustomImportOrderCheck.customImportOrderRules"
177    );
178
179    /** Map of all superclasses properties and their javadocs. */
180    private static final Map<String, DetailNode> SUPER_CLASS_PROPERTIES_JAVADOCS =
181            new HashMap<>();
182
183    /** Path to main source code folder. */
184    private static final String MAIN_FOLDER_PATH = Path.of(
185            SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString();
186
187    /** List of files who are superclasses and contain certain properties that checks inherit. */
188    private static final List<Path> MODULE_SUPER_CLASS_PATHS = List.of(
189        Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractAccessControlNameCheck.java"),
190        Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractNameCheck.java"),
191        Path.of(MAIN_FOLDER_PATH, CHECKS, "javadoc", "AbstractJavadocCheck.java"),
192        Path.of(MAIN_FOLDER_PATH, "api", "AbstractFileSetCheck.java"),
193        Path.of(MAIN_FOLDER_PATH, CHECKS, "header", "AbstractHeaderCheck.java"),
194        Path.of(MAIN_FOLDER_PATH, CHECKS, "metrics", "AbstractClassCouplingCheck.java"),
195        Path.of(MAIN_FOLDER_PATH, CHECKS, "whitespace", "AbstractParenPadCheck.java")
196    );
197
198    /**
199     * Private utility constructor.
200     */
201    private SiteUtil() {
202    }
203
204    /**
205     * Get string values of the message keys from the given check class.
206     *
207     * @param module class to examine.
208     * @return a set of checkstyle's module message keys.
209     * @throws MacroExecutionException if extraction of message keys fails.
210     */
211    public static Set<String> getMessageKeys(Class<?> module)
212            throws MacroExecutionException {
213        final Set<Field> messageKeyFields = getCheckMessageKeys(module);
214        // We use a TreeSet to sort the message keys alphabetically
215        final Set<String> messageKeys = new TreeSet<>();
216        for (Field field : messageKeyFields) {
217            messageKeys.add(getFieldValue(field, module).toString());
218        }
219        return messageKeys;
220    }
221
222    /**
223     * Gets the check's messages keys.
224     *
225     * @param module class to examine.
226     * @return a set of checkstyle's module message fields.
227     * @throws MacroExecutionException if the attempt to read a protected class fails.
228     * @noinspection ChainOfInstanceofChecks
229     * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at
230     *                     <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a>
231     *
232     */
233    private static Set<Field> getCheckMessageKeys(Class<?> module)
234            throws MacroExecutionException {
235        try {
236            final Set<Field> checkstyleMessages = new HashSet<>();
237
238            // get all fields from current class
239            final Field[] fields = module.getDeclaredFields();
240
241            for (Field field : fields) {
242                if (field.getName().startsWith("MSG_")) {
243                    checkstyleMessages.add(field);
244                }
245            }
246
247            // deep scan class through hierarchy
248            final Class<?> superModule = module.getSuperclass();
249
250            if (superModule != null) {
251                checkstyleMessages.addAll(getCheckMessageKeys(superModule));
252            }
253
254            // special cases that require additional classes
255            if (module == RegexpMultilineCheck.class) {
256                checkstyleMessages.addAll(getCheckMessageKeys(Class
257                    .forName("com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector")));
258            }
259            else if (module == RegexpSinglelineCheck.class
260                    || module == RegexpSinglelineJavaCheck.class) {
261                checkstyleMessages.addAll(getCheckMessageKeys(Class
262                    .forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector")));
263            }
264
265            return checkstyleMessages;
266        }
267        catch (ClassNotFoundException exc) {
268            final String message = String.format(Locale.ROOT, "Couldn't find class: %s",
269                    module.getName());
270            throw new MacroExecutionException(message, exc);
271        }
272    }
273
274    /**
275     * Returns the value of the given field.
276     *
277     * @param field the field.
278     * @param instance the instance of the module.
279     * @return the value of the field.
280     * @throws MacroExecutionException if the value could not be retrieved.
281     */
282    public static Object getFieldValue(Field field, Object instance)
283            throws MacroExecutionException {
284        try {
285            // required for package/private classes
286            field.trySetAccessible();
287            return field.get(instance);
288        }
289        catch (IllegalAccessException exc) {
290            throw new MacroExecutionException("Couldn't get field value", exc);
291        }
292    }
293
294    /**
295     * Returns the instance of the module with the given name.
296     *
297     * @param moduleName the name of the module.
298     * @return the instance of the module.
299     * @throws MacroExecutionException if the module could not be created.
300     */
301    public static Object getModuleInstance(String moduleName) throws MacroExecutionException {
302        final ModuleFactory factory = getPackageObjectFactory();
303        try {
304            return factory.createModule(moduleName);
305        }
306        catch (CheckstyleException exc) {
307            throw new MacroExecutionException("Couldn't find class: " + moduleName, exc);
308        }
309    }
310
311    /**
312     * Returns the default PackageObjectFactory with the default package names.
313     *
314     * @return the default PackageObjectFactory.
315     * @throws MacroExecutionException if the PackageObjectFactory cannot be created.
316     */
317    private static PackageObjectFactory getPackageObjectFactory() throws MacroExecutionException {
318        try {
319            final ClassLoader cl = ViolationMessagesMacro.class.getClassLoader();
320            final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
321            return new PackageObjectFactory(packageNames, cl);
322        }
323        catch (CheckstyleException exc) {
324            throw new MacroExecutionException("Couldn't load checkstyle modules", exc);
325        }
326    }
327
328    /**
329     * Construct a string with a leading newline character and followed by
330     * the given amount of spaces. We use this method only to match indentation in
331     * regular xdocs and have minimal diff when parsing the templates.
332     * This method exists until
333     * <a href="https://github.com/checkstyle/checkstyle/issues/13426">13426</a>
334     *
335     * @param amountOfSpaces the amount of spaces to add after the newline.
336     * @return the constructed string.
337     */
338    public static String getNewlineAndIndentSpaces(int amountOfSpaces) {
339        return System.lineSeparator() + WHITESPACE.repeat(amountOfSpaces);
340    }
341
342    /**
343     * Returns path to the template for the given module name or throws an exception if the
344     * template cannot be found.
345     *
346     * @param moduleName the module whose template we are looking for.
347     * @return path to the template.
348     * @throws MacroExecutionException if the template cannot be found.
349     */
350    public static Path getTemplatePath(String moduleName) throws MacroExecutionException {
351        final String fileNamePattern = ".*[\\\\/]"
352                + moduleName.toLowerCase(Locale.ROOT) + "\\..*";
353        return getXdocsTemplatesFilePaths()
354            .stream()
355            .filter(path -> path.toString().matches(fileNamePattern))
356            .findFirst()
357            .orElse(null);
358    }
359
360    /**
361     * Gets xdocs template file paths. These are files ending with .xml.template.
362     * This method will be changed to gather .xml once
363     * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
364     *
365     * @return a set of xdocs template file paths.
366     * @throws MacroExecutionException if an I/O error occurs.
367     */
368    public static Set<Path> getXdocsTemplatesFilePaths() throws MacroExecutionException {
369        final Path directory = Path.of("src/site/xdoc");
370        try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
371                (path, attr) -> {
372                    return attr.isRegularFile()
373                            && path.toString().endsWith(".xml.template");
374                })) {
375            return stream.collect(Collectors.toUnmodifiableSet());
376        }
377        catch (IOException ioException) {
378            throw new MacroExecutionException("Failed to find xdocs templates", ioException);
379        }
380    }
381
382    /**
383     * Returns the parent module name for the given module class. Returns either
384     * "TreeWalker" or "Checker". Returns null if the module class is null.
385     *
386     * @param moduleClass the module class.
387     * @return the parent module name as a string.
388     * @throws MacroExecutionException if the parent module cannot be found.
389     */
390    public static String getParentModule(Class<?> moduleClass)
391                throws MacroExecutionException {
392        String parentModuleName = "";
393        Class<?> parentClass = moduleClass.getSuperclass();
394
395        while (parentClass != null) {
396            parentModuleName = CLASS_TO_PARENT_MODULE.get(parentClass);
397            if (parentModuleName != null) {
398                break;
399            }
400            parentClass = parentClass.getSuperclass();
401        }
402
403        // If parent class is not found, check interfaces
404        if (parentModuleName == null || parentModuleName.isEmpty()) {
405            final Class<?>[] interfaces = moduleClass.getInterfaces();
406            for (Class<?> interfaceClass : interfaces) {
407                parentModuleName = CLASS_TO_PARENT_MODULE.get(interfaceClass);
408                if (parentModuleName != null) {
409                    break;
410                }
411            }
412        }
413
414        if (parentModuleName == null || parentModuleName.isEmpty()) {
415            final String message = String.format(Locale.ROOT,
416                    "Failed to find parent module for %s", moduleClass.getSimpleName());
417            throw new MacroExecutionException(message);
418        }
419
420        return parentModuleName;
421    }
422
423    /**
424     * Get a set of properties for the given class that should be documented.
425     *
426     * @param clss the class to get the properties for.
427     * @param instance the instance of the module.
428     * @return a set of properties for the given class.
429     */
430    public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) {
431        final Set<String> properties =
432                getProperties(clss).stream()
433                    .filter(prop -> {
434                        return !isGlobalProperty(clss, prop) && !isUndocumentedProperty(clss, prop);
435                    })
436                    .collect(Collectors.toCollection(HashSet::new));
437        properties.addAll(getNonExplicitProperties(instance, clss));
438        return new TreeSet<>(properties);
439    }
440
441    /**
442     * Gets the javadoc of module class.
443     *
444     * @param moduleName name of module.
445     * @param modulePath module's path.
446     * @return javadoc of module.
447     * @throws MacroExecutionException if an error occurs during processing.
448     */
449    public static DetailNode getModuleJavadoc(String moduleName, Path modulePath)
450            throws MacroExecutionException {
451
452        processModule(moduleName, modulePath);
453        return JavadocScraperResultUtil.getModuleJavadocNode();
454    }
455
456    /**
457     * Get the javadocs of the properties of the module. If the property is not present in the
458     * module, then the javadoc of the property from the superclass(es) is used.
459     *
460     * @param properties the properties of the module.
461     * @param moduleName the name of the module.
462     * @param modulePath the module file path.
463     * @return the javadocs of the properties of the module.
464     * @throws MacroExecutionException if an error occurs during processing.
465     */
466    public static Map<String, DetailNode> getPropertiesJavadocs(Set<String> properties,
467                                                                String moduleName, Path modulePath)
468            throws MacroExecutionException {
469        // lazy initialization
470        if (SUPER_CLASS_PROPERTIES_JAVADOCS.isEmpty()) {
471            processSuperclasses();
472        }
473
474        processModule(moduleName, modulePath);
475
476        final Map<String, DetailNode> unmodifiablePropertiesJavadocs =
477                JavadocScraperResultUtil.getPropertiesJavadocNode();
478        final Map<String, DetailNode> propertiesJavadocs =
479            new LinkedHashMap<>(unmodifiablePropertiesJavadocs);
480
481        properties.forEach(property -> {
482            final DetailNode superClassPropertyJavadoc =
483                    SUPER_CLASS_PROPERTIES_JAVADOCS.get(property);
484            if (superClassPropertyJavadoc != null) {
485                propertiesJavadocs.putIfAbsent(property, superClassPropertyJavadoc);
486            }
487        });
488
489        assertAllPropertySetterJavadocsAreFound(properties, moduleName, propertiesJavadocs);
490
491        return propertiesJavadocs;
492    }
493
494    /**
495     * Assert that each property has a corresponding setter javadoc that is not null.
496     * 'tokens' and 'javadocTokens' are excluded from this check, because their
497     * description is different from the description of the setter.
498     *
499     * @param properties the properties of the module.
500     * @param moduleName the name of the module.
501     * @param javadocs the javadocs of the properties of the module.
502     * @throws MacroExecutionException if an error occurs during processing.
503     */
504    private static void assertAllPropertySetterJavadocsAreFound(
505            Set<String> properties, String moduleName, Map<String, DetailNode> javadocs)
506            throws MacroExecutionException {
507        for (String property : properties) {
508            final boolean isDocumented = javadocs.containsKey(property)
509                   || SUPER_CLASS_PROPERTIES_JAVADOCS.containsKey(property)
510                   || TOKENS.equals(property) || JAVADOC_TOKENS.equals(property);
511            if (!isDocumented) {
512                throw new MacroExecutionException(String.format(Locale.ROOT,
513                   "%s: Missing documentation for property '%s'. Check superclasses.",
514                        moduleName, property));
515            }
516        }
517    }
518
519    /**
520     * Collect the properties setters javadocs of the superclasses.
521     *
522     * @throws MacroExecutionException if an error occurs during processing.
523     */
524    private static void processSuperclasses() throws MacroExecutionException {
525        for (Path superclassPath : MODULE_SUPER_CLASS_PATHS) {
526            final Path fileNamePath = superclassPath.getFileName();
527            if (fileNamePath == null) {
528                throw new MacroExecutionException("Invalid superclass path: " + superclassPath);
529            }
530            final String superclassName = CommonUtil.getFileNameWithoutExtension(
531                fileNamePath.toString());
532            processModule(superclassName, superclassPath);
533            final Map<String, DetailNode> superclassPropertiesJavadocs =
534                JavadocScraperResultUtil.getPropertiesJavadocNode();
535            SUPER_CLASS_PROPERTIES_JAVADOCS.putAll(superclassPropertiesJavadocs);
536        }
537    }
538
539    /**
540     * Scrape the Javadocs of the class and its properties setters with
541     * ClassAndPropertiesSettersJavadocScraper.
542     *
543     * @param moduleName the name of the module.
544     * @param modulePath the module Path.
545     * @throws MacroExecutionException if an error occurs during processing.
546     */
547    private static void processModule(String moduleName, Path modulePath)
548            throws MacroExecutionException {
549        if (!Files.isRegularFile(modulePath)) {
550            final String message = String.format(Locale.ROOT,
551                    "File %s is not a file. Please check the 'modulePath' property.", modulePath);
552            throw new MacroExecutionException(message);
553        }
554        ClassAndPropertiesSettersJavadocScraper.initialize(moduleName);
555        final Checker checker = new Checker();
556        checker.setModuleClassLoader(Checker.class.getClassLoader());
557        final DefaultConfiguration scraperCheckConfig =
558                        new DefaultConfiguration(
559                                ClassAndPropertiesSettersJavadocScraper.class.getName());
560        final DefaultConfiguration defaultConfiguration =
561                new DefaultConfiguration("configuration");
562        final DefaultConfiguration treeWalkerConfig =
563                new DefaultConfiguration(TreeWalker.class.getName());
564        defaultConfiguration.addProperty(CHARSET, StandardCharsets.UTF_8.name());
565        defaultConfiguration.addChild(treeWalkerConfig);
566        treeWalkerConfig.addChild(scraperCheckConfig);
567        try {
568            checker.configure(defaultConfiguration);
569            final List<File> filesToProcess = List.of(modulePath.toFile());
570            checker.process(filesToProcess);
571            checker.destroy();
572        }
573        catch (CheckstyleException checkstyleException) {
574            final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName);
575            throw new MacroExecutionException(message, checkstyleException);
576        }
577    }
578
579    /**
580     * Get a set of properties for the given class.
581     *
582     * @param clss the class to get the properties for.
583     * @return a set of properties for the given class.
584     */
585    public static Set<String> getProperties(Class<?> clss) {
586        final Set<String> result = new TreeSet<>();
587        final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss);
588
589        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
590            if (propertyDescriptor.getWriteMethod() != null) {
591                result.add(propertyDescriptor.getName());
592            }
593        }
594
595        return result;
596    }
597
598    /**
599     * Checks if the property is a global property. Global properties come from the base classes
600     * and are common to all checks. For example id, severity, tabWidth, etc.
601     *
602     * @param clss the class of the module.
603     * @param propertyName the name of the property.
604     * @return true if the property is a global property.
605     */
606    private static boolean isGlobalProperty(Class<?> clss, String propertyName) {
607        return AbstractCheck.class.isAssignableFrom(clss)
608                    && CHECK_PROPERTIES.contains(propertyName)
609                || AbstractJavadocCheck.class.isAssignableFrom(clss)
610                    && JAVADOC_CHECK_PROPERTIES.contains(propertyName)
611                || AbstractFileSetCheck.class.isAssignableFrom(clss)
612                    && FILESET_PROPERTIES.contains(propertyName);
613    }
614
615    /**
616     * Checks if the property is supposed to be documented.
617     *
618     * @param clss the class of the module.
619     * @param propertyName the name of the property.
620     * @return true if the property is supposed to be documented.
621     */
622    private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) {
623        return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName);
624    }
625
626    /**
627     * Gets properties that are not explicitly captured but should be documented if
628     * certain conditions are met.
629     *
630     * @param instance the instance of the module.
631     * @param clss the class of the module.
632     * @return the non explicit properties.
633     */
634    private static Set<String> getNonExplicitProperties(
635            Object instance, Class<?> clss) {
636        final Set<String> result = new TreeSet<>();
637        if (AbstractCheck.class.isAssignableFrom(clss)) {
638            final AbstractCheck check = (AbstractCheck) instance;
639
640            final int[] acceptableTokens = check.getAcceptableTokens();
641            Arrays.sort(acceptableTokens);
642            final int[] defaultTokens = check.getDefaultTokens();
643            Arrays.sort(defaultTokens);
644            final int[] requiredTokens = check.getRequiredTokens();
645            Arrays.sort(requiredTokens);
646
647            if (!Arrays.equals(acceptableTokens, defaultTokens)
648                    || !Arrays.equals(acceptableTokens, requiredTokens)) {
649                result.add(TOKENS);
650            }
651        }
652
653        if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
654            final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
655            result.add("violateExecutionOnNonTightHtml");
656
657            final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
658            Arrays.sort(acceptableJavadocTokens);
659            final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
660            Arrays.sort(defaultJavadocTokens);
661            final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
662            Arrays.sort(requiredJavadocTokens);
663
664            if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
665                    || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
666                result.add(JAVADOC_TOKENS);
667            }
668        }
669
670        if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
671            result.add(FILE_EXTENSIONS);
672        }
673        return result;
674    }
675
676    /**
677     * Get the description of the property.
678     *
679     * @param propertyName the name of the property.
680     * @param javadoc the Javadoc of the property setter method.
681     * @param moduleName the name of the module.
682     * @return the description of the property.
683     * @throws MacroExecutionException if the description could not be extracted.
684     */
685    public static String getPropertyDescription(
686            String propertyName, DetailNode javadoc, String moduleName)
687            throws MacroExecutionException {
688        final String description;
689        if (TOKENS.equals(propertyName)) {
690            description = "tokens to check";
691        }
692        else if (JAVADOC_TOKENS.equals(propertyName)) {
693            description = "javadoc tokens to check";
694        }
695        else {
696            final String descriptionString = SETTER_PATTERN.matcher(
697                    DescriptionExtractor.getDescriptionFromJavadoc(javadoc, moduleName))
698                    .replaceFirst("");
699
700            final String firstLetterCapitalized = descriptionString.substring(0, 1)
701                    .toUpperCase(Locale.ROOT);
702            description = firstLetterCapitalized + descriptionString.substring(1);
703        }
704        return description;
705    }
706
707    /**
708     * Get the since version of the property.
709     *
710     * @param moduleName the name of the module.
711     * @param moduleJavadoc the Javadoc of the module.
712     * @param propertyName the name of the property.
713     * @param propertyJavadoc the Javadoc of the property setter method.
714     * @return the since version of the property.
715     * @throws MacroExecutionException if the module since version could not be extracted.
716     */
717    public static String getPropertySinceVersion(String moduleName, DetailNode moduleJavadoc,
718                                         String propertyName, DetailNode propertyJavadoc)
719            throws MacroExecutionException {
720        final String sinceVersion;
721
722        final Optional<String> specifiedPropertyVersionInModule =
723            getSpecifiedPropertyVersionInModule(propertyName, moduleJavadoc);
724
725        if (specifiedPropertyVersionInModule.isPresent()) {
726            sinceVersion = specifiedPropertyVersionInModule.get();
727        }
728        else {
729            final String moduleSince = getSinceVersionFromJavadoc(moduleJavadoc);
730
731            if (moduleSince == null) {
732                throw new MacroExecutionException(
733                        "Missing @since on module " + moduleName);
734            }
735
736            String propertySince = null;
737            if (propertyJavadoc != null) {
738                propertySince = getSinceVersionFromJavadoc(propertyJavadoc);
739            }
740
741            if (propertySince != null && isVersionAtLeast(propertySince, moduleSince)) {
742                sinceVersion = propertySince;
743            }
744            else {
745                sinceVersion = moduleSince;
746            }
747        }
748
749        return sinceVersion;
750    }
751
752    /**
753     * Gets the specifically indicated version of module's property from the javadoc of module.
754     *
755     * @param propertyName the name of property.
756     * @param moduleJavadoc the javadoc of module.
757     * @return the specific since version of module's property.
758     * @throws MacroExecutionException if the module since version could not be extracted.
759     */
760    private static Optional<String> getSpecifiedPropertyVersionInModule(String propertyName,
761                                                                        DetailNode moduleJavadoc)
762            throws MacroExecutionException {
763        Optional<String> specifiedVersion = Optional.empty();
764
765        final Optional<DetailNode> propertyNodeFromModuleJavadoc =
766            getPropertyJavadocNodeInModule(propertyName, moduleJavadoc);
767
768        if (propertyNodeFromModuleJavadoc.isPresent()) {
769            final List<DetailNode> propertyModuleTextNodes = getNodesOfSpecificType(
770                propertyNodeFromModuleJavadoc.get().getChildren(), JavadocTokenTypes.TEXT);
771
772            final Optional<String> sinceVersionLine = propertyModuleTextNodes.stream()
773                .map(DetailNode::getText)
774                .filter(text -> text.startsWith(WHITESPACE + SINCE_VERSION))
775                .findFirst();
776
777            if (sinceVersionLine.isPresent()) {
778                final String sinceVersionText = sinceVersionLine.get();
779                final int sinceVersionIndex = sinceVersionText.indexOf('.') - 1;
780
781                if (sinceVersionIndex > 0) {
782                    specifiedVersion = Optional.of(sinceVersionText.substring(sinceVersionIndex));
783                }
784                else {
785                    throw new MacroExecutionException(sinceVersionText
786                        + " has no valid version, at least one '.' is expected.");
787                }
788            }
789        }
790        else {
791            throw new MacroExecutionException("Property '" + propertyName
792                + "' is not found in module's javadoc.");
793        }
794
795        return specifiedVersion;
796    }
797
798    /**
799     * Gets the javadoc node part of the property from the javadoc of the module.
800     *
801     * @param propertyName the name of property.
802     * @param moduleJavadoc the javadoc of module.
803     * @return the Optional of javadoc node part of the property.
804     */
805    public static Optional<DetailNode> getPropertyJavadocNodeInModule(String propertyName,
806                                                             DetailNode moduleJavadoc) {
807        final List<DetailNode> htmlElementNodes = getNodesOfSpecificType(
808            moduleJavadoc.getChildren(), JavadocTokenTypes.HTML_ELEMENT);
809
810        final List<DetailNode> ulTags = htmlElementNodes.stream()
811            .map(JavadocUtil::getFirstChild)
812            .filter(child -> {
813                final boolean isHtmlTag = child.getType() == JavadocTokenTypes.HTML_TAG;
814                final DetailNode htmlTagNameNode = JavadocUtil.findFirstToken(
815                    JavadocUtil.getFirstChild(child), JavadocTokenTypes.HTML_TAG_NAME);
816
817                return isHtmlTag && "ul".equals(htmlTagNameNode.getText());
818            })
819            .toList();
820        final DetailNode[] childrenOfUlTags = ulTags.stream()
821            .flatMap(ulTag -> Arrays.stream(ulTag.getChildren()))
822            .toArray(DetailNode[]::new);
823        final List<DetailNode> innerHtmlElementsOfUlTags =
824            getNodesOfSpecificType(childrenOfUlTags, JavadocTokenTypes.HTML_ELEMENT);
825
826        final List<DetailNode> liTags = innerHtmlElementsOfUlTags.stream()
827            .map(JavadocUtil::getFirstChild)
828            .filter(tag -> tag.getType() == JavadocTokenTypes.LI)
829            .toList();
830
831        final List<DetailNode> liTagsInlineTexts = liTags.stream()
832            .map(liTag -> JavadocUtil.findFirstToken(liTag, JavadocTokenTypes.JAVADOC_INLINE_TAG))
833            .filter(Objects::nonNull)
834            .map(inlineTag -> JavadocUtil.findFirstToken(inlineTag, JavadocTokenTypes.TEXT))
835            .toList();
836
837        return liTagsInlineTexts.stream()
838            .filter(text -> text.getText().equals(propertyName))
839            .map(textNode -> textNode.getParent().getParent())
840            .findFirst();
841
842    }
843
844    /**
845     * Gets all javadoc nodes of selected type.
846     *
847     * @param allNodes Nodes to choose from.
848     * @param neededType the Javadoc token type to select.
849     * @return the List of DetailNodes of selected type.
850     */
851    public static List<DetailNode> getNodesOfSpecificType(DetailNode[] allNodes, int neededType) {
852        return Arrays.stream(allNodes)
853            .filter(child -> child.getType() == neededType)
854            .toList();
855    }
856
857    /**
858     * Extract the since version from the Javadoc.
859     *
860     * @param javadoc the Javadoc to extract the since version from.
861     * @return the since version of the setter.
862     */
863    @Nullable
864    private static String getSinceVersionFromJavadoc(DetailNode javadoc) {
865        final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc);
866        return Optional.ofNullable(sinceJavadocTag)
867            .map(tag -> JavadocUtil.findFirstToken(tag, JavadocTokenTypes.DESCRIPTION))
868            .map(description -> JavadocUtil.findFirstToken(description, JavadocTokenTypes.TEXT))
869            .map(DetailNode::getText)
870            .orElse(null);
871    }
872
873    /**
874     * Find the since Javadoc tag node in the given Javadoc.
875     *
876     * @param javadoc the Javadoc to search.
877     * @return the since Javadoc tag node or null if not found.
878     */
879    private static DetailNode getSinceJavadocTag(DetailNode javadoc) {
880        final DetailNode[] children = javadoc.getChildren();
881        DetailNode javadocTagWithSince = null;
882        for (final DetailNode child : children) {
883            if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) {
884                final DetailNode sinceNode = JavadocUtil.findFirstToken(
885                        child, JavadocTokenTypes.SINCE_LITERAL);
886                if (sinceNode != null) {
887                    javadocTagWithSince = child;
888                    break;
889                }
890            }
891        }
892        return javadocTagWithSince;
893    }
894
895    /**
896     * Returns {@code true} if {@code actualVersion} ≥ {@code requiredVersion}.
897     * Both versions have any trailing "-SNAPSHOT" stripped before comparison.
898     *
899     * @param actualVersion   e.g. "8.3" or "8.3-SNAPSHOT"
900     * @param requiredVersion e.g. "8.3"
901     * @return {@code true} if actualVersion exists, and, numerically, is at least requiredVersion
902     */
903    private static boolean isVersionAtLeast(String actualVersion,
904                                            String requiredVersion) {
905        final Version actualVersionParsed = Version.parse(actualVersion);
906        final Version requiredVersionParsed = Version.parse(requiredVersion);
907
908        return actualVersionParsed.compareTo(requiredVersionParsed) >= 0;
909    }
910
911    /**
912     * Get the type of the property.
913     *
914     * @param field the field to get the type of.
915     * @param propertyName the name of the property.
916     * @param moduleName the name of the module.
917     * @param instance the instance of the module.
918     * @return the type of the property.
919     * @throws MacroExecutionException if an error occurs during getting the type.
920     */
921    public static String getType(Field field, String propertyName,
922                                 String moduleName, Object instance)
923            throws MacroExecutionException {
924        final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance);
925        return Optional.ofNullable(field)
926                .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
927                .map(propertyType -> propertyType.value().getDescription())
928                .orElseGet(fieldClass::getSimpleName);
929    }
930
931    /**
932     * Get the default value of the property.
933     *
934     * @param propertyName the name of the property.
935     * @param field the field to get the default value of.
936     * @param classInstance the instance of the class to get the default value of.
937     * @param moduleName the name of the module.
938     * @return the default value of the property.
939     * @throws MacroExecutionException if an error occurs during getting the default value.
940     * @noinspection IfStatementWithTooManyBranches
941     * @noinspectionreason IfStatementWithTooManyBranches - complex nature of getting properties
942     *      from XML files requires giant if/else statement
943     */
944    // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
945    public static String getDefaultValue(String propertyName, Field field,
946                                         Object classInstance, String moduleName)
947            throws MacroExecutionException {
948        final Object value = getFieldValue(field, classInstance);
949        final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, classInstance);
950        String result = null;
951        if (CHARSET.equals(propertyName)) {
952            result = "the charset property of the parent"
953                    + " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module";
954        }
955        else if (classInstance instanceof PropertyCacheFile) {
956            result = "null (no cache file)";
957        }
958        else if (fieldClass == boolean.class) {
959            result = value.toString();
960        }
961        else if (fieldClass == int.class) {
962            result = value.toString();
963        }
964        else if (fieldClass == int[].class) {
965            result = getIntArrayPropertyValue(value);
966        }
967        else if (fieldClass == double[].class) {
968            result = removeSquareBrackets(Arrays.toString((double[]) value).replace(".0", ""));
969            if (result.isEmpty()) {
970                result = CURLY_BRACKETS;
971            }
972        }
973        else if (fieldClass == String[].class) {
974            result = getStringArrayPropertyValue(propertyName, value);
975        }
976        else if (fieldClass == URI.class || fieldClass == String.class) {
977            if (value != null) {
978                result = '"' + value.toString() + '"';
979            }
980        }
981        else if (fieldClass == Pattern.class) {
982            if (value != null) {
983                result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t")
984                        .replace("\r", "\\r").replace("\f", "\\f") + '"';
985            }
986        }
987        else if (fieldClass == Pattern[].class) {
988            result = getPatternArrayPropertyValue(value);
989        }
990        else if (fieldClass.isEnum()) {
991            if (value != null) {
992                result = value.toString().toLowerCase(Locale.ENGLISH);
993            }
994        }
995        else if (fieldClass == AccessModifierOption[].class) {
996            result = removeSquareBrackets(Arrays.toString((Object[]) value));
997        }
998        else {
999            final String message = String.format(Locale.ROOT,
1000                    "Unknown property type: %s", fieldClass.getSimpleName());
1001            throw new MacroExecutionException(message);
1002        }
1003
1004        if (result == null) {
1005            result = "null";
1006        }
1007
1008        return result;
1009    }
1010
1011    /**
1012     * Gets the name of the bean property's default value for the Pattern array class.
1013     *
1014     * @param fieldValue The bean property's value
1015     * @return String form of property's default value
1016     */
1017    private static String getPatternArrayPropertyValue(Object fieldValue) {
1018        Object value = fieldValue;
1019        if (value instanceof Collection<?> collection) {
1020
1021            value = collection.stream()
1022                    .map(Pattern.class::cast)
1023                    .toArray(Pattern[]::new);
1024        }
1025
1026        String result = "";
1027        if (value != null && Array.getLength(value) > 0) {
1028            result = removeSquareBrackets(
1029                    Arrays.stream((Pattern[]) value)
1030                    .map(Pattern::pattern)
1031                    .collect(Collectors.joining(COMMA_SPACE)));
1032        }
1033
1034        if (result.isEmpty()) {
1035            result = CURLY_BRACKETS;
1036        }
1037        return result;
1038    }
1039
1040    /**
1041     * Removes square brackets [ and ] from the given string.
1042     *
1043     * @param value the string to remove square brackets from.
1044     * @return the string without square brackets.
1045     */
1046    private static String removeSquareBrackets(String value) {
1047        return value
1048                .replace("[", "")
1049                .replace("]", "");
1050    }
1051
1052    /**
1053     * Gets the name of the bean property's default value for the string array class.
1054     *
1055     * @param propertyName The bean property's name
1056     * @param value The bean property's value
1057     * @return String form of property's default value
1058     */
1059    private static String getStringArrayPropertyValue(String propertyName, Object value) {
1060        String result;
1061        if (value == null) {
1062            result = "";
1063        }
1064        else {
1065            try (Stream<?> valuesStream = getValuesStream(value)) {
1066                result = valuesStream
1067                    .map(String.class::cast)
1068                    .sorted()
1069                    .collect(Collectors.joining(COMMA_SPACE));
1070            }
1071        }
1072
1073        if (result.isEmpty()) {
1074            if (FILE_EXTENSIONS.equals(propertyName)) {
1075                result = "all files";
1076            }
1077            else {
1078                result = CURLY_BRACKETS;
1079            }
1080        }
1081        return result;
1082    }
1083
1084    /**
1085     * Generates a stream of values from the given value.
1086     *
1087     * @param value the value to generate the stream from.
1088     * @return the stream of values.
1089     */
1090    private static Stream<?> getValuesStream(Object value) {
1091        final Stream<?> valuesStream;
1092        if (value instanceof Collection<?> collection) {
1093            valuesStream = collection.stream();
1094        }
1095        else {
1096            final Object[] array = (Object[]) value;
1097            valuesStream = Arrays.stream(array);
1098        }
1099        return valuesStream;
1100    }
1101
1102    /**
1103     * Returns the name of the bean property's default value for the int array class.
1104     *
1105     * @param value The bean property's value.
1106     * @return String form of property's default value.
1107     */
1108    private static String getIntArrayPropertyValue(Object value) {
1109        try (IntStream stream = getIntStream(value)) {
1110            String result = stream
1111                    .mapToObj(TokenUtil::getTokenName)
1112                    .sorted()
1113                    .collect(Collectors.joining(COMMA_SPACE));
1114            if (result.isEmpty()) {
1115                result = CURLY_BRACKETS;
1116            }
1117            return result;
1118        }
1119    }
1120
1121    /**
1122     * Get the int stream from the given value.
1123     *
1124     * @param value the value to get the int stream from.
1125     * @return the int stream.
1126     */
1127    private static IntStream getIntStream(Object value) {
1128        final IntStream stream;
1129        if (value instanceof Collection<?> collection) {
1130            stream = collection.stream()
1131                    .mapToInt(int.class::cast);
1132        }
1133        else if (value instanceof BitSet) {
1134            stream = ((BitSet) value).stream();
1135        }
1136        else {
1137            stream = Arrays.stream((int[]) value);
1138        }
1139        return stream;
1140    }
1141
1142    /**
1143     * Gets the class of the given field.
1144     *
1145     * @param field the field to get the class of.
1146     * @param propertyName the name of the property.
1147     * @param moduleName the name of the module.
1148     * @param instance the instance of the module.
1149     * @return the class of the field.
1150     * @throws MacroExecutionException if an error occurs during getting the class.
1151     */
1152    // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
1153    private static Class<?> getFieldClass(Field field, String propertyName,
1154                                          String moduleName, Object instance)
1155            throws MacroExecutionException {
1156        Class<?> result = null;
1157
1158        if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1159                .contains(moduleName + DOT + propertyName)) {
1160            result = getPropertyClass(propertyName, instance);
1161        }
1162        if (field != null && result == null) {
1163            result = field.getType();
1164        }
1165        if (result == null) {
1166            throw new MacroExecutionException(
1167                    "Could not find field " + propertyName + " in class " + moduleName);
1168        }
1169        if (field != null && (result == List.class || result == Set.class)) {
1170            final ParameterizedType type = (ParameterizedType) field.getGenericType();
1171            final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1172
1173            if (parameterClass == Integer.class) {
1174                result = int[].class;
1175            }
1176            else if (parameterClass == String.class) {
1177                result = String[].class;
1178            }
1179            else if (parameterClass == Pattern.class) {
1180                result = Pattern[].class;
1181            }
1182            else {
1183                final String message = "Unknown parameterized type: "
1184                        + parameterClass.getSimpleName();
1185                throw new MacroExecutionException(message);
1186            }
1187        }
1188        else if (result == BitSet.class) {
1189            result = int[].class;
1190        }
1191
1192        return result;
1193    }
1194
1195    /**
1196     * Gets the class of the given java property.
1197     *
1198     * @param propertyName the name of the property.
1199     * @param instance the instance of the module.
1200     * @return the class of the java property.
1201     * @throws MacroExecutionException if an error occurs during getting the class.
1202     */
1203    // -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field
1204    public static Class<?> getPropertyClass(String propertyName, Object instance)
1205            throws MacroExecutionException {
1206        final Class<?> result;
1207        try {
1208            final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1209                    propertyName);
1210            result = descriptor.getPropertyType();
1211        }
1212        catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1213            throw new MacroExecutionException(exc.getMessage(), exc);
1214        }
1215        return result;
1216    }
1217
1218    /**
1219     * Get the difference between two lists of tokens.
1220     *
1221     * @param tokens the list of tokens to remove from.
1222     * @param subtractions the tokens to remove.
1223     * @return the difference between the two lists.
1224     */
1225    public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1226        final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1227                .boxed()
1228                .collect(Collectors.toUnmodifiableSet());
1229        return Arrays.stream(tokens)
1230                .boxed()
1231                .filter(token -> !subtractionsSet.contains(token))
1232                .toList();
1233    }
1234
1235    /**
1236     * Gets the field with the given name from the given class.
1237     *
1238     * @param fieldClass the class to get the field from.
1239     * @param propertyName the name of the field.
1240     * @return the field we are looking for.
1241     */
1242    public static Field getField(Class<?> fieldClass, String propertyName) {
1243        Field result = null;
1244        Class<?> currentClass = fieldClass;
1245
1246        while (!Object.class.equals(currentClass)) {
1247            try {
1248                result = currentClass.getDeclaredField(propertyName);
1249                result.trySetAccessible();
1250                break;
1251            }
1252            catch (NoSuchFieldException ignored) {
1253                currentClass = currentClass.getSuperclass();
1254            }
1255        }
1256
1257        return result;
1258    }
1259
1260    /**
1261     * Constructs string with relative link to the provided document.
1262     *
1263     * @param moduleName the name of the module.
1264     * @param document the path of the document.
1265     * @return relative link to the document.
1266     * @throws MacroExecutionException if link to the document cannot be constructed.
1267     */
1268    public static String getLinkToDocument(String moduleName, String document)
1269            throws MacroExecutionException {
1270        final Path templatePath = getTemplatePath(moduleName.replace("Check", ""));
1271        if (templatePath == null) {
1272            throw new MacroExecutionException(
1273                    String.format(Locale.ROOT,
1274                            "Could not find template for %s", moduleName));
1275        }
1276        final Path templatePathParent = templatePath.getParent();
1277        if (templatePathParent == null) {
1278            throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1279        }
1280        return templatePathParent
1281                .relativize(Path.of(SRC, "site/xdoc", document))
1282                .toString()
1283                .replace(".xml", ".html")
1284                .replace('\\', '/');
1285    }
1286
1287    /** Utility class for extracting description from a method's Javadoc. */
1288    private static final class DescriptionExtractor {
1289
1290        /**
1291         * Extracts the description from the javadoc detail node. Performs a DFS traversal on the
1292         * detail node and extracts the text nodes.
1293         *
1294         * @param javadoc the Javadoc to extract the description from.
1295         * @param moduleName the name of the module.
1296         * @return the description of the setter.
1297         * @throws MacroExecutionException if the description could not be extracted.
1298         * @noinspection TooBroadScope
1299         * @noinspectionreason TooBroadScope - complex nature of method requires large scope
1300         */
1301        // -@cs[NPathComplexity] Splitting would not make the code more readable
1302        // -@cs[CyclomaticComplexity] Splitting would not make the code more readable.
1303        private static String getDescriptionFromJavadoc(DetailNode javadoc, String moduleName)
1304                throws MacroExecutionException {
1305            boolean isInCodeLiteral = false;
1306            boolean isInHtmlElement = false;
1307            boolean isInHrefAttribute = false;
1308            final StringBuilder description = new StringBuilder(128);
1309            final Deque<DetailNode> queue = new ArrayDeque<>();
1310            final List<DetailNode> descriptionNodes = getDescriptionNodes(javadoc);
1311            Lists.reverse(descriptionNodes).forEach(queue::push);
1312
1313            // Perform DFS traversal on description nodes
1314            while (!queue.isEmpty()) {
1315                final DetailNode node = queue.pop();
1316                Lists.reverse(Arrays.asList(node.getChildren())).forEach(queue::push);
1317
1318                if (node.getType() == JavadocTokenTypes.HTML_TAG_NAME
1319                        && "href".equals(node.getText())) {
1320                    isInHrefAttribute = true;
1321                }
1322                if (isInHrefAttribute && node.getType() == JavadocTokenTypes.ATTR_VALUE) {
1323                    final String href = node.getText();
1324                    if (href.contains(CHECKSTYLE_ORG_URL)) {
1325                        handleInternalLink(description, moduleName, href);
1326                    }
1327                    else {
1328                        description.append(href);
1329                    }
1330
1331                    isInHrefAttribute = false;
1332                    continue;
1333                }
1334                if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
1335                    isInHtmlElement = true;
1336                }
1337                if (node.getType() == JavadocTokenTypes.END
1338                        && node.getParent().getType() == JavadocTokenTypes.HTML_ELEMENT_END) {
1339                    description.append(node.getText());
1340                    isInHtmlElement = false;
1341                }
1342                if (node.getType() == JavadocTokenTypes.TEXT
1343                        // If a node has children, its text is not part of the description
1344                        || isInHtmlElement && node.getChildren().length == 0
1345                            // Some HTML elements span multiple lines, so we avoid the asterisk
1346                            && node.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
1347                    description.append(node.getText());
1348                }
1349                if (node.getType() == JavadocTokenTypes.CODE_LITERAL) {
1350                    isInCodeLiteral = true;
1351                    description.append("<code>");
1352                }
1353                if (isInCodeLiteral
1354                        && node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
1355                    isInCodeLiteral = false;
1356                    description.append("</code>");
1357                }
1358            }
1359            return description.toString().trim();
1360        }
1361
1362        /**
1363         * Converts the href value to a relative link to the document and appends it to the
1364         * description.
1365         *
1366         * @param description the description to append the relative link to.
1367         * @param moduleName the name of the module.
1368         * @param value the href value.
1369         * @throws MacroExecutionException if the relative link could not be created.
1370         */
1371        private static void handleInternalLink(StringBuilder description,
1372                                               String moduleName, String value)
1373                throws MacroExecutionException {
1374            String href = value;
1375            href = href.replace(CHECKSTYLE_ORG_URL, "");
1376            // Remove first and last characters, they are always double quotes
1377            href = href.substring(1, href.length() - 1);
1378
1379            final String relativeHref = getLinkToDocument(moduleName, href);
1380            final char doubleQuote = '\"';
1381            description.append(doubleQuote).append(relativeHref).append(doubleQuote);
1382        }
1383
1384        /**
1385         * Extracts description nodes from javadoc.
1386         *
1387         * @param javadoc the Javadoc to extract the description from.
1388         * @return the description nodes of the setter.
1389         */
1390        private static List<DetailNode> getDescriptionNodes(DetailNode javadoc) {
1391            final DetailNode[] children = javadoc.getChildren();
1392            final List<DetailNode> descriptionNodes = new ArrayList<>();
1393            for (final DetailNode child : children) {
1394                if (isEndOfDescription(child)) {
1395                    break;
1396                }
1397                descriptionNodes.add(child);
1398            }
1399            return descriptionNodes;
1400        }
1401
1402        /**
1403         * Determines if the given child index is the end of the description. The end of the
1404         * description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK, NEWLINE,
1405         * LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the one below
1406         * this line.
1407         *
1408         * @param child the child to check.
1409         * @return true if the given child index is the end of the description.
1410         */
1411        private static boolean isEndOfDescription(DetailNode child) {
1412            final DetailNode nextSibling = JavadocUtil.getNextSibling(child);
1413            final DetailNode secondNextSibling = JavadocUtil.getNextSibling(nextSibling);
1414            final DetailNode thirdNextSibling = JavadocUtil.getNextSibling(secondNextSibling);
1415
1416            return child.getType() == JavadocTokenTypes.NEWLINE
1417                        && nextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK
1418                        && secondNextSibling.getType() == JavadocTokenTypes.NEWLINE
1419                        && thirdNextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK;
1420        }
1421    }
1422}