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