001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.site;
021
022import java.beans.PropertyDescriptor;
023import java.io.File;
024import java.io.IOException;
025import java.lang.module.ModuleDescriptor.Version;
026import java.lang.reflect.Array;
027import java.lang.reflect.Field;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.ParameterizedType;
030import java.net.URI;
031import java.nio.file.Files;
032import java.nio.file.Path;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.BitSet;
036import java.util.Collection;
037import java.util.Collections;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Locale;
041import java.util.Map;
042import java.util.Optional;
043import java.util.Set;
044import java.util.TreeMap;
045import java.util.TreeSet;
046import java.util.regex.Pattern;
047import java.util.stream.Collectors;
048import java.util.stream.IntStream;
049import java.util.stream.Stream;
050
051import org.apache.commons.beanutils.PropertyUtils;
052import org.apache.maven.doxia.macro.MacroExecutionException;
053
054import com.puppycrawl.tools.checkstyle.Checker;
055import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
056import com.puppycrawl.tools.checkstyle.ModuleFactory;
057import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
058import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
059import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
060import com.puppycrawl.tools.checkstyle.PropertyType;
061import com.puppycrawl.tools.checkstyle.TreeWalker;
062import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
063import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
064import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
065import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
066import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
067import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
068import com.puppycrawl.tools.checkstyle.api.DetailNode;
069import com.puppycrawl.tools.checkstyle.api.Filter;
070import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
071import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
072import com.puppycrawl.tools.checkstyle.internal.annotation.PreserveOrder;
073import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraperUtil;
074import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
075import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
076import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
077
078/**
079 * Utility class for site generation.
080 */
081public final class SiteUtil {
082
083    /** The string 'tokens'. */
084    public static final String TOKENS = "tokens";
085    /** The string 'javadocTokens'. */
086    public static final String JAVADOC_TOKENS = "javadocTokens";
087    /** The string 'violateExecutionOnNonTightHtml'. */
088    public static final String VIOLATE_EXECUTION_ON_NON_TIGHT_HTML =
089            "violateExecutionOnNonTightHtml";
090    /** The string '.'. */
091    public static final String DOT = ".";
092    /** The string ','. */
093    public static final String COMMA = ",";
094    /** The whitespace. */
095    public static final String WHITESPACE = " ";
096    /** The string ', '. */
097    public static final String COMMA_SPACE = COMMA + WHITESPACE;
098    /** The string 'TokenTypes'. */
099    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}