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