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