View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.beans.PropertyDescriptor;
23  import java.io.File;
24  import java.io.IOException;
25  import java.lang.module.ModuleDescriptor.Version;
26  import java.lang.reflect.Array;
27  import java.lang.reflect.Field;
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.ParameterizedType;
30  import java.net.URI;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.util.ArrayDeque;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.BitSet;
38  import java.util.Collection;
39  import java.util.Deque;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.LinkedHashMap;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.Optional;
47  import java.util.Set;
48  import java.util.TreeSet;
49  import java.util.regex.Pattern;
50  import java.util.stream.Collectors;
51  import java.util.stream.IntStream;
52  import java.util.stream.Stream;
53  
54  import javax.annotation.Nullable;
55  
56  import org.apache.commons.beanutils.PropertyUtils;
57  import org.apache.maven.doxia.macro.MacroExecutionException;
58  
59  import com.google.common.collect.Lists;
60  import com.puppycrawl.tools.checkstyle.Checker;
61  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
62  import com.puppycrawl.tools.checkstyle.ModuleFactory;
63  import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
64  import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
65  import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
66  import com.puppycrawl.tools.checkstyle.TreeWalker;
67  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
68  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
69  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
70  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
71  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
72  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
73  import com.puppycrawl.tools.checkstyle.api.DetailNode;
74  import com.puppycrawl.tools.checkstyle.api.Filter;
75  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
76  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
77  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
78  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
79  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
80  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
81  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
82  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
83  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
84  
85  /**
86   * Utility class for site generation.
87   */
88  public final class SiteUtil {
89  
90      /** The string 'tokens'. */
91      public static final String TOKENS = "tokens";
92      /** The string 'javadocTokens'. */
93      public static final String JAVADOC_TOKENS = "javadocTokens";
94      /** The string '.'. */
95      public static final String DOT = ".";
96      /** The string ', '. */
97      public static final String COMMA_SPACE = ", ";
98      /** The string 'TokenTypes'. */
99      public static final String TOKEN_TYPES = "TokenTypes";
100     /** The path to the TokenTypes.html file. */
101     public static final String PATH_TO_TOKEN_TYPES =
102             "apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html";
103     /** The path to the JavadocTokenTypes.html file. */
104     public static final String PATH_TO_JAVADOC_TOKEN_TYPES =
105             "apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html";
106     /** The string of JavaDoc module marking 'Since version'. */
107     public static final String SINCE_VERSION = "Since version";
108     /** The url of the checkstyle website. */
109     private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/";
110     /** The string 'charset'. */
111     private static final String CHARSET = "charset";
112     /** The string '{}'. */
113     private static final String CURLY_BRACKETS = "{}";
114     /** The string 'fileExtensions'. */
115     private static final String FILE_EXTENSIONS = "fileExtensions";
116     /** The string 'checks'. */
117     private static final String CHECKS = "checks";
118     /** The string 'naming'. */
119     private static final String NAMING = "naming";
120     /** The string 'src'. */
121     private static final String SRC = "src";
122     /** The whitespace. */
123     private static final String WHITESPACE = " ";
124 
125     /** Precompiled regex pattern to remove the "Setter to " prefix from strings. */
126     private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to ");
127 
128     /** Class name and their corresponding parent module name. */
129     private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries(
130         Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()),
131         Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()),
132         Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()),
133         Map.entry(Filter.class, Checker.class.getSimpleName()),
134         Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName())
135     );
136 
137     /** Set of properties that every check has. */
138     private static final Set<String> CHECK_PROPERTIES =
139             getProperties(AbstractCheck.class);
140 
141     /** Set of properties that every Javadoc check has. */
142     private static final Set<String> JAVADOC_CHECK_PROPERTIES =
143             getProperties(AbstractJavadocCheck.class);
144 
145     /** Set of properties that every FileSet check has. */
146     private static final Set<String> FILESET_PROPERTIES =
147             getProperties(AbstractFileSetCheck.class);
148 
149     /**
150      * Check and property name.
151      */
152     private static final String HEADER_CHECK_HEADER = "HeaderCheck.header";
153 
154     /**
155      * Check and property name.
156      */
157     private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header";
158 
159     /** Set of properties that are undocumented. Those are internal properties. */
160     private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
161         "SuppressWithNearbyCommentFilter.fileContents",
162         "SuppressionCommentFilter.fileContents"
163     );
164 
165     /** Properties that can not be gathered from class instance. */
166     private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
167         // static field (all upper case)
168         "SuppressWarningsHolder.aliasList",
169         // loads string into memory similar to file
170         HEADER_CHECK_HEADER,
171         REGEXP_HEADER_CHECK_HEADER,
172         // property is an int, but we cut off excess to accommodate old versions
173         "RedundantModifierCheck.jdkVersion",
174         // until https://github.com/checkstyle/checkstyle/issues/13376
175         "CustomImportOrderCheck.customImportOrderRules"
176     );
177 
178     /** Map of all superclasses properties and their javadocs. */
179     private static final Map<String, DetailNode> SUPER_CLASS_PROPERTIES_JAVADOCS =
180             new HashMap<>();
181 
182     /** Path to main source code folder. */
183     private static final String MAIN_FOLDER_PATH = Path.of(
184             SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString();
185 
186     /** List of files who are superclasses and contain certain properties that checks inherit. */
187     private static final List<Path> MODULE_SUPER_CLASS_PATHS = List.of(
188         Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractAccessControlNameCheck.java"),
189         Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractNameCheck.java"),
190         Path.of(MAIN_FOLDER_PATH, CHECKS, "javadoc", "AbstractJavadocCheck.java"),
191         Path.of(MAIN_FOLDER_PATH, "api", "AbstractFileSetCheck.java"),
192         Path.of(MAIN_FOLDER_PATH, CHECKS, "header", "AbstractHeaderCheck.java"),
193         Path.of(MAIN_FOLDER_PATH, CHECKS, "metrics", "AbstractClassCouplingCheck.java"),
194         Path.of(MAIN_FOLDER_PATH, CHECKS, "whitespace", "AbstractParenPadCheck.java")
195     );
196 
197     /**
198      * Private utility constructor.
199      */
200     private SiteUtil() {
201     }
202 
203     /**
204      * Get string values of the message keys from the given check class.
205      *
206      * @param module class to examine.
207      * @return a set of checkstyle's module message keys.
208      * @throws MacroExecutionException if extraction of message keys fails.
209      */
210     public static Set<String> getMessageKeys(Class<?> module)
211             throws MacroExecutionException {
212         final Set<Field> messageKeyFields = getCheckMessageKeys(module);
213         // We use a TreeSet to sort the message keys alphabetically
214         final Set<String> messageKeys = new TreeSet<>();
215         for (Field field : messageKeyFields) {
216             messageKeys.add(getFieldValue(field, module).toString());
217         }
218         return messageKeys;
219     }
220 
221     /**
222      * Gets the check's messages keys.
223      *
224      * @param module class to examine.
225      * @return a set of checkstyle's module message fields.
226      * @throws MacroExecutionException if the attempt to read a protected class fails.
227      * @noinspection ChainOfInstanceofChecks
228      * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at
229      *                     <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a>
230      *
231      */
232     private static Set<Field> getCheckMessageKeys(Class<?> module)
233             throws MacroExecutionException {
234         try {
235             final Set<Field> checkstyleMessages = new HashSet<>();
236 
237             // get all fields from current class
238             final Field[] fields = module.getDeclaredFields();
239 
240             for (Field field : fields) {
241                 if (field.getName().startsWith("MSG_")) {
242                     checkstyleMessages.add(field);
243                 }
244             }
245 
246             // deep scan class through hierarchy
247             final Class<?> superModule = module.getSuperclass();
248 
249             if (superModule != null) {
250                 checkstyleMessages.addAll(getCheckMessageKeys(superModule));
251             }
252 
253             // special cases that require additional classes
254             if (module == RegexpMultilineCheck.class) {
255                 checkstyleMessages.addAll(getCheckMessageKeys(Class
256                     .forName("com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector")));
257             }
258             else if (module == RegexpSinglelineCheck.class
259                     || module == RegexpSinglelineJavaCheck.class) {
260                 checkstyleMessages.addAll(getCheckMessageKeys(Class
261                     .forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector")));
262             }
263 
264             return checkstyleMessages;
265         }
266         catch (ClassNotFoundException exc) {
267             final String message = String.format(Locale.ROOT, "Couldn't find class: %s",
268                     module.getName());
269             throw new MacroExecutionException(message, exc);
270         }
271     }
272 
273     /**
274      * Returns the value of the given field.
275      *
276      * @param field the field.
277      * @param instance the instance of the module.
278      * @return the value of the field.
279      * @throws MacroExecutionException if the value could not be retrieved.
280      */
281     public static Object getFieldValue(Field field, Object instance)
282             throws MacroExecutionException {
283         try {
284             // required for package/private classes
285             field.trySetAccessible();
286             return field.get(instance);
287         }
288         catch (IllegalAccessException exc) {
289             throw new MacroExecutionException("Couldn't get field value", exc);
290         }
291     }
292 
293     /**
294      * Returns the instance of the module with the given name.
295      *
296      * @param moduleName the name of the module.
297      * @return the instance of the module.
298      * @throws MacroExecutionException if the module could not be created.
299      */
300     public static Object getModuleInstance(String moduleName) throws MacroExecutionException {
301         final ModuleFactory factory = getPackageObjectFactory();
302         try {
303             return factory.createModule(moduleName);
304         }
305         catch (CheckstyleException exc) {
306             throw new MacroExecutionException("Couldn't find class: " + moduleName, exc);
307         }
308     }
309 
310     /**
311      * Returns the default PackageObjectFactory with the default package names.
312      *
313      * @return the default PackageObjectFactory.
314      * @throws MacroExecutionException if the PackageObjectFactory cannot be created.
315      */
316     private static PackageObjectFactory getPackageObjectFactory() throws MacroExecutionException {
317         try {
318             final ClassLoader cl = ViolationMessagesMacro.class.getClassLoader();
319             final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
320             return new PackageObjectFactory(packageNames, cl);
321         }
322         catch (CheckstyleException exc) {
323             throw new MacroExecutionException("Couldn't load checkstyle modules", exc);
324         }
325     }
326 
327     /**
328      * Construct a string with a leading newline character and followed by
329      * the given amount of spaces. We use this method only to match indentation in
330      * regular xdocs and have minimal diff when parsing the templates.
331      * This method exists until
332      * <a href="https://github.com/checkstyle/checkstyle/issues/13426">13426</a>
333      *
334      * @param amountOfSpaces the amount of spaces to add after the newline.
335      * @return the constructed string.
336      */
337     public static String getNewlineAndIndentSpaces(int amountOfSpaces) {
338         return System.lineSeparator() + WHITESPACE.repeat(amountOfSpaces);
339     }
340 
341     /**
342      * Returns path to the template for the given module name or throws an exception if the
343      * template cannot be found.
344      *
345      * @param moduleName the module whose template we are looking for.
346      * @return path to the template.
347      * @throws MacroExecutionException if the template cannot be found.
348      */
349     public static Path getTemplatePath(String moduleName) throws MacroExecutionException {
350         final String fileNamePattern = ".*[\\\\/]"
351                 + moduleName.toLowerCase(Locale.ROOT) + "\\..*";
352         return getXdocsTemplatesFilePaths()
353             .stream()
354             .filter(path -> path.toString().matches(fileNamePattern))
355             .findFirst()
356             .orElse(null);
357     }
358 
359     /**
360      * Gets xdocs template file paths. These are files ending with .xml.template.
361      * This method will be changed to gather .xml once
362      * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
363      *
364      * @return a set of xdocs template file paths.
365      * @throws MacroExecutionException if an I/O error occurs.
366      */
367     public static Set<Path> getXdocsTemplatesFilePaths() throws MacroExecutionException {
368         final Path directory = Path.of("src/site/xdoc");
369         try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
370                 (path, attr) -> {
371                     return attr.isRegularFile()
372                             && path.toString().endsWith(".xml.template");
373                 })) {
374             return stream.collect(Collectors.toUnmodifiableSet());
375         }
376         catch (IOException ioException) {
377             throw new MacroExecutionException("Failed to find xdocs templates", ioException);
378         }
379     }
380 
381     /**
382      * Returns the parent module name for the given module class. Returns either
383      * "TreeWalker" or "Checker". Returns null if the module class is null.
384      *
385      * @param moduleClass the module class.
386      * @return the parent module name as a string.
387      * @throws MacroExecutionException if the parent module cannot be found.
388      */
389     public static String getParentModule(Class<?> moduleClass)
390                 throws MacroExecutionException {
391         String parentModuleName = "";
392         Class<?> parentClass = moduleClass.getSuperclass();
393 
394         while (parentClass != null) {
395             parentModuleName = CLASS_TO_PARENT_MODULE.get(parentClass);
396             if (parentModuleName != null) {
397                 break;
398             }
399             parentClass = parentClass.getSuperclass();
400         }
401 
402         // If parent class is not found, check interfaces
403         if (parentModuleName == null || parentModuleName.isEmpty()) {
404             final Class<?>[] interfaces = moduleClass.getInterfaces();
405             for (Class<?> interfaceClass : interfaces) {
406                 parentModuleName = CLASS_TO_PARENT_MODULE.get(interfaceClass);
407                 if (parentModuleName != null) {
408                     break;
409                 }
410             }
411         }
412 
413         if (parentModuleName == null || parentModuleName.isEmpty()) {
414             final String message = String.format(Locale.ROOT,
415                     "Failed to find parent module for %s", moduleClass.getSimpleName());
416             throw new MacroExecutionException(message);
417         }
418 
419         return parentModuleName;
420     }
421 
422     /**
423      * Get a set of properties for the given class that should be documented.
424      *
425      * @param clss the class to get the properties for.
426      * @param instance the instance of the module.
427      * @return a set of properties for the given class.
428      */
429     public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) {
430         final Set<String> properties =
431                 getProperties(clss).stream()
432                     .filter(prop -> {
433                         return !isGlobalProperty(clss, prop) && !isUndocumentedProperty(clss, prop);
434                     })
435                     .collect(Collectors.toCollection(HashSet::new));
436         properties.addAll(getNonExplicitProperties(instance, clss));
437         return new TreeSet<>(properties);
438     }
439 
440     /**
441      * Get the javadocs of the properties of the module. If the property is not present in the
442      * module, then the javadoc of the property from the superclass(es) is used.
443      *
444      * @param properties the properties of the module.
445      * @param moduleName the name of the module.
446      * @param modulePath the module file path.
447      * @return the javadocs of the properties of the module.
448      * @throws MacroExecutionException if an error occurs during processing.
449      */
450     public static Map<String, DetailNode> getPropertiesJavadocs(Set<String> properties,
451                                                                 String moduleName, Path modulePath)
452             throws MacroExecutionException {
453         // lazy initialization
454         if (SUPER_CLASS_PROPERTIES_JAVADOCS.isEmpty()) {
455             processSuperclasses();
456         }
457 
458         processModule(moduleName, modulePath);
459 
460         final Map<String, DetailNode> unmodifiableJavadocs =
461                 ClassAndPropertiesSettersJavadocScraper.getJavadocsForModuleOrProperty();
462         final Map<String, DetailNode> javadocs = new LinkedHashMap<>(unmodifiableJavadocs);
463 
464         properties.forEach(property -> {
465             final DetailNode superClassPropertyJavadoc =
466                     SUPER_CLASS_PROPERTIES_JAVADOCS.get(property);
467             if (superClassPropertyJavadoc != null) {
468                 javadocs.putIfAbsent(property, superClassPropertyJavadoc);
469             }
470         });
471 
472         assertAllPropertySetterJavadocsAreFound(properties, moduleName, javadocs);
473 
474         return javadocs;
475     }
476 
477     /**
478      * Assert that each property has a corresponding setter javadoc that is not null.
479      * 'tokens' and 'javadocTokens' are excluded from this check, because their
480      * description is different from the description of the setter.
481      *
482      * @param properties the properties of the module.
483      * @param moduleName the name of the module.
484      * @param javadocs the javadocs of the properties of the module.
485      * @throws MacroExecutionException if an error occurs during processing.
486      */
487     private static void assertAllPropertySetterJavadocsAreFound(
488             Set<String> properties, String moduleName, Map<String, DetailNode> javadocs)
489             throws MacroExecutionException {
490         for (String property : properties) {
491             final boolean isDocumented = javadocs.containsKey(property)
492                    || SUPER_CLASS_PROPERTIES_JAVADOCS.containsKey(property)
493                    || TOKENS.equals(property) || JAVADOC_TOKENS.equals(property);
494             if (!isDocumented) {
495                 throw new MacroExecutionException(String.format(Locale.ROOT,
496                    "%s: Missing documentation for property '%s'. Check superclasses.",
497                         moduleName, property));
498             }
499         }
500     }
501 
502     /**
503      * Collect the properties setters javadocs of the superclasses.
504      *
505      * @throws MacroExecutionException if an error occurs during processing.
506      */
507     private static void processSuperclasses() throws MacroExecutionException {
508         for (Path superclassPath : MODULE_SUPER_CLASS_PATHS) {
509             final Path fileNamePath = superclassPath.getFileName();
510             if (fileNamePath == null) {
511                 throw new MacroExecutionException("Invalid superclass path: " + superclassPath);
512             }
513             final String superclassName = CommonUtil.getFileNameWithoutExtension(
514                 fileNamePath.toString());
515             processModule(superclassName, superclassPath);
516             final Map<String, DetailNode> superclassJavadocs =
517                 ClassAndPropertiesSettersJavadocScraper.getJavadocsForModuleOrProperty();
518             SUPER_CLASS_PROPERTIES_JAVADOCS.putAll(superclassJavadocs);
519         }
520     }
521 
522     /**
523      * Scrape the Javadocs of the class and its properties setters with
524      * ClassAndPropertiesSettersJavadocScraper.
525      *
526      * @param moduleName the name of the module.
527      * @param modulePath the module Path.
528      * @throws MacroExecutionException if an error occurs during processing.
529      */
530     private static void processModule(String moduleName, Path modulePath)
531             throws MacroExecutionException {
532         if (!Files.isRegularFile(modulePath)) {
533             final String message = String.format(Locale.ROOT,
534                     "File %s is not a file. Please check the 'modulePath' property.", modulePath);
535             throw new MacroExecutionException(message);
536         }
537         ClassAndPropertiesSettersJavadocScraper.initialize(moduleName);
538         final Checker checker = new Checker();
539         checker.setModuleClassLoader(Checker.class.getClassLoader());
540         final DefaultConfiguration scraperCheckConfig =
541                         new DefaultConfiguration(
542                                 ClassAndPropertiesSettersJavadocScraper.class.getName());
543         final DefaultConfiguration defaultConfiguration =
544                 new DefaultConfiguration("configuration");
545         final DefaultConfiguration treeWalkerConfig =
546                 new DefaultConfiguration(TreeWalker.class.getName());
547         defaultConfiguration.addProperty(CHARSET, StandardCharsets.UTF_8.name());
548         defaultConfiguration.addChild(treeWalkerConfig);
549         treeWalkerConfig.addChild(scraperCheckConfig);
550         try {
551             checker.configure(defaultConfiguration);
552             final List<File> filesToProcess = List.of(modulePath.toFile());
553             checker.process(filesToProcess);
554             checker.destroy();
555         }
556         catch (CheckstyleException checkstyleException) {
557             final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName);
558             throw new MacroExecutionException(message, checkstyleException);
559         }
560     }
561 
562     /**
563      * Get a set of properties for the given class.
564      *
565      * @param clss the class to get the properties for.
566      * @return a set of properties for the given class.
567      */
568     public static Set<String> getProperties(Class<?> clss) {
569         final Set<String> result = new TreeSet<>();
570         final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss);
571 
572         for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
573             if (propertyDescriptor.getWriteMethod() != null) {
574                 result.add(propertyDescriptor.getName());
575             }
576         }
577 
578         return result;
579     }
580 
581     /**
582      * Checks if the property is a global property. Global properties come from the base classes
583      * and are common to all checks. For example id, severity, tabWidth, etc.
584      *
585      * @param clss the class of the module.
586      * @param propertyName the name of the property.
587      * @return true if the property is a global property.
588      */
589     private static boolean isGlobalProperty(Class<?> clss, String propertyName) {
590         return AbstractCheck.class.isAssignableFrom(clss)
591                     && CHECK_PROPERTIES.contains(propertyName)
592                 || AbstractJavadocCheck.class.isAssignableFrom(clss)
593                     && JAVADOC_CHECK_PROPERTIES.contains(propertyName)
594                 || AbstractFileSetCheck.class.isAssignableFrom(clss)
595                     && FILESET_PROPERTIES.contains(propertyName);
596     }
597 
598     /**
599      * Checks if the property is supposed to be documented.
600      *
601      * @param clss the class of the module.
602      * @param propertyName the name of the property.
603      * @return true if the property is supposed to be documented.
604      */
605     private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) {
606         return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName);
607     }
608 
609     /**
610      * Gets properties that are not explicitly captured but should be documented if
611      * certain conditions are met.
612      *
613      * @param instance the instance of the module.
614      * @param clss the class of the module.
615      * @return the non explicit properties.
616      */
617     private static Set<String> getNonExplicitProperties(
618             Object instance, Class<?> clss) {
619         final Set<String> result = new TreeSet<>();
620         if (AbstractCheck.class.isAssignableFrom(clss)) {
621             final AbstractCheck check = (AbstractCheck) instance;
622 
623             final int[] acceptableTokens = check.getAcceptableTokens();
624             Arrays.sort(acceptableTokens);
625             final int[] defaultTokens = check.getDefaultTokens();
626             Arrays.sort(defaultTokens);
627             final int[] requiredTokens = check.getRequiredTokens();
628             Arrays.sort(requiredTokens);
629 
630             if (!Arrays.equals(acceptableTokens, defaultTokens)
631                     || !Arrays.equals(acceptableTokens, requiredTokens)) {
632                 result.add(TOKENS);
633             }
634         }
635 
636         if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
637             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
638             result.add("violateExecutionOnNonTightHtml");
639 
640             final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
641             Arrays.sort(acceptableJavadocTokens);
642             final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
643             Arrays.sort(defaultJavadocTokens);
644             final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
645             Arrays.sort(requiredJavadocTokens);
646 
647             if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
648                     || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
649                 result.add(JAVADOC_TOKENS);
650             }
651         }
652 
653         if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
654             result.add(FILE_EXTENSIONS);
655         }
656         return result;
657     }
658 
659     /**
660      * Get the description of the property.
661      *
662      * @param propertyName the name of the property.
663      * @param javadoc the Javadoc of the property setter method.
664      * @param moduleName the name of the module.
665      * @return the description of the property.
666      * @throws MacroExecutionException if the description could not be extracted.
667      */
668     public static String getPropertyDescription(
669             String propertyName, DetailNode javadoc, String moduleName)
670             throws MacroExecutionException {
671         final String description;
672         if (TOKENS.equals(propertyName)) {
673             description = "tokens to check";
674         }
675         else if (JAVADOC_TOKENS.equals(propertyName)) {
676             description = "javadoc tokens to check";
677         }
678         else {
679             final String descriptionString = SETTER_PATTERN.matcher(
680                     DescriptionExtractor.getDescriptionFromJavadoc(javadoc, moduleName))
681                     .replaceFirst("");
682 
683             final String firstLetterCapitalized = descriptionString.substring(0, 1)
684                     .toUpperCase(Locale.ROOT);
685             description = firstLetterCapitalized + descriptionString.substring(1);
686         }
687         return description;
688     }
689 
690     /**
691      * Get the since version of the property.
692      *
693      * @param moduleName the name of the module.
694      * @param moduleJavadoc the Javadoc of the module.
695      * @param propertyName the name of the property.
696      * @param propertyJavadoc the Javadoc of the property setter method.
697      * @return the since version of the property.
698      * @throws MacroExecutionException if the module since version could not be extracted.
699      */
700     public static String getSinceVersion(String moduleName, DetailNode moduleJavadoc,
701                                          String propertyName, DetailNode propertyJavadoc)
702             throws MacroExecutionException {
703         final String sinceVersion;
704 
705         final Optional<String> specifiedPropertyVersion =
706             getSpecifiedPropertyVersion(propertyName, moduleJavadoc);
707 
708         if (specifiedPropertyVersion.isPresent()) {
709             sinceVersion = specifiedPropertyVersion.get();
710         }
711         else {
712             final String moduleSince = getSinceVersionFromJavadoc(moduleJavadoc);
713 
714             if (moduleSince == null) {
715                 throw new MacroExecutionException(
716                         "Missing @since on module " + moduleName);
717             }
718 
719             String propertySince = null;
720             if (propertyJavadoc != null) {
721                 propertySince = getSinceVersionFromJavadoc(propertyJavadoc);
722             }
723 
724             if (propertySince != null && isVersionAtLeast(propertySince, moduleSince)) {
725                 sinceVersion = propertySince;
726             }
727             else {
728                 sinceVersion = moduleSince;
729             }
730         }
731 
732         return sinceVersion;
733     }
734 
735     /**
736      * Gets the specifically indicated version of module's property from the javadoc of module.
737      *
738      * @param propertyName the name of property.
739      * @param moduleJavadoc the javadoc of module.
740      * @return the specific since version of module's property.
741      * @throws MacroExecutionException if the module since version could not be extracted.
742      */
743     private static Optional<String> getSpecifiedPropertyVersion(String propertyName,
744                                                       DetailNode moduleJavadoc)
745             throws MacroExecutionException {
746         Optional<String> specifiedVersion = Optional.empty();
747 
748         final Optional<DetailNode> propertyModuleJavadoc =
749             getPropertyJavadocNodeInModule(propertyName, moduleJavadoc);
750 
751         if (propertyModuleJavadoc.isPresent()) {
752             final DetailNode primaryJavadocInlineTag = JavadocUtil.findFirstToken(
753                 propertyModuleJavadoc.get(), JavadocTokenTypes.JAVADOC_INLINE_TAG);
754 
755             for (DetailNode textNode = JavadocUtil
756                 .getNextSibling(primaryJavadocInlineTag, JavadocTokenTypes.TEXT);
757                  textNode != null && specifiedVersion.isEmpty();
758                  textNode = JavadocUtil.getNextSibling(
759                      textNode, JavadocTokenTypes.TEXT)) {
760 
761                 final String textNodeText = textNode.getText();
762 
763                 if (textNodeText.startsWith(WHITESPACE + SINCE_VERSION)) {
764                     final int sinceVersionIndex = textNodeText.indexOf('.') - 1;
765 
766                     if (sinceVersionIndex > 0) {
767                         specifiedVersion = Optional.of(textNodeText.substring(sinceVersionIndex));
768                     }
769                     else {
770                         throw new MacroExecutionException(textNodeText
771                             + " has no valid version, at least one '.' is expected.");
772                     }
773 
774                 }
775             }
776         }
777 
778         return specifiedVersion;
779     }
780 
781     /**
782      * Gets the javadoc node part of the property from the javadoc of the module.
783      *
784      * @param propertyName the name of property.
785      * @param moduleJavadoc the javadoc of module.
786      * @return the Optional of javadoc node part of the property.
787      */
788     private static Optional<DetailNode> getPropertyJavadocNodeInModule(String propertyName,
789                                                              DetailNode moduleJavadoc) {
790         Optional<DetailNode> propertyJavadocNode = Optional.empty();
791 
792         for (DetailNode htmlElement = JavadocUtil.getNextSibling(
793                 JavadocUtil.getFirstChild(moduleJavadoc), JavadocTokenTypes.HTML_ELEMENT);
794             htmlElement != null && propertyJavadocNode.isEmpty();
795             htmlElement = JavadocUtil.getNextSibling(
796                 htmlElement, JavadocTokenTypes.HTML_ELEMENT)) {
797 
798             final DetailNode htmlTag = JavadocUtil.findFirstToken(
799                 htmlElement, JavadocTokenTypes.HTML_TAG);
800             final Optional<String> htmlTagName = Optional.ofNullable(htmlTag)
801                 .map(JavadocUtil::getFirstChild)
802                 .map(htmlStart -> {
803                     return JavadocUtil.findFirstToken(htmlStart, JavadocTokenTypes.HTML_TAG_NAME);
804                 })
805                 .map(DetailNode::getText);
806 
807             if (htmlTag != null && "ul".equals(htmlTagName.orElse(null))) {
808 
809                 boolean foundProperty = false;
810 
811                 for (DetailNode innerHtmlElement = JavadocUtil.getNextSibling(
812                         JavadocUtil.getFirstChild(htmlTag), JavadocTokenTypes.HTML_ELEMENT);
813                     innerHtmlElement != null && !foundProperty;
814                     innerHtmlElement = JavadocUtil.getNextSibling(
815                         innerHtmlElement, JavadocTokenTypes.HTML_ELEMENT)) {
816 
817                     final DetailNode liTag = JavadocUtil.getFirstChild(innerHtmlElement);
818 
819                     if (liTag.getType() == JavadocTokenTypes.LI) {
820 
821                         final DetailNode primeJavadocInlineTag = JavadocUtil.findFirstToken(liTag,
822                             JavadocTokenTypes.JAVADOC_INLINE_TAG);
823 
824                         if (primeJavadocInlineTag == null) {
825                             break;
826                         }
827 
828                         final String examinedPropertyName = JavadocUtil.findFirstToken(
829                             primeJavadocInlineTag, JavadocTokenTypes.TEXT).getText();
830 
831                         if (examinedPropertyName.equals(propertyName)) {
832                             propertyJavadocNode = Optional.ofNullable(liTag);
833                             foundProperty = true;
834                         }
835                     }
836                 }
837             }
838         }
839 
840         return propertyJavadocNode;
841     }
842 
843     /**
844      * Extract the since version from the Javadoc.
845      *
846      * @param javadoc the Javadoc to extract the since version from.
847      * @return the since version of the setter.
848      */
849     @Nullable
850     private static String getSinceVersionFromJavadoc(DetailNode javadoc) {
851         final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc);
852         return Optional.ofNullable(sinceJavadocTag)
853             .map(tag -> JavadocUtil.findFirstToken(tag, JavadocTokenTypes.DESCRIPTION))
854             .map(description -> JavadocUtil.findFirstToken(description, JavadocTokenTypes.TEXT))
855             .map(DetailNode::getText)
856             .orElse(null);
857     }
858 
859     /**
860      * Find the since Javadoc tag node in the given Javadoc.
861      *
862      * @param javadoc the Javadoc to search.
863      * @return the since Javadoc tag node or null if not found.
864      */
865     private static DetailNode getSinceJavadocTag(DetailNode javadoc) {
866         final DetailNode[] children = javadoc.getChildren();
867         DetailNode javadocTagWithSince = null;
868         for (final DetailNode child : children) {
869             if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) {
870                 final DetailNode sinceNode = JavadocUtil.findFirstToken(
871                         child, JavadocTokenTypes.SINCE_LITERAL);
872                 if (sinceNode != null) {
873                     javadocTagWithSince = child;
874                     break;
875                 }
876             }
877         }
878         return javadocTagWithSince;
879     }
880 
881     /**
882      * Returns {@code true} if {@code actualVersion} ≥ {@code requiredVersion}.
883      * Both versions have any trailing "-SNAPSHOT" stripped before comparison.
884      *
885      * @param actualVersion   e.g. "8.3" or "8.3-SNAPSHOT"
886      * @param requiredVersion e.g. "8.3"
887      * @return {@code true} if actualVersion exists, and, numerically, is at least requiredVersion
888      */
889     private static boolean isVersionAtLeast(String actualVersion,
890                                             String requiredVersion) {
891         final Version actualVersionParsed = Version.parse(actualVersion);
892         final Version requiredVersionParsed = Version.parse(requiredVersion);
893 
894         return actualVersionParsed.compareTo(requiredVersionParsed) >= 0;
895     }
896 
897     /**
898      * Get the type of the property.
899      *
900      * @param field the field to get the type of.
901      * @param propertyName the name of the property.
902      * @param moduleName the name of the module.
903      * @param instance the instance of the module.
904      * @return the type of the property.
905      * @throws MacroExecutionException if an error occurs during getting the type.
906      */
907     public static String getType(Field field, String propertyName,
908                                  String moduleName, Object instance)
909             throws MacroExecutionException {
910         final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance);
911         return Optional.ofNullable(field)
912                 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
913                 .map(propertyType -> propertyType.value().getDescription())
914                 .orElseGet(fieldClass::getSimpleName);
915     }
916 
917     /**
918      * Get the default value of the property.
919      *
920      * @param propertyName the name of the property.
921      * @param field the field to get the default value of.
922      * @param classInstance the instance of the class to get the default value of.
923      * @param moduleName the name of the module.
924      * @return the default value of the property.
925      * @throws MacroExecutionException if an error occurs during getting the default value.
926      * @noinspection IfStatementWithTooManyBranches
927      * @noinspectionreason IfStatementWithTooManyBranches - complex nature of getting properties
928      *      from XML files requires giant if/else statement
929      */
930     // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
931     public static String getDefaultValue(String propertyName, Field field,
932                                          Object classInstance, String moduleName)
933             throws MacroExecutionException {
934         final Object value = getFieldValue(field, classInstance);
935         final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, classInstance);
936         String result = null;
937         if (CHARSET.equals(propertyName)) {
938             result = "the charset property of the parent"
939                     + " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module";
940         }
941         else if (classInstance instanceof PropertyCacheFile) {
942             result = "null (no cache file)";
943         }
944         else if (fieldClass == boolean.class) {
945             result = value.toString();
946         }
947         else if (fieldClass == int.class) {
948             result = value.toString();
949         }
950         else if (fieldClass == int[].class) {
951             result = getIntArrayPropertyValue(value);
952         }
953         else if (fieldClass == double[].class) {
954             result = removeSquareBrackets(Arrays.toString((double[]) value).replace(".0", ""));
955             if (result.isEmpty()) {
956                 result = CURLY_BRACKETS;
957             }
958         }
959         else if (fieldClass == String[].class) {
960             result = getStringArrayPropertyValue(propertyName, value);
961         }
962         else if (fieldClass == URI.class || fieldClass == String.class) {
963             if (value != null) {
964                 result = '"' + value.toString() + '"';
965             }
966         }
967         else if (fieldClass == Pattern.class) {
968             if (value != null) {
969                 result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t")
970                         .replace("\r", "\\r").replace("\f", "\\f") + '"';
971             }
972         }
973         else if (fieldClass == Pattern[].class) {
974             result = getPatternArrayPropertyValue(value);
975         }
976         else if (fieldClass.isEnum()) {
977             if (value != null) {
978                 result = value.toString().toLowerCase(Locale.ENGLISH);
979             }
980         }
981         else if (fieldClass == AccessModifierOption[].class) {
982             result = removeSquareBrackets(Arrays.toString((Object[]) value));
983         }
984         else {
985             final String message = String.format(Locale.ROOT,
986                     "Unknown property type: %s", fieldClass.getSimpleName());
987             throw new MacroExecutionException(message);
988         }
989 
990         if (result == null) {
991             result = "null";
992         }
993 
994         return result;
995     }
996 
997     /**
998      * Gets the name of the bean property's default value for the Pattern array class.
999      *
1000      * @param fieldValue The bean property's value
1001      * @return String form of property's default value
1002      */
1003     private static String getPatternArrayPropertyValue(Object fieldValue) {
1004         Object value = fieldValue;
1005         if (value instanceof Collection) {
1006             final Collection<?> collection = (Collection<?>) value;
1007 
1008             value = collection.stream()
1009                     .map(Pattern.class::cast)
1010                     .toArray(Pattern[]::new);
1011         }
1012 
1013         String result = "";
1014         if (value != null && Array.getLength(value) > 0) {
1015             result = removeSquareBrackets(
1016                     Arrays.stream((Pattern[]) value)
1017                     .map(Pattern::pattern)
1018                     .collect(Collectors.joining(COMMA_SPACE)));
1019         }
1020 
1021         if (result.isEmpty()) {
1022             result = CURLY_BRACKETS;
1023         }
1024         return result;
1025     }
1026 
1027     /**
1028      * Removes square brackets [ and ] from the given string.
1029      *
1030      * @param value the string to remove square brackets from.
1031      * @return the string without square brackets.
1032      */
1033     private static String removeSquareBrackets(String value) {
1034         return value
1035                 .replace("[", "")
1036                 .replace("]", "");
1037     }
1038 
1039     /**
1040      * Gets the name of the bean property's default value for the string array class.
1041      *
1042      * @param propertyName The bean property's name
1043      * @param value The bean property's value
1044      * @return String form of property's default value
1045      */
1046     private static String getStringArrayPropertyValue(String propertyName, Object value) {
1047         String result;
1048         if (value == null) {
1049             result = "";
1050         }
1051         else {
1052             try (Stream<?> valuesStream = getValuesStream(value)) {
1053                 result = valuesStream
1054                     .map(String.class::cast)
1055                     .sorted()
1056                     .collect(Collectors.joining(COMMA_SPACE));
1057             }
1058         }
1059 
1060         if (result.isEmpty()) {
1061             if (FILE_EXTENSIONS.equals(propertyName)) {
1062                 result = "all files";
1063             }
1064             else {
1065                 result = CURLY_BRACKETS;
1066             }
1067         }
1068         return result;
1069     }
1070 
1071     /**
1072      * Generates a stream of values from the given value.
1073      *
1074      * @param value the value to generate the stream from.
1075      * @return the stream of values.
1076      */
1077     private static Stream<?> getValuesStream(Object value) {
1078         final Stream<?> valuesStream;
1079         if (value instanceof Collection) {
1080             final Collection<?> collection = (Collection<?>) value;
1081             valuesStream = collection.stream();
1082         }
1083         else {
1084             final Object[] array = (Object[]) value;
1085             valuesStream = Arrays.stream(array);
1086         }
1087         return valuesStream;
1088     }
1089 
1090     /**
1091      * Returns the name of the bean property's default value for the int array class.
1092      *
1093      * @param value The bean property's value.
1094      * @return String form of property's default value.
1095      */
1096     private static String getIntArrayPropertyValue(Object value) {
1097         try (IntStream stream = getIntStream(value)) {
1098             String result = stream
1099                     .mapToObj(TokenUtil::getTokenName)
1100                     .sorted()
1101                     .collect(Collectors.joining(COMMA_SPACE));
1102             if (result.isEmpty()) {
1103                 result = CURLY_BRACKETS;
1104             }
1105             return result;
1106         }
1107     }
1108 
1109     /**
1110      * Get the int stream from the given value.
1111      *
1112      * @param value the value to get the int stream from.
1113      * @return the int stream.
1114      */
1115     private static IntStream getIntStream(Object value) {
1116         final IntStream stream;
1117         if (value instanceof Collection) {
1118             final Collection<?> collection = (Collection<?>) value;
1119             stream = collection.stream()
1120                     .mapToInt(int.class::cast);
1121         }
1122         else if (value instanceof BitSet) {
1123             stream = ((BitSet) value).stream();
1124         }
1125         else {
1126             stream = Arrays.stream((int[]) value);
1127         }
1128         return stream;
1129     }
1130 
1131     /**
1132      * Gets the class of the given field.
1133      *
1134      * @param field the field to get the class of.
1135      * @param propertyName the name of the property.
1136      * @param moduleName the name of the module.
1137      * @param instance the instance of the module.
1138      * @return the class of the field.
1139      * @throws MacroExecutionException if an error occurs during getting the class.
1140      */
1141     // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
1142     private static Class<?> getFieldClass(Field field, String propertyName,
1143                                           String moduleName, Object instance)
1144             throws MacroExecutionException {
1145         Class<?> result = null;
1146 
1147         if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1148                 .contains(moduleName + DOT + propertyName)) {
1149             result = getPropertyClass(propertyName, instance);
1150         }
1151         if (field != null && result == null) {
1152             result = field.getType();
1153         }
1154         if (result == null) {
1155             throw new MacroExecutionException(
1156                     "Could not find field " + propertyName + " in class " + moduleName);
1157         }
1158         if (field != null && (result == List.class || result == Set.class)) {
1159             final ParameterizedType type = (ParameterizedType) field.getGenericType();
1160             final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1161 
1162             if (parameterClass == Integer.class) {
1163                 result = int[].class;
1164             }
1165             else if (parameterClass == String.class) {
1166                 result = String[].class;
1167             }
1168             else if (parameterClass == Pattern.class) {
1169                 result = Pattern[].class;
1170             }
1171             else {
1172                 final String message = "Unknown parameterized type: "
1173                         + parameterClass.getSimpleName();
1174                 throw new MacroExecutionException(message);
1175             }
1176         }
1177         else if (result == BitSet.class) {
1178             result = int[].class;
1179         }
1180 
1181         return result;
1182     }
1183 
1184     /**
1185      * Gets the class of the given java property.
1186      *
1187      * @param propertyName the name of the property.
1188      * @param instance the instance of the module.
1189      * @return the class of the java property.
1190      * @throws MacroExecutionException if an error occurs during getting the class.
1191      */
1192     // -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field
1193     public static Class<?> getPropertyClass(String propertyName, Object instance)
1194             throws MacroExecutionException {
1195         final Class<?> result;
1196         try {
1197             final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1198                     propertyName);
1199             result = descriptor.getPropertyType();
1200         }
1201         catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1202             throw new MacroExecutionException(exc.getMessage(), exc);
1203         }
1204         return result;
1205     }
1206 
1207     /**
1208      * Get the difference between two lists of tokens.
1209      *
1210      * @param tokens the list of tokens to remove from.
1211      * @param subtractions the tokens to remove.
1212      * @return the difference between the two lists.
1213      */
1214     public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1215         final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1216                 .boxed()
1217                 .collect(Collectors.toUnmodifiableSet());
1218         return Arrays.stream(tokens)
1219                 .boxed()
1220                 .filter(token -> !subtractionsSet.contains(token))
1221                 .collect(Collectors.toUnmodifiableList());
1222     }
1223 
1224     /**
1225      * Gets the field with the given name from the given class.
1226      *
1227      * @param fieldClass the class to get the field from.
1228      * @param propertyName the name of the field.
1229      * @return the field we are looking for.
1230      */
1231     public static Field getField(Class<?> fieldClass, String propertyName) {
1232         Field result = null;
1233         Class<?> currentClass = fieldClass;
1234 
1235         while (!Object.class.equals(currentClass)) {
1236             try {
1237                 result = currentClass.getDeclaredField(propertyName);
1238                 result.trySetAccessible();
1239                 break;
1240             }
1241             catch (NoSuchFieldException ignored) {
1242                 currentClass = currentClass.getSuperclass();
1243             }
1244         }
1245 
1246         return result;
1247     }
1248 
1249     /**
1250      * Constructs string with relative link to the provided document.
1251      *
1252      * @param moduleName the name of the module.
1253      * @param document the path of the document.
1254      * @return relative link to the document.
1255      * @throws MacroExecutionException if link to the document cannot be constructed.
1256      */
1257     public static String getLinkToDocument(String moduleName, String document)
1258             throws MacroExecutionException {
1259         final Path templatePath = getTemplatePath(moduleName.replace("Check", ""));
1260         if (templatePath == null) {
1261             throw new MacroExecutionException(
1262                     String.format(Locale.ROOT,
1263                             "Could not find template for %s", moduleName));
1264         }
1265         final Path templatePathParent = templatePath.getParent();
1266         if (templatePathParent == null) {
1267             throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1268         }
1269         return templatePathParent
1270                 .relativize(Path.of(SRC, "site/xdoc", document))
1271                 .toString()
1272                 .replace(".xml", ".html")
1273                 .replace('\\', '/');
1274     }
1275 
1276     /** Utility class for extracting description from a method's Javadoc. */
1277     private static final class DescriptionExtractor {
1278 
1279         /**
1280          * Extracts the description from the javadoc detail node. Performs a DFS traversal on the
1281          * detail node and extracts the text nodes.
1282          *
1283          * @param javadoc the Javadoc to extract the description from.
1284          * @param moduleName the name of the module.
1285          * @return the description of the setter.
1286          * @throws MacroExecutionException if the description could not be extracted.
1287          * @noinspection TooBroadScope
1288          * @noinspectionreason TooBroadScope - complex nature of method requires large scope
1289          */
1290         // -@cs[NPathComplexity] Splitting would not make the code more readable
1291         // -@cs[CyclomaticComplexity] Splitting would not make the code more readable.
1292         private static String getDescriptionFromJavadoc(DetailNode javadoc, String moduleName)
1293                 throws MacroExecutionException {
1294             boolean isInCodeLiteral = false;
1295             boolean isInHtmlElement = false;
1296             boolean isInHrefAttribute = false;
1297             final StringBuilder description = new StringBuilder(128);
1298             final Deque<DetailNode> queue = new ArrayDeque<>();
1299             final List<DetailNode> descriptionNodes = getDescriptionNodes(javadoc);
1300             Lists.reverse(descriptionNodes).forEach(queue::push);
1301 
1302             // Perform DFS traversal on description nodes
1303             while (!queue.isEmpty()) {
1304                 final DetailNode node = queue.pop();
1305                 Lists.reverse(Arrays.asList(node.getChildren())).forEach(queue::push);
1306 
1307                 if (node.getType() == JavadocTokenTypes.HTML_TAG_NAME
1308                         && "href".equals(node.getText())) {
1309                     isInHrefAttribute = true;
1310                 }
1311                 if (isInHrefAttribute && node.getType() == JavadocTokenTypes.ATTR_VALUE) {
1312                     final String href = node.getText();
1313                     if (href.contains(CHECKSTYLE_ORG_URL)) {
1314                         handleInternalLink(description, moduleName, href);
1315                     }
1316                     else {
1317                         description.append(href);
1318                     }
1319 
1320                     isInHrefAttribute = false;
1321                     continue;
1322                 }
1323                 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
1324                     isInHtmlElement = true;
1325                 }
1326                 if (node.getType() == JavadocTokenTypes.END
1327                         && node.getParent().getType() == JavadocTokenTypes.HTML_ELEMENT_END) {
1328                     description.append(node.getText());
1329                     isInHtmlElement = false;
1330                 }
1331                 if (node.getType() == JavadocTokenTypes.TEXT
1332                         // If a node has children, its text is not part of the description
1333                         || isInHtmlElement && node.getChildren().length == 0
1334                             // Some HTML elements span multiple lines, so we avoid the asterisk
1335                             && node.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
1336                     description.append(node.getText());
1337                 }
1338                 if (node.getType() == JavadocTokenTypes.CODE_LITERAL) {
1339                     isInCodeLiteral = true;
1340                     description.append("<code>");
1341                 }
1342                 if (isInCodeLiteral
1343                         && node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
1344                     isInCodeLiteral = false;
1345                     description.append("</code>");
1346                 }
1347             }
1348             return description.toString().trim();
1349         }
1350 
1351         /**
1352          * Converts the href value to a relative link to the document and appends it to the
1353          * description.
1354          *
1355          * @param description the description to append the relative link to.
1356          * @param moduleName the name of the module.
1357          * @param value the href value.
1358          * @throws MacroExecutionException if the relative link could not be created.
1359          */
1360         private static void handleInternalLink(StringBuilder description,
1361                                                String moduleName, String value)
1362                 throws MacroExecutionException {
1363             String href = value;
1364             href = href.replace(CHECKSTYLE_ORG_URL, "");
1365             // Remove first and last characters, they are always double quotes
1366             href = href.substring(1, href.length() - 1);
1367 
1368             final String relativeHref = getLinkToDocument(moduleName, href);
1369             final char doubleQuote = '\"';
1370             description.append(doubleQuote).append(relativeHref).append(doubleQuote);
1371         }
1372 
1373         /**
1374          * Extracts description nodes from javadoc.
1375          *
1376          * @param javadoc the Javadoc to extract the description from.
1377          * @return the description nodes of the setter.
1378          */
1379         private static List<DetailNode> getDescriptionNodes(DetailNode javadoc) {
1380             final DetailNode[] children = javadoc.getChildren();
1381             final List<DetailNode> descriptionNodes = new ArrayList<>();
1382             for (final DetailNode child : children) {
1383                 if (isEndOfDescription(child)) {
1384                     break;
1385                 }
1386                 descriptionNodes.add(child);
1387             }
1388             return descriptionNodes;
1389         }
1390 
1391         /**
1392          * Determines if the given child index is the end of the description. The end of the
1393          * description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK, NEWLINE,
1394          * LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the one below
1395          * this line.
1396          *
1397          * @param child the child to check.
1398          * @return true if the given child index is the end of the description.
1399          */
1400         private static boolean isEndOfDescription(DetailNode child) {
1401             final DetailNode nextSibling = JavadocUtil.getNextSibling(child);
1402             final DetailNode secondNextSibling = JavadocUtil.getNextSibling(nextSibling);
1403             final DetailNode thirdNextSibling = JavadocUtil.getNextSibling(secondNextSibling);
1404 
1405             return child.getType() == JavadocTokenTypes.NEWLINE
1406                         && nextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK
1407                         && secondNextSibling.getType() == JavadocTokenTypes.NEWLINE
1408                         && thirdNextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK;
1409         }
1410     }
1411 }