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