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