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