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