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.file.Files;
32  import java.nio.file.Path;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.BitSet;
36  import java.util.Collection;
37  import java.util.Collections;
38  import java.util.HashSet;
39  import java.util.List;
40  import java.util.Locale;
41  import java.util.Map;
42  import java.util.Optional;
43  import java.util.Set;
44  import java.util.TreeMap;
45  import java.util.TreeSet;
46  import java.util.regex.Pattern;
47  import java.util.stream.Collectors;
48  import java.util.stream.IntStream;
49  import java.util.stream.Stream;
50  
51  import org.apache.commons.beanutils.PropertyUtils;
52  import org.apache.maven.doxia.macro.MacroExecutionException;
53  
54  import com.puppycrawl.tools.checkstyle.Checker;
55  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
56  import com.puppycrawl.tools.checkstyle.ModuleFactory;
57  import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
58  import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
59  import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
60  import com.puppycrawl.tools.checkstyle.PropertyType;
61  import com.puppycrawl.tools.checkstyle.TreeWalker;
62  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
63  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
64  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
65  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
66  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
67  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
68  import com.puppycrawl.tools.checkstyle.api.DetailNode;
69  import com.puppycrawl.tools.checkstyle.api.Filter;
70  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
71  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
72  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
73  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
74  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
75  import com.puppycrawl.tools.checkstyle.internal.annotation.PreserveOrder;
76  import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraperUtil;
77  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
78  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
79  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
80  
81  /**
82   * Utility class for site generation.
83   */
84  public final class SiteUtil {
85  
86      /** The string 'tokens'. */
87      public static final String TOKENS = "tokens";
88      /** The string 'javadocTokens'. */
89      public static final String JAVADOC_TOKENS = "javadocTokens";
90      /** The string 'violateExecutionOnNonTightHtml'. */
91      public static final String VIOLATE_EXECUTION_ON_NON_TIGHT_HTML =
92              "violateExecutionOnNonTightHtml";
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 
118     /** Precompiled regex pattern to remove the "Setter to " prefix from strings. */
119     private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to ");
120 
121     /** The url of the checkstyle website. */
122     private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/";
123     /** The string 'checks'. */
124     private static final String CHECKS = "checks";
125     /** The string 'naming'. */
126     private static final String NAMING = "naming";
127     /** The string 'src'. */
128     private static final String SRC = "src";
129     /** Template file extension. */
130     private static final String TEMPLATE_FILE_EXTENSION = ".xml.template";
131 
132     /** The precompiled pattern for a comma followed by a space. */
133     private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", ");
134 
135     /** The string '{}'. */
136     private static final String EMPTY_CURLY_BRACES = "{}";
137 
138     /** The string 'null'. */
139     private static final String NULL_STR = "null";
140 
141     /** Class name and their corresponding parent module name. */
142     private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries(
143         Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()),
144         Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()),
145         Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()),
146         Map.entry(Filter.class, Checker.class.getSimpleName()),
147         Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName())
148     );
149 
150     /** Set of properties that every check has. */
151     private static final Set<String> CHECK_PROPERTIES =
152             getProperties(AbstractCheck.class);
153 
154     /** Set of properties that every Javadoc check has. */
155     private static final Set<String> JAVADOC_CHECK_PROPERTIES =
156             getProperties(AbstractJavadocCheck.class);
157 
158     /** Set of properties that every FileSet check has. */
159     private static final Set<String> FILESET_PROPERTIES =
160             getProperties(AbstractFileSetCheck.class);
161 
162     /**
163      * Check and property name.
164      */
165     private static final String HEADER_CHECK_HEADER = "HeaderCheck.header";
166 
167     /**
168      * Check and property name.
169      */
170     private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header";
171 
172     /**
173      * The string 'api'.
174      */
175     private static final String API = "api";
176 
177     /** Set of properties that are undocumented. Those are internal properties. */
178     private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
179         "SuppressWithNearbyCommentFilter.fileContents",
180         "SuppressionCommentFilter.fileContents"
181     );
182 
183     /** Properties that can not be gathered from class instance. */
184     private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
185         // static field (all upper case)
186         "SuppressWarningsHolder.aliasList",
187         // loads string into memory similar to file
188         HEADER_CHECK_HEADER,
189         REGEXP_HEADER_CHECK_HEADER,
190         // property is an int, but we cut off excess to accommodate old versions
191         "RedundantModifierCheck.jdkVersion",
192         // until https://github.com/checkstyle/checkstyle/issues/13376
193         "CustomImportOrderCheck.customImportOrderRules"
194     );
195 
196     /** Path to main source code folder. */
197     private static final String MAIN_FOLDER_PATH = Path.of(
198             SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString();
199 
200     /** List of files who are superclasses and contain certain properties that checks inherit. */
201     private static final List<Path> MODULE_SUPER_CLASS_PATHS = List.of(
202         Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractAccessControlNameCheck.java"),
203         Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractNameCheck.java"),
204         Path.of(MAIN_FOLDER_PATH, CHECKS, "javadoc", "AbstractJavadocCheck.java"),
205         Path.of(MAIN_FOLDER_PATH, API, "AbstractFileSetCheck.java"),
206         Path.of(MAIN_FOLDER_PATH, API, "AbstractCheck.java"),
207         Path.of(MAIN_FOLDER_PATH, CHECKS, "header", "AbstractHeaderCheck.java"),
208         Path.of(MAIN_FOLDER_PATH, CHECKS, "metrics", "AbstractClassCouplingCheck.java"),
209         Path.of(MAIN_FOLDER_PATH, CHECKS, "whitespace", "AbstractParenPadCheck.java")
210     );
211 
212     /**
213      * Private utility constructor.
214      */
215     private SiteUtil() {
216     }
217 
218     /**
219      * Get string values of the message keys from the given check class.
220      *
221      * @param module class to examine.
222      * @return a set of checkstyle's module message keys.
223      * @throws MacroExecutionException if extraction of message keys fails.
224      */
225     public static Set<String> getMessageKeys(Class<?> module)
226             throws MacroExecutionException {
227         final Set<Field> messageKeyFields = getCheckMessageKeysFields(module);
228         final Set<String> messageKeys = new TreeSet<>();
229         for (Field field : messageKeyFields) {
230             messageKeys.add(getFieldValue(field, module).toString());
231         }
232         return messageKeys;
233     }
234 
235     /**
236      * Gets the check's messages keys.
237      *
238      * @param module class to examine.
239      * @return a set of checkstyle's module message fields.
240      * @throws MacroExecutionException if the attempt to read a protected class fails.
241      * @noinspection ChainOfInstanceofChecks
242      * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at
243      *                     <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a>
244      *
245      */
246     private static Set<Field> getCheckMessageKeysFields(Class<?> module)
247             throws MacroExecutionException {
248         try {
249             final Set<Field> checkstyleMessages = new HashSet<>();
250 
251             // get all fields from current class
252             final Field[] fields = module.getDeclaredFields();
253 
254             for (Field field : fields) {
255                 if (field.getName().startsWith("MSG_")) {
256                     checkstyleMessages.add(field);
257                 }
258             }
259 
260             final Class<?> superModule = module.getSuperclass();
261             if (superModule != null) {
262                 checkstyleMessages.addAll(getCheckMessageKeysFields(superModule));
263             }
264 
265             if (module == RegexpMultilineCheck.class) {
266                 checkstyleMessages.addAll(getCheckMessageKeysFields(Class.forName(
267                         "com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector")));
268             }
269             else if (module == RegexpSinglelineCheck.class
270                     || module == RegexpSinglelineJavaCheck.class) {
271                 checkstyleMessages.addAll(getCheckMessageKeysFields(Class
272                     .forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector")));
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         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         return parentModuleName;
434     }
435 
436     /**
437      * Get a set of properties for the given class that should be documented.
438      *
439      * @param clss the class to get the properties for.
440      * @param instance the instance of the module.
441      * @return a set of properties for the given class.
442      */
443     public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) {
444         final Set<String> properties =
445                 getProperties(clss).stream()
446                         .filter(prop -> {
447                             return !isGlobalProperty(clss, prop)
448                                     && !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 since version of the module.
457      *
458      * @param moduleClassName name of module class.
459      * @param modulePath module's path.
460      * @return since version of module.
461      * @throws MacroExecutionException if an error occurs during processing.
462      */
463     public static String getModuleSinceVersion(String moduleClassName, Path modulePath)
464             throws MacroExecutionException {
465         processModule(moduleClassName, modulePath);
466         return JavadocScraperResultUtil.getModuleSinceVersion();
467     }
468 
469     /**
470      * Get the property details of the module. If the property is not present in the
471      * module, then the property details from the superclass(es) is used.
472      *
473      * <p>Superclass property data is built fresh on every call and never cached
474      * statically, to prevent stale data from a previous Maven execution in the
475      * same JVM from corrupting results.</p>
476      *
477      * @param properties the properties of the module.
478      * @param moduleName the name of the module.
479      * @param modulePath the module file path.
480      * @param instance the instance of the module.
481      * @return the property details of the module.
482      * @throws MacroExecutionException if an error occurs during processing.
483      */
484     public static Map<String, PropertyDetails> buildPropertyDetails(Set<String> properties,
485                                                              String moduleName, Path modulePath,
486                                                              Object instance)
487             throws MacroExecutionException {
488         final Map<String, PropertyDetails> superClassPropertyData = buildSuperClassPropertyData();
489         processModule(moduleName, modulePath, instance, properties);
490 
491         final Map<String, PropertyDetails> currentPropertiesDetails =
492                 new TreeMap<>(JavadocScraperResultUtil.getPropertiesDetails());
493 
494         for (String property : properties) {
495             if (!currentPropertiesDetails.containsKey(property)) {
496                 processInheritedProperty(currentPropertiesDetails, property,
497                         instance, moduleName, superClassPropertyData);
498             }
499         }
500         assertAllPropertiesAreFound(properties, moduleName, currentPropertiesDetails);
501         return Collections.unmodifiableMap(currentPropertiesDetails);
502     }
503 
504     /**
505      * Processes an inherited property and adds its details to the provided map.
506      *
507      * @param detailsMap the map to add the property details to.
508      * @param property the name of the property.
509      * @param instance the module instance.
510      * @param moduleName the module name.
511      * @param superClassPropertyData the superclass property data built for this invocation.
512      * @throws MacroExecutionException if an error occurs.
513      */
514     private static void processInheritedProperty(
515             Map<String, PropertyDetails> detailsMap,
516             String property, Object instance,
517             String moduleName,
518             Map<String, PropertyDetails> superClassPropertyData)
519             throws MacroExecutionException {
520         final String moduleSince = JavadocScraperResultUtil.getModuleSinceVersion();
521         final PropertyDetails inherited = superClassPropertyData.get(property);
522         if (inherited != null) {
523             final String description = inherited.getDescription();
524             final String inheritedSince = inherited.getSinceVersion();
525 
526             final String since;
527             if (inheritedSince.isEmpty()
528                     || !moduleSince.isEmpty()
529                     && isVersionAtLeast(moduleSince, inheritedSince)) {
530                 if (moduleSince.isEmpty()) {
531                     since = inheritedSince;
532                 }
533                 else {
534                     since = moduleSince;
535                 }
536             }
537             else {
538                 since = inheritedSince;
539             }
540             final Field field = getField(instance.getClass(), property);
541             final PropertyDetails.Builder builder = new PropertyDetails.Builder()
542                     .name(property)
543                     .description(description)
544                     .sinceVersion(since);
545             detailsMap.put(property, constructPropertyDetails(builder,
546                     instance, field, property, moduleName));
547         }
548         else if (TOKENS.equals(property)
549                 || JAVADOC_TOKENS.equals(property)
550                 || VIOLATE_EXECUTION_ON_NON_TIGHT_HTML.equals(property)) {
551             final String description = getPropertyDescriptionForXdoc(property, null,
552                     moduleName);
553             final String since = getPropertySinceVersion(moduleSince, null);
554             final Field field = getField(instance.getClass(), property);
555             final PropertyDetails.Builder builder = new PropertyDetails.Builder()
556                     .name(property)
557                     .description(description)
558                     .sinceVersion(since);
559             detailsMap.put(property, constructPropertyDetails(builder,
560                     instance, field, property, moduleName));
561         }
562     }
563 
564     /**
565      * Assert that each property has a corresponding detail object.
566      *
567      * @param properties the properties of the module.
568      * @param moduleName the name of the module.
569      * @param details the details of the properties of the module.
570      * @throws MacroExecutionException if an error occurs during processing.
571      */
572     private static void assertAllPropertiesAreFound(
573             Set<String> properties, String moduleName, Map<String, PropertyDetails> details)
574             throws MacroExecutionException {
575         for (String property : properties) {
576             if (!details.containsKey(property)) {
577                 throw new MacroExecutionException(String.format(Locale.ROOT,
578                         "%s: Missing documentation for property '%s'.", moduleName, property));
579             }
580         }
581     }
582 
583     /**
584      * Builds a fresh map of superclass property data by scraping each superclass file.
585      * This method is called once per {@link #buildPropertyDetails} invocation and returns
586      * a new local map — it never populates any static field.
587      *
588      * @return map of property name to PropertyDetails for all known superclasses.
589      * @throws MacroExecutionException if an error occurs during processing.
590      */
591     private static Map<String, PropertyDetails> buildSuperClassPropertyData()
592             throws MacroExecutionException {
593         final Map<String, PropertyDetails> result = new TreeMap<>();
594         for (Path superclassPath : MODULE_SUPER_CLASS_PATHS) {
595             final Path fileNamePath = superclassPath.getFileName();
596             if (fileNamePath == null) {
597                 throw new MacroExecutionException("Invalid superclass path: " + superclassPath);
598             }
599             final String superclassName = CommonUtil.getFileNameWithoutExtension(
600                     fileNamePath.toString());
601 
602             final String pathString = superclassPath.toString().replace('\\', '/');
603             final String marker = "com/puppycrawl/tools/checkstyle/";
604             final String classPath = pathString.substring(pathString.indexOf(marker));
605             final String classFullName = classPath
606                     .substring(0, classPath.lastIndexOf(".java"))
607                     .replace('/', '.');
608             final Set<String> properties;
609             try {
610                 final Class<?> superClass = Class.forName(classFullName);
611                 final Set<String> setterProperties = new TreeSet<>(getProperties(superClass));
612                 if (AbstractFileSetCheck.class.isAssignableFrom(superClass)) {
613                     setterProperties.add(FILE_EXTENSIONS);
614                 }
615                 if (AbstractJavadocCheck.class.isAssignableFrom(superClass)) {
616                     setterProperties.add(VIOLATE_EXECUTION_ON_NON_TIGHT_HTML);
617                 }
618                 properties = setterProperties;
619             }
620             catch (ClassNotFoundException exc) {
621                 throw new MacroExecutionException("Failed to find class: " + classFullName, exc);
622             }
623 
624             processModule(superclassName, superclassPath, null, properties);
625             result.putAll(JavadocScraperResultUtil.getPropertiesDetails());
626         }
627         return result;
628     }
629 
630     /**
631      * Scrape the Javadocs of the class and its properties setters.
632      *
633      * @param moduleName the name of the module.
634      * @param modulePath the module Path.
635      * @throws MacroExecutionException if an error occurs during processing.
636      */
637     public static void processModule(String moduleName, Path modulePath)
638             throws MacroExecutionException {
639         final Object instance = getModuleInstance(moduleName);
640         final Set<String> properties = getPropertiesForDocumentation(instance.getClass(),
641                 instance);
642         processModule(moduleName, modulePath, instance, properties);
643     }
644 
645     /**
646      * Scrape the Javadocs of the class and its properties setters with
647      * ClassAndPropertiesSettersJavadocScraper.
648      *
649      * @param moduleName the name of the module.
650      * @param modulePath the module Path.
651      * @param instance the instance of the module.
652      * @param properties the properties of the module.
653      * @throws MacroExecutionException if an error occurs during processing.
654      */
655     private static void processModule(String moduleName, Path modulePath, Object instance,
656                                       Set<String> properties)
657             throws MacroExecutionException {
658         final Path resolvedPath = Path.of("").toAbsolutePath()
659                 .resolve(modulePath.toString().replace('\\', '/'))
660                 .normalize();
661         if (!Files.isRegularFile(resolvedPath)) {
662             final String message = String.format(Locale.ROOT,
663                     "File %s is not a file. Please check the 'modulePath' property.", modulePath);
664             throw new MacroExecutionException(message);
665         }
666         ClassAndPropertiesSettersJavadocScraper.initialize(moduleName, instance, properties);
667         final Checker checker = new Checker();
668         checker.setModuleClassLoader(Checker.class.getClassLoader());
669         final DefaultConfiguration scraperCheckConfig =
670                         new DefaultConfiguration(
671                                 ClassAndPropertiesSettersJavadocScraper.class.getName());
672         final DefaultConfiguration defaultConfiguration =
673                 new DefaultConfiguration("configuration");
674         final DefaultConfiguration treeWalkerConfig =
675                 new DefaultConfiguration(TreeWalker.class.getName());
676         defaultConfiguration.addProperty(CHARSET, "UTF-8");
677         defaultConfiguration.addChild(treeWalkerConfig);
678         treeWalkerConfig.addChild(scraperCheckConfig);
679         try {
680             checker.configure(defaultConfiguration);
681             final List<File> filesToProcess = List.of(resolvedPath.toFile());
682             checker.process(filesToProcess);
683             checker.destroy();
684         }
685         catch (CheckstyleException checkstyleException) {
686             final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName);
687             throw new MacroExecutionException(message, checkstyleException);
688         }
689     }
690 
691     /**
692      * Constructs a PropertyDetails object for the given property.
693      *
694      * @param builder the builder already containing name, description, and since version.
695      * @param instance the instance of the module.
696      * @param field the field of the property.
697      * @param propertyName the name of the property.
698      * @param moduleName the name of the module.
699      * @return the PropertyDetails object.
700      * @throws MacroExecutionException if an error occurs.
701      */
702     public static PropertyDetails constructPropertyDetails(PropertyDetails.Builder builder,
703                                                            Object instance, Field field,
704                                                            String propertyName, String moduleName)
705             throws MacroExecutionException {
706         if (TOKENS.equals(propertyName)) {
707             configureTokensDetails(builder, (AbstractCheck) instance);
708         }
709         else if (JAVADOC_TOKENS.equals(propertyName)) {
710             configureJavadocTokensDetails(builder, (AbstractJavadocCheck) instance);
711         }
712         else {
713             configureOtherPropertyDetails(builder, instance, field, propertyName, moduleName);
714         }
715         return builder.build();
716     }
717 
718     /**
719      * Configures the tokens details for a property.
720      *
721      * @param builder the property details builder.
722      * @param check the check instance.
723      */
724     private static void configureTokensDetails(PropertyDetails.Builder builder,
725                                                AbstractCheck check) {
726         final int[] requiredTokens = check.getRequiredTokens();
727         final int[] acceptableTokens = check.getAcceptableTokens();
728         final int[] defaultTokens = check.getDefaultTokens();
729         final int[] allTokenIds = TokenUtil.getAllTokenIds();
730         if (requiredTokens.length == 0
731                 && Arrays.equals(acceptableTokens, allTokenIds)) {
732             builder.tokenPropertyType(PropertyDetails.TokenPropertyType.TOKEN_SET);
733         }
734         else {
735             builder.tokenPropertyType(PropertyDetails.TokenPropertyType.TOKEN_SUBSET);
736             builder.configurableTokens(getDifference(acceptableTokens,
737                     requiredTokens).stream().map(TokenUtil::getTokenName).toList());
738         }
739         if (Arrays.equals(defaultTokens, allTokenIds)) {
740             builder.defaultValueTokens(List.of(TOKEN_TYPES));
741         }
742         else {
743             builder.defaultValueTokens(getDifference(defaultTokens,
744                     requiredTokens).stream().map(TokenUtil::getTokenName).toList());
745         }
746     }
747 
748     /**
749      * Configures the javadoc tokens details for a property.
750      *
751      * @param builder the property details builder.
752      * @param check the javadoc check instance.
753      */
754     private static void configureJavadocTokensDetails(PropertyDetails.Builder builder,
755                                                       AbstractJavadocCheck check) {
756         builder.tokenPropertyType(PropertyDetails.TokenPropertyType.JAVADOC_TOKEN_SUBSET);
757         builder.configurableTokens(getDifference(check.getAcceptableJavadocTokens(),
758                 check.getRequiredJavadocTokens()).stream()
759                 .map(JavadocUtil::getTokenName).toList());
760         builder.defaultValueTokens(getDifference(check.getDefaultJavadocTokens(),
761                 check.getRequiredJavadocTokens()).stream()
762                 .map(JavadocUtil::getTokenName).toList());
763     }
764 
765     /**
766      * Configures the details for properties other than tokens and javadoc tokens.
767      *
768      * @param builder the property details builder.
769      * @param instance the module instance.
770      * @param field the field of the property.
771      * @param propertyName the name of the property.
772      * @param moduleName the name of the module.
773      * @throws MacroExecutionException if an error occurs.
774      */
775     private static void configureOtherPropertyDetails(PropertyDetails.Builder builder,
776                                                       Object instance, Field field,
777                                                       String propertyName, String moduleName)
778             throws MacroExecutionException {
779         final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance);
780         final String type;
781         if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
782             type = "subset of tokens TokenTypes";
783         }
784         else {
785             final String rawType = getType(field, propertyName, moduleName, instance);
786             type = simplifyTypeName(rawType);
787         }
788         builder.type(type);
789 
790         String defaultValue;
791         if (field != null) {
792             defaultValue = getDefaultValue(propertyName, field, instance, moduleName);
793         }
794         else {
795             final Class<?> propertyClass = getPropertyClass(propertyName, instance);
796             if (propertyClass.isArray()) {
797                 defaultValue = EMPTY_CURLY_BRACES;
798             }
799             else {
800                 defaultValue = NULL_STR;
801             }
802         }
803 
804         if (defaultValue.isEmpty() && fieldClass.isArray()) {
805             defaultValue = EMPTY_CURLY_BRACES;
806         }
807 
808         if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)
809                 && !EMPTY_CURLY_BRACES.equals(defaultValue)) {
810             builder.defaultValueTokens(Arrays.asList(COMMA_SPACE_PATTERN.split(defaultValue)));
811         }
812         else {
813             builder.defaultValue(defaultValue);
814         }
815     }
816 
817     /**
818      * Get a set of properties for the given class.
819      *
820      * @param clss the class to get the properties for.
821      * @return a set of properties for the given class.
822      */
823     public static Set<String> getProperties(Class<?> clss) {
824         final Set<String> result = new TreeSet<>();
825         final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss);
826 
827         for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
828             if (propertyDescriptor.getWriteMethod() != null) {
829                 result.add(propertyDescriptor.getName());
830             }
831         }
832 
833         return result;
834     }
835 
836     /**
837      * Checks if the property is a global property. Global properties come from the base classes
838      * and are common to all checks. For example id, severity, tabWidth, etc.
839      *
840      * @param clss the class of the module.
841      * @param propertyName the name of the property.
842      * @return true if the property is a global property.
843      */
844     private static boolean isGlobalProperty(Class<?> clss, String propertyName) {
845         return AbstractCheck.class.isAssignableFrom(clss)
846                     && CHECK_PROPERTIES.contains(propertyName)
847                 || AbstractJavadocCheck.class.isAssignableFrom(clss)
848                     && JAVADOC_CHECK_PROPERTIES.contains(propertyName)
849                 || AbstractFileSetCheck.class.isAssignableFrom(clss)
850                     && FILESET_PROPERTIES.contains(propertyName);
851     }
852 
853     /**
854      * Checks if the property is supposed to be documented.
855      *
856      * @param clss the class of the module.
857      * @param propertyName the name of the property.
858      * @return true if the property is supposed to be documented.
859      */
860     private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) {
861         return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName);
862     }
863 
864     /**
865      * Gets properties that are not explicitly captured but should be documented if
866      * certain conditions are met.
867      *
868      * @param instance the instance of the module.
869      * @param clss the class of the module.
870      * @return the non explicit properties.
871      */
872     private static Set<String> getNonExplicitProperties(
873             Object instance, Class<?> clss) {
874         final Set<String> result = new TreeSet<>();
875         if (AbstractCheck.class.isAssignableFrom(clss)) {
876             final AbstractCheck check = (AbstractCheck) instance;
877 
878             final int[] acceptableTokens = check.getAcceptableTokens();
879             Arrays.sort(acceptableTokens);
880             final int[] defaultTokens = check.getDefaultTokens();
881             Arrays.sort(defaultTokens);
882             final int[] requiredTokens = check.getRequiredTokens();
883             Arrays.sort(requiredTokens);
884 
885             if (!Arrays.equals(acceptableTokens, defaultTokens)
886                     || !Arrays.equals(acceptableTokens, requiredTokens)) {
887                 result.add(TOKENS);
888             }
889         }
890 
891         if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
892             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
893             result.add(VIOLATE_EXECUTION_ON_NON_TIGHT_HTML);
894 
895             final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
896             Arrays.sort(acceptableJavadocTokens);
897             final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
898             Arrays.sort(defaultJavadocTokens);
899             final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
900             Arrays.sort(requiredJavadocTokens);
901 
902             if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
903                     || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
904                 result.add(JAVADOC_TOKENS);
905             }
906         }
907 
908         if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
909             result.add(FILE_EXTENSIONS);
910         }
911         return result;
912     }
913 
914     /**
915      * Get the description of the property.
916      *
917      * @param propertyName the name of the property.
918      * @param javadoc the Javadoc of the property setter method.
919      * @param moduleName the name of the module.
920      * @return the description of the property.
921      * @throws MacroExecutionException if the description could not be extracted.
922      */
923     public static String getPropertyDescriptionForXdoc(
924             String propertyName, DetailNode javadoc, String moduleName)
925             throws MacroExecutionException {
926         final String description;
927         if (TOKENS.equals(propertyName)) {
928             description = "tokens to check";
929         }
930         else if (JAVADOC_TOKENS.equals(propertyName)) {
931             description = "javadoc tokens to check";
932         }
933         else if (VIOLATE_EXECUTION_ON_NON_TIGHT_HTML.equals(propertyName)) {
934             description = "Control when to print violations if the Javadoc being"
935                     + " examined by this check violates the tight html rules defined at"
936                     + " <a href=\"" + CHECKSTYLE_ORG_URL
937                     + "writingjavadocchecks.html#Tight-HTML_rules\">"
938                     + "Tight-HTML Rules</a>.";
939         }
940         else if (FILE_EXTENSIONS.equals(propertyName)) {
941             description = "Specify the file extensions of the files to process.";
942         }
943         else {
944             final String javadocDescription =
945                     getDescriptionFromJavadocForXdoc(javadoc, moduleName);
946             final String descriptionString = SETTER_PATTERN.matcher(javadocDescription)
947                     .replaceFirst("");
948 
949             if (descriptionString.isEmpty()) {
950                 description = "";
951             }
952             else {
953                 final String firstLetterCapitalized = descriptionString.substring(0, 1)
954                         .toUpperCase(Locale.ROOT);
955                 description = firstLetterCapitalized + descriptionString.substring(1);
956             }
957         }
958         return description;
959     }
960 
961     /**
962      * Get the since version of the property.
963      *
964      * <p>Note: the {@code moduleName} parameter has been removed because it was unused.
965      * All call sites have been updated accordingly.</p>
966      *
967      * @param moduleSince the since version of the module.
968      * @param propertyJavadoc the Javadoc of the property setter method.
969      * @return the since version of the property.
970      */
971     public static String getPropertySinceVersion(String moduleSince,
972                                                  DetailNode propertyJavadoc) {
973         final String sinceVersion;
974 
975         final Optional<String> specifiedPropertyVersionInPropertyJavadoc =
976                 getPropertyVersionFromItsJavadoc(propertyJavadoc);
977 
978         if (specifiedPropertyVersionInPropertyJavadoc.isPresent()) {
979             sinceVersion = specifiedPropertyVersionInPropertyJavadoc.get();
980         }
981         else {
982             String propertySetterSince = null;
983             if (propertyJavadoc != null) {
984                 propertySetterSince = getSinceVersionFromJavadoc(propertyJavadoc);
985             }
986 
987             if (propertySetterSince != null
988                     && (moduleSince == null || moduleSince.isEmpty()
989                     || isVersionAtLeast(propertySetterSince, moduleSince))) {
990                 sinceVersion = propertySetterSince;
991             }
992             else {
993                 sinceVersion = Optional.ofNullable(moduleSince).orElse("");
994             }
995         }
996 
997         return sinceVersion;
998     }
999 
1000     /**
1001      * Extract the property since version from its Javadoc.
1002      *
1003      * @param propertyJavadoc the property Javadoc to extract the since version from.
1004      * @return the Optional of property version specified in its javadoc.
1005      */
1006     private static Optional<String> getPropertyVersionFromItsJavadoc(DetailNode propertyJavadoc) {
1007         Optional<String> result = Optional.empty();
1008 
1009         if (propertyJavadoc != null) {
1010             final Optional<DetailNode> propertyJavadocTag =
1011                     getPropertySinceJavadocTag(propertyJavadoc);
1012 
1013             result = propertyJavadocTag
1014                     .map(tag -> {
1015                         return JavadocUtil.findFirstToken(
1016                                 tag, JavadocCommentsTokenTypes.DESCRIPTION);
1017                     })
1018                     .map(description -> {
1019                         return JavadocUtil.findFirstToken(
1020                                 description, JavadocCommentsTokenTypes.TEXT);
1021                     })
1022                     .map(DetailNode::getText)
1023                     .map(String::trim);
1024         }
1025         return result;
1026     }
1027 
1028     /**
1029      * Find the propertySince Javadoc tag node in the given property Javadoc.
1030      *
1031      * @param javadoc the Javadoc to search.
1032      * @return the Optional of propertySince Javadoc tag node or null if not found.
1033      */
1034     private static Optional<DetailNode> getPropertySinceJavadocTag(DetailNode javadoc) {
1035         Optional<DetailNode> propertySinceJavadocTag = Optional.empty();
1036         if (javadoc != null) {
1037             DetailNode child = javadoc.getFirstChild();
1038 
1039             while (child != null) {
1040                 if (child.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
1041                     final DetailNode customBlockTag = JavadocUtil.findFirstToken(
1042                             child, JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG);
1043 
1044                     if (customBlockTag != null
1045                             && "propertySince".equals(JavadocUtil.findFirstToken(
1046                             customBlockTag,
1047                             JavadocCommentsTokenTypes.TAG_NAME).getText())) {
1048                         propertySinceJavadocTag = Optional.of(customBlockTag);
1049                         break;
1050                     }
1051                 }
1052                 child = child.getNextSibling();
1053             }
1054         }
1055         return propertySinceJavadocTag;
1056     }
1057 
1058     /**
1059      * Gets all javadoc nodes of selected type.
1060      *
1061      * @param allNodes Nodes to choose from.
1062      * @param neededType the Javadoc token type to select.
1063      * @return the List of DetailNodes of selected type.
1064      */
1065     public static List<DetailNode> getNodesOfSpecificType(DetailNode[] allNodes, int neededType) {
1066         return Arrays.stream(allNodes)
1067             .filter(child -> child.getType() == neededType)
1068             .toList();
1069     }
1070 
1071     /**
1072      * Extract the since version from the Javadoc.
1073      *
1074      * @param javadoc the Javadoc to extract the since version from.
1075      * @return the since version of the setter, or {@code null} if not found.
1076      */
1077     private static String getSinceVersionFromJavadoc(DetailNode javadoc) {
1078         String result = null;
1079 
1080         if (javadoc != null) {
1081             final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc);
1082             result = Optional.ofNullable(sinceJavadocTag)
1083                     .map(tag -> {
1084                         return JavadocUtil.findFirstToken(
1085                                 tag, JavadocCommentsTokenTypes.DESCRIPTION);
1086                     })
1087                     .map(description -> {
1088                         return JavadocUtil.findFirstToken(
1089                                 description, JavadocCommentsTokenTypes.TEXT);
1090                     })
1091                     .map(DetailNode::getText)
1092                     .map(String::trim)
1093                     .orElse(null);
1094         }
1095         return result;
1096     }
1097 
1098     /**
1099      * Find the since Javadoc tag node in the given Javadoc.
1100      *
1101      * @param javadoc the Javadoc to search.
1102      * @return the since Javadoc tag node or null if not found.
1103      */
1104     private static DetailNode getSinceJavadocTag(DetailNode javadoc) {
1105         DetailNode javadocTagWithSince = null;
1106 
1107         if (javadoc != null) {
1108             DetailNode child = javadoc.getFirstChild();
1109 
1110             while (child != null) {
1111                 if (child.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
1112                     final DetailNode sinceNode = JavadocUtil.findFirstToken(
1113                             child, JavadocCommentsTokenTypes.SINCE_BLOCK_TAG);
1114 
1115                     if (sinceNode != null) {
1116                         javadocTagWithSince = sinceNode;
1117                         break;
1118                     }
1119                 }
1120                 child = child.getNextSibling();
1121             }
1122         }
1123 
1124         return javadocTagWithSince;
1125     }
1126 
1127     /**
1128      * Returns {@code true} if {@code actualVersion} >= {@code requiredVersion}.
1129      * Both versions have any trailing "-SNAPSHOT" stripped before comparison.
1130      *
1131      * @param actualVersion   e.g. "8.3" or "8.3-SNAPSHOT"
1132      * @param requiredVersion e.g. "8.3"
1133      * @return {@code true} if actualVersion exists, and, numerically, is at least requiredVersion
1134      */
1135     private static boolean isVersionAtLeast(String actualVersion,
1136                                             String requiredVersion) {
1137         final Version actualVersionParsed = Version.parse(actualVersion);
1138         final Version requiredVersionParsed = Version.parse(requiredVersion);
1139 
1140         return actualVersionParsed.compareTo(requiredVersionParsed) >= 0;
1141     }
1142 
1143     /**
1144      * Get the type of the property.
1145      *
1146      * @param field the field to get the type of.
1147      * @param propertyName the name of the property.
1148      * @param moduleName the name of the module.
1149      * @param instance the instance of the module.
1150      * @return the type of the property.
1151      * @throws MacroExecutionException if an error occurs during getting the type.
1152      */
1153     public static String getType(Field field, String propertyName,
1154                                  String moduleName, Object instance)
1155             throws MacroExecutionException {
1156         final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance);
1157         return Optional.ofNullable(field)
1158                 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
1159                 .filter(propertyType -> propertyType.value() != PropertyType.TOKEN_ARRAY)
1160                 .map(propertyType -> propertyType.value().getDescription())
1161                 .orElseGet(fieldClass::getTypeName);
1162     }
1163 
1164     /**
1165      * Get the default value of the property.
1166      *
1167      * @param propertyName the name of the property.
1168      * @param field the field to get the default value of.
1169      * @param classInstance the instance of the class to get the default value of.
1170      * @param moduleName the name of the module.
1171      * @return the default value of the property.
1172      * @throws MacroExecutionException if an error occurs during getting the default value.
1173      */
1174     public static String getDefaultValue(String propertyName, Field field,
1175                                          Object classInstance, String moduleName)
1176             throws MacroExecutionException {
1177 
1178         final String result;
1179         if (classInstance instanceof PropertyCacheFile) {
1180             result = "null (no cache file)";
1181         }
1182         else {
1183             final Object value = getFieldValue(field, classInstance);
1184             final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName,
1185                     classInstance);
1186 
1187             final String fieldValue = getFieldDefaultValue(field, fieldClass, value);
1188             result = Optional.ofNullable(fieldValue).orElse(NULL_STR);
1189         }
1190 
1191         return result;
1192     }
1193 
1194     /**
1195      * Gets the string representation of a field's default value based on its type.
1196      * Returns {@code null} if the field type is not recognized or the value is null.
1197      *
1198      * @param field the field to get the default value of.
1199      * @param fieldClass the class of the field.
1200      * @param value the current value of the field.
1201      * @return string form of the default value, or {@code null} if unrecognized.
1202      */
1203     private static String getFieldDefaultValue(Field field, Class<?> fieldClass, Object value) {
1204         String result = getScalarFieldDefaultValue(fieldClass, value);
1205         if (result == null) {
1206             result = getArrayFieldDefaultValue(field, fieldClass, value);
1207         }
1208         return result;
1209     }
1210 
1211     /**
1212      * Gets the default value string for scalar (non-array) field types.
1213      * Returns {@code null} if the field class is not a handled scalar type.
1214      *
1215      * @param fieldClass the class of the field.
1216      * @param value the current value of the field.
1217      * @return string form of the default value, or {@code null} if not a scalar type.
1218      */
1219     private static String getScalarFieldDefaultValue(Class<?> fieldClass, Object value) {
1220         final String result;
1221         if (fieldClass == boolean.class
1222                 || fieldClass == int.class
1223                 || fieldClass == URI.class
1224                 || fieldClass == String.class) {
1225             result = Optional.ofNullable(value).map(Object::toString).orElse(null);
1226         }
1227         else if (fieldClass == Pattern.class) {
1228             result = getPatternDefaultValue(value);
1229         }
1230         else if (fieldClass.isEnum()) {
1231             result = Optional.ofNullable(value)
1232                     .map(object -> object.toString().toLowerCase(Locale.ENGLISH))
1233                     .orElse(null);
1234         }
1235         else {
1236             result = null;
1237         }
1238         return result;
1239     }
1240 
1241     /**
1242      * Gets the default value string for array field types.
1243      * Returns {@code null} if the field class is not a handled array type.
1244      *
1245      * @param field the field (used for annotation checks).
1246      * @param fieldClass the class of the field.
1247      * @param value the current value of the field.
1248      * @return string form of the default value, or {@code null} if not an array type.
1249      */
1250     private static String getArrayFieldDefaultValue(Field field, Class<?> fieldClass,
1251                                                     Object value) {
1252         final String result;
1253 
1254         if (fieldClass == int[].class
1255                 || ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
1256             result = getIntArrayPropertyValue(value);
1257         }
1258         else {
1259             result = switch (fieldClass.getSimpleName()) {
1260                 case "double[]" -> removeSquareBrackets(
1261                         Arrays.toString((double[]) value).replace(".0", ""));
1262                 case "String[]" -> getStringArrayPropertyValue(value,
1263                         hasPreserveOrderAnnotation(field));
1264                 case "Pattern[]" -> getPatternArrayPropertyValue(value);
1265                 case "AccessModifierOption[]" -> getAccessModifierDefaultValue(value);
1266                 case null, default -> null;
1267             };
1268         }
1269 
1270         return result;
1271     }
1272 
1273     /**
1274      * Gets the string representation of a Pattern field's default value.
1275      *
1276      * @param value the current value of the field.
1277      * @return string form of the Pattern default value, or {@code null} if value is null.
1278      */
1279     private static String getPatternDefaultValue(Object value) {
1280         final String result;
1281         if (value == null) {
1282             result = null;
1283         }
1284         else {
1285             result = value.toString()
1286                     .replace("\n", "\\n")
1287                     .replace("\t", "\\t")
1288                     .replace("\r", "\\r")
1289                     .replace("\f", "\\f");
1290         }
1291         return result;
1292     }
1293 
1294     /**
1295      * Gets the string representation of an AccessModifierOption array field's default value.
1296      *
1297      * @param value the current value of the field.
1298      * @return string form of the default value.
1299      */
1300     private static String getAccessModifierDefaultValue(Object value) {
1301         final String result;
1302         if (value != null && Array.getLength(value) > 0) {
1303             result = removeSquareBrackets(Arrays.toString((Object[]) value));
1304         }
1305         else {
1306             result = "";
1307         }
1308         return result;
1309     }
1310 
1311     /**
1312      * Checks if a field has the {@code PreserveOrder} annotation.
1313      *
1314      * @param field the field to check
1315      * @return true if the field has {@code PreserveOrder} annotation, false otherwise
1316      */
1317     private static boolean hasPreserveOrderAnnotation(Field field) {
1318         return field != null && field.isAnnotationPresent(PreserveOrder.class);
1319     }
1320 
1321     /**
1322      * Gets the name of the bean property's default value for the Pattern array class.
1323      *
1324      * @param fieldValue The bean property's value
1325      * @return String form of property's default value
1326      */
1327     private static String getPatternArrayPropertyValue(Object fieldValue) {
1328         Object value = fieldValue;
1329         if (value instanceof Collection<?> collection) {
1330             value = collection.stream()
1331                     .map(Pattern.class::cast)
1332                     .toArray(Pattern[]::new);
1333         }
1334 
1335         String result = "";
1336         if (value != null && Array.getLength(value) > 0) {
1337             result = removeSquareBrackets(
1338                     Arrays.stream((Pattern[]) value)
1339                     .map(Pattern::pattern)
1340                     .collect(Collectors.joining(COMMA_SPACE)));
1341         }
1342 
1343         return result;
1344     }
1345 
1346     /**
1347      * Removes square brackets [ and ] from the given string.
1348      *
1349      * @param value the string to remove square brackets from.
1350      * @return the string without square brackets.
1351      */
1352     private static String removeSquareBrackets(String value) {
1353         return value
1354                 .replace("[", "")
1355                 .replace("]", "");
1356     }
1357 
1358     /**
1359      * Gets the name of the bean property's default value for the string array class.
1360      *
1361      * @param value The bean property's value
1362      * @param preserveOrder whether to preserve the original order
1363      * @return String form of property's default value
1364      */
1365     private static String getStringArrayPropertyValue(Object value, boolean preserveOrder) {
1366         final String result;
1367         if (value == null) {
1368             result = "";
1369         }
1370         else {
1371             try (Stream<?> valuesStream = getValuesStream(value)) {
1372                 final List<String> stringList = valuesStream
1373                     .map(String.class::cast)
1374                     .collect(Collectors.toCollection(ArrayList<String>::new));
1375 
1376                 if (preserveOrder) {
1377                     result = String.join(COMMA_SPACE, stringList);
1378                 }
1379                 else {
1380                     result = stringList.stream()
1381                     .sorted()
1382                     .collect(Collectors.joining(COMMA_SPACE));
1383                 }
1384             }
1385         }
1386         return result;
1387     }
1388 
1389     /**
1390      * Generates a stream of values from the given value.
1391      *
1392      * @param value the value to generate the stream from.
1393      * @return the stream of values.
1394      */
1395     private static Stream<?> getValuesStream(Object value) {
1396         final Stream<?> valuesStream;
1397         if (value instanceof Collection<?> collection) {
1398             valuesStream = collection.stream();
1399         }
1400         else {
1401             final Object[] array = (Object[]) value;
1402             valuesStream = Arrays.stream(array);
1403         }
1404         return valuesStream;
1405     }
1406 
1407     /**
1408      * Returns the name of the bean property's default value for the int array class.
1409      *
1410      * @param value The bean property's value.
1411      * @return String form of property's default value.
1412      */
1413     private static String getIntArrayPropertyValue(Object value) {
1414         try (IntStream stream = getIntStream(value)) {
1415             return stream
1416                     .mapToObj(TokenUtil::getTokenName)
1417                     .sorted()
1418                     .collect(Collectors.joining(COMMA_SPACE));
1419         }
1420     }
1421 
1422     /**
1423      * Get the int stream from the given value.
1424      *
1425      * @param value the value to get the int stream from.
1426      * @return the int stream.
1427      * @throws IllegalArgumentException if parameter is null.
1428      */
1429     private static IntStream getIntStream(Object value) {
1430         return switch (value) {
1431             case null -> throw new IllegalArgumentException("value is null");
1432             case Collection<?> collection -> collection.stream()
1433                     .mapToInt(Integer.class::cast);
1434             case BitSet set -> set.stream();
1435             default -> Arrays.stream((int[]) value);
1436         };
1437     }
1438 
1439     /**
1440      * Gets the class of the given field.
1441      *
1442      * @param field the field to get the class of.
1443      * @param propertyName the name of the property.
1444      * @param moduleName the name of the module.
1445      * @param instance the instance of the module.
1446      * @return the class of the field.
1447      * @throws MacroExecutionException if an error occurs during getting the class.
1448      */
1449     // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
1450     // -@cs[ForbidWildcardAsReturnType] Implied by design to return different types
1451     public static Class<?> getFieldClass(Field field, String propertyName,
1452                                           String moduleName, Object instance)
1453             throws MacroExecutionException {
1454         Class<?> result = null;
1455 
1456         if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1457                 .contains(moduleName + DOT + propertyName)) {
1458             result = getPropertyClass(propertyName, instance);
1459         }
1460         if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
1461             result = String[].class;
1462         }
1463         if (field != null && result == null) {
1464             result = field.getType();
1465         }
1466 
1467         if (result == null) {
1468             throw new MacroExecutionException(
1469                     "Could not find field " + propertyName + " in class " + moduleName);
1470         }
1471 
1472         if (field != null && (result == List.class || result == Set.class)) {
1473             result = getParameterizedTypeClass(field);
1474         }
1475         else if (result == BitSet.class) {
1476             result = int[].class;
1477         }
1478 
1479         return result;
1480     }
1481 
1482     /**
1483      * Gets the class of the parameterized type for the given field.
1484      *
1485      * @param field the field to get the parameterized type class of.
1486      * @return the class of the parameterized type.
1487      * @throws MacroExecutionException if an error occurs.
1488      */
1489     private static Class<?> getParameterizedTypeClass(Field field) throws MacroExecutionException {
1490         final ParameterizedType type = (ParameterizedType) field.getGenericType();
1491         final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1492         final Class<?> result;
1493 
1494         if (parameterClass == Integer.class) {
1495             result = int[].class;
1496         }
1497         else if (parameterClass == String.class) {
1498             result = String[].class;
1499         }
1500         else if (parameterClass == Pattern.class) {
1501             result = Pattern[].class;
1502         }
1503         else {
1504             final String message = "Unknown parameterized type: "
1505                     + parameterClass.getSimpleName();
1506             throw new MacroExecutionException(message);
1507         }
1508         return result;
1509     }
1510 
1511     /**
1512      * Gets the class of the given java property.
1513      *
1514      * @param propertyName the name of the property.
1515      * @param instance the instance of the module.
1516      * @return the class of the java property.
1517      * @throws MacroExecutionException if an error occurs during getting the class.
1518      */
1519     // -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field
1520     public static Class<?> getPropertyClass(String propertyName, Object instance)
1521             throws MacroExecutionException {
1522         final Class<?> result;
1523         try {
1524             final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1525                     propertyName);
1526             result = descriptor.getPropertyType();
1527         }
1528         catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1529             throw new MacroExecutionException("Failed to retrieve property type", exc);
1530         }
1531         return result;
1532     }
1533 
1534     /**
1535      * Get the difference between two lists of tokens.
1536      *
1537      * @param tokens the list of tokens to remove from.
1538      * @param subtractions the tokens to remove.
1539      * @return the difference between the two lists.
1540      */
1541     public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1542         final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1543                 .boxed()
1544                 .collect(Collectors.toUnmodifiableSet());
1545         return Arrays.stream(tokens)
1546                 .boxed()
1547                 .filter(token -> !subtractionsSet.contains(token))
1548                 .toList();
1549     }
1550 
1551     /**
1552      * Gets the field with the given name from the given class.
1553      *
1554      * @param fieldClass the class to get the field from.
1555      * @param propertyName the name of the field.
1556      * @return the field we are looking for.
1557      */
1558     public static Field getField(Class<?> fieldClass, String propertyName) {
1559         Field result = null;
1560         Class<?> currentClass = fieldClass;
1561 
1562         while (currentClass != Object.class) {
1563             try {
1564                 result = currentClass.getDeclaredField(propertyName);
1565                 result.trySetAccessible();
1566                 break;
1567             }
1568             catch (NoSuchFieldException ignored) {
1569                 currentClass = currentClass.getSuperclass();
1570             }
1571         }
1572 
1573         return result;
1574     }
1575 
1576     /**
1577      * Constructs string with relative link to the provided document.
1578      *
1579      * @param moduleName the name of the module.
1580      * @param document the path of the document.
1581      * @return relative link to the document.
1582      * @throws MacroExecutionException if link to the document cannot be constructed.
1583      */
1584     public static String getLinkToDocument(String moduleName, String document)
1585             throws MacroExecutionException {
1586         final Path templatePath = getTemplatePath(FINAL_CHECK.matcher(moduleName).replaceAll(""));
1587         if (templatePath == null) {
1588             throw new MacroExecutionException(
1589                     String.format(Locale.ROOT,
1590                             "Could not find template for %s", moduleName));
1591         }
1592         final Path templatePathParent = templatePath.getParent();
1593         if (templatePathParent == null) {
1594             throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1595         }
1596         return templatePathParent
1597                 .relativize(Path.of(SRC, "site/xdoc", document))
1598                 .toString()
1599                 .replace(".xml", ".html")
1600                 .replace('\\', '/');
1601     }
1602 
1603     /**
1604      * Get all templates whose content contains properties macro.
1605      *
1606      * @return templates whose content contains properties macro.
1607      * @throws CheckstyleException if file could not be read.
1608      * @throws MacroExecutionException if template file is not found.
1609      */
1610     public static List<Path> getTemplatesThatContainPropertiesMacro()
1611             throws CheckstyleException, MacroExecutionException {
1612         final List<Path> result = new ArrayList<>();
1613         final Set<Path> templatesPaths = getXdocsTemplatesFilePaths();
1614         for (Path templatePath: templatesPaths) {
1615             final String content = getFileContents(templatePath);
1616             final String propertiesMacroDefinition = "<macro name=\"properties\"";
1617             if (content.contains(propertiesMacroDefinition)) {
1618                 result.add(templatePath);
1619             }
1620         }
1621         return result;
1622     }
1623 
1624     /**
1625      * Get file contents as string.
1626      *
1627      * @param pathToFile path to file.
1628      * @return file contents as string.
1629      * @throws CheckstyleException if file could not be read.
1630      */
1631     private static String getFileContents(Path pathToFile) throws CheckstyleException {
1632         final String content;
1633         try {
1634             content = Files.readString(pathToFile);
1635         }
1636         catch (IOException ioException) {
1637             final String message = String.format(Locale.ROOT, "Failed to read file: %s",
1638                     pathToFile);
1639             throw new CheckstyleException(message, ioException);
1640         }
1641         return content;
1642     }
1643 
1644     /**
1645      * Get the module name from the file. The module name is the file name without the extension.
1646      *
1647      * @param file file to extract the module name from.
1648      * @return module name.
1649      */
1650     public static String getModuleName(File file) {
1651         final String fullFileName = file.getName();
1652         return CommonUtil.getFileNameWithoutExtension(fullFileName);
1653     }
1654 
1655     /**
1656      * Extracts the description from the javadoc detail node. Performs a DFS traversal on the
1657      * detail node and extracts the text nodes. This description is additionally processed to
1658      * fit Xdoc format.
1659      *
1660      * @param javadoc the Javadoc to extract the description from.
1661      * @param moduleName the name of the module.
1662      * @return the description of the setter.
1663      * @throws MacroExecutionException if the description could not be extracted.
1664      */
1665     // -@cs[NPathComplexity] Splitting would not make the code more readable
1666     // -@cs[CyclomaticComplexity] Splitting would not make the code more readable.
1667     // -@cs[ExecutableStatementCount] Splitting would not make the code more readable.
1668     private static String getDescriptionFromJavadocForXdoc(DetailNode javadoc, String moduleName)
1669             throws MacroExecutionException {
1670         final List<DetailNode> descriptionNodes = getFirstJavadocParagraphNodes(javadoc);
1671         final StringBuilder description = new StringBuilder(128);
1672 
1673         if (!descriptionNodes.isEmpty()) {
1674             DetailNode node = descriptionNodes.getFirst();
1675             final DetailNode endNode = descriptionNodes.getLast();
1676 
1677             final DescriptionTraversalState state = new DescriptionTraversalState();
1678 
1679             while (node != null) {
1680                 processDescriptionNode(node, description, state, moduleName);
1681 
1682                 DetailNode toVisit = node.getFirstChild();
1683                 while (node != endNode && toVisit == null) {
1684                     toVisit = node.getNextSibling();
1685                     node = node.getParent();
1686                 }
1687 
1688                 node = toVisit;
1689             }
1690         }
1691 
1692         return description.toString().trim();
1693     }
1694 
1695     /**
1696      * Processes a single node during description extraction and updates the state.
1697      * Delegates href-attribute handling and non-href node handling to separate helpers
1698      * to keep cyclomatic complexity within limits.
1699      *
1700      * @param node the current node being visited.
1701      * @param description the description buffer to append to.
1702      * @param state the mutable traversal state.
1703      * @param moduleName the name of the module (used for internal link resolution).
1704      * @throws MacroExecutionException if an internal link cannot be resolved.
1705      */
1706     private static void processDescriptionNode(DetailNode node,
1707                                                StringBuilder description,
1708                                                DescriptionTraversalState state,
1709                                                String moduleName)
1710             throws MacroExecutionException {
1711         if (node.getType() == JavadocCommentsTokenTypes.TAG_ATTR_NAME
1712                 && "href".equals(node.getText())) {
1713             state.inHrefAttribute = true;
1714         }
1715         if (state.inHrefAttribute && node.getType()
1716                 == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE) {
1717             processHrefAttributeValue(node, description, state, moduleName);
1718         }
1719         else {
1720             processNonHrefNode(node, description, state);
1721         }
1722     }
1723 
1724     /**
1725      * Handles an ATTRIBUTE_VALUE node that belongs to an href attribute.
1726      *
1727      * @param node the ATTRIBUTE_VALUE node.
1728      * @param description the description buffer to append to.
1729      * @param state the mutable traversal state.
1730      * @param moduleName the name of the module (used for internal link resolution).
1731      * @throws MacroExecutionException if an internal link cannot be resolved.
1732      */
1733     private static void processHrefAttributeValue(DetailNode node,
1734                                                   StringBuilder description,
1735                                                   DescriptionTraversalState state,
1736                                                   String moduleName)
1737             throws MacroExecutionException {
1738         final String href = node.getText();
1739         if (href.contains(CHECKSTYLE_ORG_URL)) {
1740             final String internalHref = href.replace(CHECKSTYLE_ORG_URL, "");
1741             final String path = internalHref.substring(1, internalHref.length() - 1);
1742             final String relativeHref = getLinkToDocument(moduleName, path);
1743 
1744             description.append('\"').append(relativeHref).append('\"');
1745         }
1746         else {
1747             description.append(href);
1748         }
1749         state.inHrefAttribute = false;
1750     }
1751 
1752     /**
1753      * Handles all nodes that are not an href ATTRIBUTE_VALUE, updating HTML-element
1754      * tracking, text content, and inline-tag (code/literal) tracking.
1755      *
1756      * @param node the current node.
1757      * @param description the description buffer to append to.
1758      * @param state the mutable traversal state.
1759      */
1760     private static void processNonHrefNode(DetailNode node,
1761                                            StringBuilder description,
1762                                            DescriptionTraversalState state) {
1763         processHtmlElementTracking(node, description, state);
1764         processTextContent(node, description, state);
1765         processInlineTagTracking(node, description, state);
1766     }
1767 
1768     /**
1769      * Updates HTML-element open/close tracking and appends closing tag text.
1770      *
1771      * @param node the current node.
1772      * @param description the description buffer to append to.
1773      * @param state the mutable traversal state.
1774      */
1775     private static void processHtmlElementTracking(DetailNode node,
1776                                                    StringBuilder description,
1777                                                    DescriptionTraversalState state) {
1778         if (node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
1779             state.inHtmlElement = true;
1780         }
1781         if (node.getType() == JavadocCommentsTokenTypes.TAG_CLOSE
1782                 && node.getParent().getType()
1783                 == JavadocCommentsTokenTypes.HTML_TAG_END) {
1784             description.append(node.getText());
1785             state.inHtmlElement = false;
1786         }
1787     }
1788 
1789     /**
1790      * Appends text content from the node, escaping special characters when inside
1791      * a {@code @code} or {@code @literal} inline tag.
1792      *
1793      * @param node the current node.
1794      * @param description the description buffer to append to.
1795      * @param state the mutable traversal state.
1796      */
1797     private static void processTextContent(DetailNode node,
1798                                            StringBuilder description,
1799                                            DescriptionTraversalState state) {
1800         if (isTextContent(node, state.inHtmlElement)) {
1801             if (state.inCodeLiteral || state.inLiteralTag) {
1802                 description.append(node.getText().trim()
1803                         .replace("&", "&amp;")
1804                         .replace("<", "&lt;")
1805                         .replace(">", "&gt;"));
1806             }
1807             else {
1808                 description.append(node.getText());
1809             }
1810         }
1811     }
1812 
1813     /**
1814      * Updates {@code @code} and {@code @literal} inline-tag tracking and appends
1815      * the opening/closing {@code <code>} HTML tags as needed.
1816      *
1817      * @param node the current node.
1818      * @param description the description buffer to append to.
1819      * @param state the mutable traversal state.
1820      */
1821     private static void processInlineTagTracking(DetailNode node,
1822                                                  StringBuilder description,
1823                                                  DescriptionTraversalState state) {
1824         if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME
1825                 && node.getParent().getType()
1826                 == JavadocCommentsTokenTypes.CODE_INLINE_TAG) {
1827             state.inCodeLiteral = true;
1828             description.append("<code>");
1829         }
1830         if (state.inCodeLiteral
1831                 && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) {
1832             state.inCodeLiteral = false;
1833             description.append("</code>");
1834         }
1835         if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME
1836                 && node.getParent().getType()
1837                 == JavadocCommentsTokenTypes.LITERAL_INLINE_TAG) {
1838             state.inLiteralTag = true;
1839         }
1840         if (state.inLiteralTag
1841                 && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) {
1842             state.inLiteralTag = false;
1843         }
1844     }
1845 
1846     /**
1847      * Checks whether the node contains text content that should be written to the description.
1848      *
1849      * @param node the node to check.
1850      * @param isInHtmlElement whether we are inside an HTML element.
1851      * @return true if the node contains text content to write.
1852      */
1853     private static boolean isTextContent(DetailNode node, boolean isInHtmlElement) {
1854         return node.getType() == JavadocCommentsTokenTypes.TEXT
1855                 || isInHtmlElement && node.getFirstChild() == null
1856                 && node.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK;
1857     }
1858 
1859     /**
1860      * Get 1st paragraph from the Javadoc with no additional processing.
1861      *
1862      * @param javadoc the Javadoc to extract first paragraph from.
1863      * @return first paragraph of javadoc.
1864      */
1865     public static String getFirstParagraphFromJavadoc(DetailNode javadoc) {
1866         final String result;
1867         final List<DetailNode> firstParagraphNodes = getFirstJavadocParagraphNodes(javadoc);
1868         if (firstParagraphNodes.isEmpty()) {
1869             result = "";
1870         }
1871         else {
1872             final DetailNode startNode = firstParagraphNodes.getFirst();
1873             final DetailNode endNode = firstParagraphNodes.getLast();
1874             result = JavadocMetadataScraperUtil.constructSubTreeText(startNode, endNode);
1875         }
1876         return result;
1877     }
1878 
1879     /**
1880      * Extracts first paragraph nodes from javadoc.
1881      *
1882      * @param javadoc the Javadoc to extract the description from.
1883      * @return the first paragraph nodes of the setter.
1884      */
1885     public static List<DetailNode> getFirstJavadocParagraphNodes(DetailNode javadoc) {
1886         final List<DetailNode> firstParagraphNodes = new ArrayList<>();
1887 
1888         if (javadoc != null) {
1889             for (DetailNode child = javadoc.getFirstChild();
1890                  child != null; child = child.getNextSibling()) {
1891                 if (isEndOfFirstJavadocParagraph(child)) {
1892                     break;
1893                 }
1894                 firstParagraphNodes.add(child);
1895             }
1896         }
1897         return firstParagraphNodes;
1898     }
1899 
1900     /**
1901      * Determines if the given child index is the end of the first Javadoc paragraph. The end
1902      * of the description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK,
1903      * NEWLINE, LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the
1904      * one below this line.
1905      *
1906      * @param child the child to check.
1907      * @return true if the given child index is the end of the first javadoc paragraph.
1908      */
1909     public static boolean isEndOfFirstJavadocParagraph(DetailNode child) {
1910         final DetailNode nextSibling = child.getNextSibling();
1911         boolean result = false;
1912         if (nextSibling != null) {
1913             final DetailNode secondNextSibling = nextSibling.getNextSibling();
1914             if (secondNextSibling != null) {
1915                 final DetailNode thirdNextSibling = secondNextSibling.getNextSibling();
1916                 if (thirdNextSibling != null) {
1917                     result = child.getType() == JavadocCommentsTokenTypes.NEWLINE
1918                             && nextSibling.getType()
1919                             == JavadocCommentsTokenTypes.LEADING_ASTERISK
1920                             && secondNextSibling.getType()
1921                             == JavadocCommentsTokenTypes.NEWLINE
1922                             && thirdNextSibling.getType()
1923                             == JavadocCommentsTokenTypes.LEADING_ASTERISK;
1924                 }
1925             }
1926         }
1927         return result;
1928     }
1929 
1930     /**
1931      * Simplifies type name just to the name of the class, rather than entire package.
1932      *
1933      * @param fullTypeName full type name.
1934      * @return simplified type name, that is, name of the class.
1935      */
1936     public static String simplifyTypeName(String fullTypeName) {
1937         final int simplifiedStartIndex;
1938 
1939         if (fullTypeName.contains("$")) {
1940             simplifiedStartIndex = fullTypeName.lastIndexOf('$') + 1;
1941         }
1942         else {
1943             simplifiedStartIndex = fullTypeName.lastIndexOf('.') + 1;
1944         }
1945 
1946         return fullTypeName.substring(simplifiedStartIndex);
1947     }
1948 
1949     /**
1950      * Mutable state bag used during DFS traversal in
1951      * {@link #getDescriptionFromJavadocForXdoc(DetailNode, String)}.
1952      * Extracting these flags into a dedicated class reduces the cyclomatic complexity
1953      * of the traversal method without changing any logic.
1954      */
1955     private static final class DescriptionTraversalState {
1956         /** Whether we are currently inside a {@code @code ...} inline tag. */
1957         private boolean inCodeLiteral;
1958         /** Whether we are currently inside a {@code {@literal ...}} inline tag. */
1959         private boolean inLiteralTag;
1960         /** Whether we are currently inside an HTML element. */
1961         private boolean inHtmlElement;
1962         /** Whether the next ATTRIBUTE_VALUE token is the value of an href attribute. */
1963         private boolean inHrefAttribute;
1964     }
1965 }