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