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