View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.beans.PropertyDescriptor;
23  import java.io.File;
24  import java.io.IOException;
25  import java.lang.reflect.Array;
26  import java.lang.reflect.Field;
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.ParameterizedType;
29  import java.net.URI;
30  import java.nio.charset.StandardCharsets;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.util.ArrayDeque;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.BitSet;
37  import java.util.Collection;
38  import java.util.Deque;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.LinkedHashMap;
42  import java.util.List;
43  import java.util.Locale;
44  import java.util.Map;
45  import java.util.Optional;
46  import java.util.Set;
47  import java.util.TreeSet;
48  import java.util.regex.Pattern;
49  import java.util.stream.Collectors;
50  import java.util.stream.IntStream;
51  import java.util.stream.Stream;
52  
53  import javax.annotation.Nullable;
54  
55  import org.apache.commons.beanutils.PropertyUtils;
56  import org.apache.maven.doxia.macro.MacroExecutionException;
57  
58  import com.google.common.collect.Lists;
59  import com.puppycrawl.tools.checkstyle.Checker;
60  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
61  import com.puppycrawl.tools.checkstyle.ModuleFactory;
62  import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
63  import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
64  import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
65  import com.puppycrawl.tools.checkstyle.TreeWalker;
66  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
67  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
68  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
69  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
70  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
71  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
72  import com.puppycrawl.tools.checkstyle.api.DetailNode;
73  import com.puppycrawl.tools.checkstyle.api.Filter;
74  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
75  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
76  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
77  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
78  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
79  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
80  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
81  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
82  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
83  
84  /**
85   * Utility class for site generation.
86   */
87  public final class SiteUtil {
88  
89      /** The string 'tokens'. */
90      public static final String TOKENS = "tokens";
91      /** The string 'javadocTokens'. */
92      public static final String JAVADOC_TOKENS = "javadocTokens";
93      /** The string '.'. */
94      public static final String DOT = ".";
95      /** The string ', '. */
96      public static final String COMMA_SPACE = ", ";
97      /** The string 'TokenTypes'. */
98      public static final String TOKEN_TYPES = "TokenTypes";
99      /** 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 }