001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.site;
021
022import java.beans.PropertyDescriptor;
023import java.io.File;
024import java.io.IOException;
025import java.lang.reflect.Array;
026import java.lang.reflect.Field;
027import java.lang.reflect.InvocationTargetException;
028import java.lang.reflect.ParameterizedType;
029import java.net.URI;
030import java.nio.charset.StandardCharsets;
031import java.nio.file.Files;
032import java.nio.file.Path;
033import java.nio.file.Paths;
034import java.util.ArrayDeque;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.BitSet;
038import java.util.Collection;
039import java.util.Deque;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.LinkedHashMap;
043import java.util.List;
044import java.util.Locale;
045import java.util.Map;
046import java.util.Optional;
047import java.util.Set;
048import java.util.TreeSet;
049import java.util.regex.Pattern;
050import java.util.stream.Collectors;
051import java.util.stream.IntStream;
052import java.util.stream.Stream;
053
054import javax.annotation.Nullable;
055
056import org.apache.commons.beanutils.PropertyUtils;
057import org.apache.maven.doxia.macro.MacroExecutionException;
058
059import com.google.common.collect.Lists;
060import com.puppycrawl.tools.checkstyle.Checker;
061import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
062import com.puppycrawl.tools.checkstyle.ModuleFactory;
063import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
064import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
065import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
066import com.puppycrawl.tools.checkstyle.TreeWalker;
067import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
068import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
069import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
070import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
071import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
072import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
073import com.puppycrawl.tools.checkstyle.api.DetailNode;
074import com.puppycrawl.tools.checkstyle.api.Filter;
075import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
076import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
077import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
078import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
079import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
080import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
081import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
082import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
083import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
084
085/**
086 * Utility class for site generation.
087 */
088public final class SiteUtil {
089
090    /** The string 'tokens'. */
091    public static final String TOKENS = "tokens";
092    /** The string 'javadocTokens'. */
093    public static final String JAVADOC_TOKENS = "javadocTokens";
094    /** The string '.'. */
095    public static final String DOT = ".";
096    /** The string ', '. */
097    public static final String COMMA_SPACE = ", ";
098    /** The string 'TokenTypes'. */
099    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}