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        // 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}