View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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     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             final boolean preserveOrder = hasPreserveOrderAnnotation(field);
951             result = getStringArrayPropertyValue(value, preserveOrder);
952         }
953         else if (fieldClass == Pattern.class) {
954             if (value != null) {
955                 result = value.toString().replace("\n", "\\n").replace("\t", "\\t")
956                         .replace("\r", "\\r").replace("\f", "\\f");
957             }
958         }
959         else if (fieldClass == Pattern[].class) {
960             result = getPatternArrayPropertyValue(value);
961         }
962         else if (fieldClass.isEnum()) {
963             if (value != null) {
964                 result = value.toString().toLowerCase(Locale.ENGLISH);
965             }
966         }
967         else if (fieldClass == AccessModifierOption[].class) {
968             result = removeSquareBrackets(Arrays.toString((Object[]) value));
969         }
970 
971         if (result == null) {
972             result = "null";
973         }
974 
975         return result;
976     }
977 
978     /**
979      * Checks if a field has the {@code PreserveOrder} annotation.
980      *
981      * @param field the field to check
982      * @return true if the field has {@code PreserveOrder} annotation, false otherwise
983      */
984     private static boolean hasPreserveOrderAnnotation(Field field) {
985         return field != null && field.isAnnotationPresent(PreserveOrder.class);
986     }
987 
988     /**
989      * Gets the name of the bean property's default value for the Pattern array class.
990      *
991      * @param fieldValue The bean property's value
992      * @return String form of property's default value
993      */
994     private static String getPatternArrayPropertyValue(Object fieldValue) {
995         Object value = fieldValue;
996         if (value instanceof Collection<?> collection) {
997             value = collection.stream()
998                     .map(Pattern.class::cast)
999                     .toArray(Pattern[]::new);
1000         }
1001 
1002         String result = "";
1003         if (value != null && Array.getLength(value) > 0) {
1004             result = removeSquareBrackets(
1005                     Arrays.stream((Pattern[]) value)
1006                     .map(Pattern::pattern)
1007                     .collect(Collectors.joining(COMMA_SPACE)));
1008         }
1009 
1010         return result;
1011     }
1012 
1013     /**
1014      * Removes square brackets [ and ] from the given string.
1015      *
1016      * @param value the string to remove square brackets from.
1017      * @return the string without square brackets.
1018      */
1019     private static String removeSquareBrackets(String value) {
1020         return value
1021                 .replace("[", "")
1022                 .replace("]", "");
1023     }
1024 
1025     /**
1026      * Gets the name of the bean property's default value for the string array class.
1027      *
1028      * @param value The bean property's value
1029      * @param preserveOrder whether to preserve the original order
1030      * @return String form of property's default value
1031      */
1032     private static String getStringArrayPropertyValue(Object value, boolean preserveOrder) {
1033         final String result;
1034         if (value == null) {
1035             result = "";
1036         }
1037         else {
1038             try (Stream<?> valuesStream = getValuesStream(value)) {
1039                 final List<String> stringList = valuesStream
1040                     .map(String.class::cast)
1041                     .collect(Collectors.toCollection(ArrayList<String>::new));
1042 
1043                 if (preserveOrder) {
1044                     result = String.join(COMMA_SPACE, stringList);
1045                 }
1046                 else {
1047                     result = stringList.stream()
1048                     .sorted()
1049                     .collect(Collectors.joining(COMMA_SPACE));
1050                 }
1051             }
1052         }
1053         return result;
1054     }
1055 
1056     /**
1057      * Generates a stream of values from the given value.
1058      *
1059      * @param value the value to generate the stream from.
1060      * @return the stream of values.
1061      */
1062     private static Stream<?> getValuesStream(Object value) {
1063         final Stream<?> valuesStream;
1064         if (value instanceof Collection<?> collection) {
1065             valuesStream = collection.stream();
1066         }
1067         else {
1068             final Object[] array = (Object[]) value;
1069             valuesStream = Arrays.stream(array);
1070         }
1071         return valuesStream;
1072     }
1073 
1074     /**
1075      * Returns the name of the bean property's default value for the int array class.
1076      *
1077      * @param value The bean property's value.
1078      * @return String form of property's default value.
1079      */
1080     private static String getIntArrayPropertyValue(Object value) {
1081         try (IntStream stream = getIntStream(value)) {
1082             return stream
1083                     .mapToObj(TokenUtil::getTokenName)
1084                     .sorted()
1085                     .collect(Collectors.joining(COMMA_SPACE));
1086         }
1087     }
1088 
1089     /**
1090      * Get the int stream from the given value.
1091      *
1092      * @param value the value to get the int stream from.
1093      * @return the int stream.
1094      * @noinspection ChainOfInstanceofChecks
1095      * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at
1096      *                     <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a>
1097      */
1098     private static IntStream getIntStream(Object value) {
1099         final IntStream stream;
1100         if (value instanceof Collection<?> collection) {
1101             stream = collection.stream()
1102                     .mapToInt(int.class::cast);
1103         }
1104         else if (value instanceof BitSet set) {
1105             stream = set.stream();
1106         }
1107         else {
1108             stream = Arrays.stream((int[]) value);
1109         }
1110         return stream;
1111     }
1112 
1113     /**
1114      * Gets the class of the given field.
1115      *
1116      * @param field the field to get the class of.
1117      * @param propertyName the name of the property.
1118      * @param moduleName the name of the module.
1119      * @param instance the instance of the module.
1120      * @return the class of the field.
1121      * @throws MacroExecutionException if an error occurs during getting the class.
1122      */
1123     // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
1124     // -@cs[ForbidWildcardAsReturnType] Implied by design to return different types
1125     public static Class<?> getFieldClass(Field field, String propertyName,
1126                                           String moduleName, Object instance)
1127             throws MacroExecutionException {
1128         Class<?> result = null;
1129 
1130         if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1131                 .contains(moduleName + DOT + propertyName)) {
1132             result = getPropertyClass(propertyName, instance);
1133         }
1134         if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
1135             result = String[].class;
1136         }
1137         if (field != null && result == null) {
1138             result = field.getType();
1139         }
1140 
1141         if (result == null) {
1142             throw new MacroExecutionException(
1143                     "Could not find field " + propertyName + " in class " + moduleName);
1144         }
1145 
1146         if (field != null && (result == List.class || result == Set.class)) {
1147             final ParameterizedType type = (ParameterizedType) field.getGenericType();
1148             final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1149 
1150             if (parameterClass == Integer.class) {
1151                 result = int[].class;
1152             }
1153             else if (parameterClass == String.class) {
1154                 result = String[].class;
1155             }
1156             else if (parameterClass == Pattern.class) {
1157                 result = Pattern[].class;
1158             }
1159             else {
1160                 final String message = "Unknown parameterized type: "
1161                         + parameterClass.getSimpleName();
1162                 throw new MacroExecutionException(message);
1163             }
1164         }
1165         else if (result == BitSet.class) {
1166             result = int[].class;
1167         }
1168 
1169         return result;
1170     }
1171 
1172     /**
1173      * Gets the class of the given java property.
1174      *
1175      * @param propertyName the name of the property.
1176      * @param instance the instance of the module.
1177      * @return the class of the java property.
1178      * @throws MacroExecutionException if an error occurs during getting the class.
1179      */
1180     // -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field
1181     public static Class<?> getPropertyClass(String propertyName, Object instance)
1182             throws MacroExecutionException {
1183         final Class<?> result;
1184         try {
1185             final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1186                     propertyName);
1187             result = descriptor.getPropertyType();
1188         }
1189         catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1190             throw new MacroExecutionException("Failed to retrieve property type", exc);
1191         }
1192         return result;
1193     }
1194 
1195     /**
1196      * Get the difference between two lists of tokens.
1197      *
1198      * @param tokens the list of tokens to remove from.
1199      * @param subtractions the tokens to remove.
1200      * @return the difference between the two lists.
1201      */
1202     public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1203         final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1204                 .boxed()
1205                 .collect(Collectors.toUnmodifiableSet());
1206         return Arrays.stream(tokens)
1207                 .boxed()
1208                 .filter(token -> !subtractionsSet.contains(token))
1209                 .toList();
1210     }
1211 
1212     /**
1213      * Gets the field with the given name from the given class.
1214      *
1215      * @param fieldClass the class to get the field from.
1216      * @param propertyName the name of the field.
1217      * @return the field we are looking for.
1218      */
1219     public static Field getField(Class<?> fieldClass, String propertyName) {
1220         Field result = null;
1221         Class<?> currentClass = fieldClass;
1222 
1223         while (!Object.class.equals(currentClass)) {
1224             try {
1225                 result = currentClass.getDeclaredField(propertyName);
1226                 result.trySetAccessible();
1227                 break;
1228             }
1229             catch (NoSuchFieldException ignored) {
1230                 currentClass = currentClass.getSuperclass();
1231             }
1232         }
1233 
1234         return result;
1235     }
1236 
1237     /**
1238      * Constructs string with relative link to the provided document.
1239      *
1240      * @param moduleName the name of the module.
1241      * @param document the path of the document.
1242      * @return relative link to the document.
1243      * @throws MacroExecutionException if link to the document cannot be constructed.
1244      */
1245     public static String getLinkToDocument(String moduleName, String document)
1246             throws MacroExecutionException {
1247         final Path templatePath = getTemplatePath(FINAL_CHECK.matcher(moduleName).replaceAll(""));
1248         if (templatePath == null) {
1249             throw new MacroExecutionException(
1250                     String.format(Locale.ROOT,
1251                             "Could not find template for %s", moduleName));
1252         }
1253         final Path templatePathParent = templatePath.getParent();
1254         if (templatePathParent == null) {
1255             throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1256         }
1257         return templatePathParent
1258                 .relativize(Path.of(SRC, "site/xdoc", document))
1259                 .toString()
1260                 .replace(".xml", ".html")
1261                 .replace('\\', '/');
1262     }
1263 
1264     /**
1265      * Get all templates whose content contains properties macro.
1266      *
1267      * @return templates whose content contains properties macro.
1268      * @throws CheckstyleException if file could not be read.
1269      * @throws MacroExecutionException if template file is not found.
1270      */
1271     public static List<Path> getTemplatesThatContainPropertiesMacro()
1272             throws CheckstyleException, MacroExecutionException {
1273         final List<Path> result = new ArrayList<>();
1274         final Set<Path> templatesPaths = getXdocsTemplatesFilePaths();
1275         for (Path templatePath: templatesPaths) {
1276             final String content = getFileContents(templatePath);
1277             final String propertiesMacroDefinition = "<macro name=\"properties\"";
1278             if (content.contains(propertiesMacroDefinition)) {
1279                 result.add(templatePath);
1280             }
1281         }
1282         return result;
1283     }
1284 
1285     /**
1286      * Get file contents as string.
1287      *
1288      * @param pathToFile path to file.
1289      * @return file contents as string.
1290      * @throws CheckstyleException if file could not be read.
1291      */
1292     private static String getFileContents(Path pathToFile) throws CheckstyleException {
1293         final String content;
1294         try {
1295             content = Files.readString(pathToFile);
1296         }
1297         catch (IOException ioException) {
1298             final String message = String.format(Locale.ROOT, "Failed to read file: %s",
1299                     pathToFile);
1300             throw new CheckstyleException(message, ioException);
1301         }
1302         return content;
1303     }
1304 
1305     /**
1306      * Get the module name from the file. The module name is the file name without the extension.
1307      *
1308      * @param file file to extract the module name from.
1309      * @return module name.
1310      */
1311     public static String getModuleName(File file) {
1312         final String fullFileName = file.getName();
1313         return CommonUtil.getFileNameWithoutExtension(fullFileName);
1314     }
1315 
1316     /**
1317      * Extracts the description from the javadoc detail node. Performs a DFS traversal on the
1318      * detail node and extracts the text nodes. This description is additionally processed to
1319      * fit Xdoc format.
1320      *
1321      * @param javadoc the Javadoc to extract the description from.
1322      * @param moduleName the name of the module.
1323      * @return the description of the setter.
1324      * @throws MacroExecutionException if the description could not be extracted.
1325      */
1326     // -@cs[NPathComplexity] Splitting would not make the code more readable
1327     // -@cs[CyclomaticComplexity] Splitting would not make the code more readable.
1328     // -@cs[ExecutableStatementCount] Splitting would not make the code more readable.
1329     private static String getDescriptionFromJavadocForXdoc(DetailNode javadoc, String moduleName)
1330             throws MacroExecutionException {
1331         boolean isInCodeLiteral = false;
1332         boolean isInHtmlElement = false;
1333         boolean isInHrefAttribute = false;
1334         final StringBuilder description = new StringBuilder(128);
1335         final List<DetailNode> descriptionNodes = getFirstJavadocParagraphNodes(javadoc);
1336         DetailNode node = descriptionNodes.getFirst();
1337         final DetailNode endNode = descriptionNodes.getLast();
1338 
1339         while (node != null) {
1340             if (node.getType() == JavadocCommentsTokenTypes.TAG_ATTR_NAME
1341                     && "href".equals(node.getText())) {
1342                 isInHrefAttribute = true;
1343             }
1344             if (isInHrefAttribute && node.getType()
1345                      == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE) {
1346                 final String href = node.getText();
1347                 if (href.contains(CHECKSTYLE_ORG_URL)) {
1348                     DescriptionExtractor.handleInternalLink(description, moduleName, href);
1349                 }
1350                 else {
1351                     description.append(href);
1352                 }
1353 
1354                 isInHrefAttribute = false;
1355             }
1356             else {
1357                 if (node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
1358                     isInHtmlElement = true;
1359                 }
1360                 if (node.getType() == JavadocCommentsTokenTypes.TAG_CLOSE
1361                         && node.getParent().getType() == JavadocCommentsTokenTypes.HTML_TAG_END) {
1362                     description.append(node.getText());
1363                     isInHtmlElement = false;
1364                 }
1365                 if (node.getType() == JavadocCommentsTokenTypes.TEXT
1366                         // If a node has children, its text is not part of the description
1367                         || isInHtmlElement && node.getFirstChild() == null
1368                             // Some HTML elements span multiple lines, so we avoid the asterisk
1369                             && node.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK) {
1370                     if (isInCodeLiteral) {
1371                         description.append(node.getText().trim());
1372                     }
1373                     else {
1374                         description.append(node.getText());
1375                     }
1376                 }
1377                 if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME
1378                         && node.getParent().getType()
1379                                   == JavadocCommentsTokenTypes.CODE_INLINE_TAG) {
1380                     isInCodeLiteral = true;
1381                     description.append("<code>");
1382                 }
1383                 if (isInCodeLiteral
1384                         && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) {
1385                     isInCodeLiteral = false;
1386                     description.append("</code>");
1387                 }
1388 
1389             }
1390 
1391             DetailNode toVisit = node.getFirstChild();
1392             while (node != endNode && toVisit == null) {
1393                 toVisit = node.getNextSibling();
1394                 node = node.getParent();
1395             }
1396 
1397             node = toVisit;
1398         }
1399 
1400         return description.toString().trim();
1401     }
1402 
1403     /**
1404      * Get 1st paragraph from the Javadoc with no additional processing.
1405      *
1406      * @param javadoc the Javadoc to extract first paragraph from.
1407      * @return first paragraph of javadoc.
1408      */
1409     public static String getFirstParagraphFromJavadoc(DetailNode javadoc) {
1410         final String result;
1411         final List<DetailNode> firstParagraphNodes = getFirstJavadocParagraphNodes(javadoc);
1412         if (firstParagraphNodes.isEmpty()) {
1413             result = "";
1414         }
1415         else {
1416             final DetailNode startNode = firstParagraphNodes.getFirst();
1417             final DetailNode endNode = firstParagraphNodes.getLast();
1418             result = JavadocMetadataScraperUtil.constructSubTreeText(startNode, endNode);
1419         }
1420         return result;
1421     }
1422 
1423     /**
1424      * Extracts first paragraph nodes from javadoc.
1425      *
1426      * @param javadoc the Javadoc to extract the description from.
1427      * @return the first paragraph nodes of the setter.
1428      */
1429     public static List<DetailNode> getFirstJavadocParagraphNodes(DetailNode javadoc) {
1430         final List<DetailNode> firstParagraphNodes = new ArrayList<>();
1431 
1432         for (DetailNode child = javadoc.getFirstChild();
1433                 child != null; child = child.getNextSibling()) {
1434             if (isEndOfFirstJavadocParagraph(child)) {
1435                 break;
1436             }
1437             firstParagraphNodes.add(child);
1438         }
1439         return firstParagraphNodes;
1440     }
1441 
1442     /**
1443      * Determines if the given child index is the end of the first Javadoc paragraph. The end
1444      * of the description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK,
1445      * NEWLINE, LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the
1446      * one below this line.
1447      *
1448      * @param child the child to check.
1449      * @return true if the given child index is the end of the first javadoc paragraph.
1450      */
1451     public static boolean isEndOfFirstJavadocParagraph(DetailNode child) {
1452         final DetailNode nextSibling = child.getNextSibling();
1453         final DetailNode secondNextSibling = nextSibling.getNextSibling();
1454         final DetailNode thirdNextSibling = secondNextSibling.getNextSibling();
1455 
1456         return child.getType() == JavadocCommentsTokenTypes.NEWLINE
1457                     && nextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
1458                     && secondNextSibling.getType() == JavadocCommentsTokenTypes.NEWLINE
1459                     && thirdNextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
1460     }
1461 
1462     /**
1463      * Simplifies type name just to the name of the class, rather than entire package.
1464      *
1465      * @param fullTypeName full type name.
1466      * @return simplified type name, that is, name of the class.
1467      */
1468     public static String simplifyTypeName(String fullTypeName) {
1469         final int simplifiedStartIndex;
1470 
1471         if (fullTypeName.contains("$")) {
1472             simplifiedStartIndex = fullTypeName.lastIndexOf('$') + 1;
1473         }
1474         else {
1475             simplifiedStartIndex = fullTypeName.lastIndexOf('.') + 1;
1476         }
1477 
1478         return fullTypeName.substring(simplifiedStartIndex);
1479     }
1480 
1481     /** Utility class for extracting description from a method's Javadoc. */
1482     private static final class DescriptionExtractor {
1483 
1484         /**
1485          * Converts the href value to a relative link to the document and appends it to the
1486          * description.
1487          *
1488          * @param description the description to append the relative link to.
1489          * @param moduleName the name of the module.
1490          * @param value the href value.
1491          * @throws MacroExecutionException if the relative link could not be created.
1492          */
1493         private static void handleInternalLink(StringBuilder description,
1494                                                String moduleName, String value)
1495                 throws MacroExecutionException {
1496             String href = value;
1497             href = href.replace(CHECKSTYLE_ORG_URL, "");
1498             // Remove first and last characters, they are always double quotes
1499             href = href.substring(1, href.length() - 1);
1500 
1501             final String relativeHref = getLinkToDocument(moduleName, href);
1502             final char doubleQuote = '\"';
1503             description.append(doubleQuote).append(relativeHref).append(doubleQuote);
1504         }
1505     }
1506 }