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