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