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