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