SiteUtil.java
///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2024 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
///////////////////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.site;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.maven.doxia.macro.MacroExecutionException;
import com.google.common.collect.Lists;
import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
import com.puppycrawl.tools.checkstyle.ModuleFactory;
import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
import com.puppycrawl.tools.checkstyle.TreeWalker;
import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.Filter;
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
/**
* Utility class for site generation.
*/
public final class SiteUtil {
/** The string 'tokens'. */
public static final String TOKENS = "tokens";
/** The string 'javadocTokens'. */
public static final String JAVADOC_TOKENS = "javadocTokens";
/** The string '.'. */
public static final String DOT = ".";
/** The string ', '. */
public static final String COMMA_SPACE = ", ";
/** The string 'TokenTypes'. */
public static final String TOKEN_TYPES = "TokenTypes";
/** The path to the TokenTypes.html file. */
public static final String PATH_TO_TOKEN_TYPES =
"apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html";
/** The path to the JavadocTokenTypes.html file. */
public static final String PATH_TO_JAVADOC_TOKEN_TYPES =
"apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html";
/** The url of the checkstyle website. */
private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/";
/** The string 'charset'. */
private static final String CHARSET = "charset";
/** The string '{}'. */
private static final String CURLY_BRACKETS = "{}";
/** The string 'fileExtensions'. */
private static final String FILE_EXTENSIONS = "fileExtensions";
/** The string 'checks'. */
private static final String CHECKS = "checks";
/** The string 'naming'. */
private static final String NAMING = "naming";
/** The string 'src'. */
private static final String SRC = "src";
/** Precompiled regex pattern to remove the "Setter to " prefix from strings. */
private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to ");
/** Class name and their corresponding parent module name. */
private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries(
Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()),
Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()),
Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()),
Map.entry(Filter.class, Checker.class.getSimpleName()),
Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName())
);
/** Set of properties that every check has. */
private static final Set<String> CHECK_PROPERTIES =
getProperties(AbstractCheck.class);
/** Set of properties that every Javadoc check has. */
private static final Set<String> JAVADOC_CHECK_PROPERTIES =
getProperties(AbstractJavadocCheck.class);
/** Set of properties that every FileSet check has. */
private static final Set<String> FILESET_PROPERTIES =
getProperties(AbstractFileSetCheck.class);
/**
* Check and property name.
*/
private static final String HEADER_CHECK_HEADER = "HeaderCheck.header";
/**
* Check and property name.
*/
private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header";
/** Set of properties that are undocumented. Those are internal properties. */
private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
"SuppressWithNearbyCommentFilter.fileContents",
"SuppressionCommentFilter.fileContents"
);
/** Properties that can not be gathered from class instance. */
private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
// static field (all upper case)
"SuppressWarningsHolder.aliasList",
// loads string into memory similar to file
HEADER_CHECK_HEADER,
REGEXP_HEADER_CHECK_HEADER,
// property is an int, but we cut off excess to accommodate old versions
"RedundantModifierCheck.jdkVersion",
// until https://github.com/checkstyle/checkstyle/issues/13376
"CustomImportOrderCheck.customImportOrderRules"
);
/**
* Frequent version.
*/
private static final String VERSION_6_9 = "6.9";
/**
* Frequent version.
*/
private static final String VERSION_5_0 = "5.0";
/**
* Frequent version.
*/
private static final String VERSION_3_2 = "3.2";
/**
* Frequent version.
*/
private static final String VERSION_8_24 = "8.24";
/**
* Frequent version.
*/
private static final String VERSION_8_36 = "8.36";
/**
* Frequent version.
*/
private static final String VERSION_3_0 = "3.0";
/**
* Frequent version.
*/
private static final String VERSION_7_7 = "7.7";
/**
* Frequent version.
*/
private static final String VERSION_5_7 = "5.7";
/**
* Frequent version.
*/
private static final String VERSION_5_1 = "5.1";
/**
* Frequent version.
*/
private static final String VERSION_3_4 = "3.4";
/**
* Map of properties whose since version is different from module version but
* are not specified in code because they are inherited from their super class(es).
* Until <a href="https://github.com/checkstyle/checkstyle/issues/14052">#14052</a>.
*
* @noinspection JavacQuirks
* @noinspectionreason JavacQuirks until #14052
*/
private static final Map<String, String> SINCE_VERSION_FOR_INHERITED_PROPERTY = Map.ofEntries(
Map.entry("MissingDeprecatedCheck.violateExecutionOnNonTightHtml", VERSION_8_24),
Map.entry("NonEmptyAtclauseDescriptionCheck.violateExecutionOnNonTightHtml", "8.3"),
Map.entry("HeaderCheck.charset", VERSION_5_0),
Map.entry("HeaderCheck.fileExtensions", VERSION_6_9),
Map.entry("HeaderCheck.headerFile", VERSION_3_2),
Map.entry(HEADER_CHECK_HEADER, VERSION_5_0),
Map.entry("RegexpHeaderCheck.charset", VERSION_5_0),
Map.entry("RegexpHeaderCheck.fileExtensions", VERSION_6_9),
Map.entry("RegexpHeaderCheck.headerFile", VERSION_3_2),
Map.entry(REGEXP_HEADER_CHECK_HEADER, VERSION_5_0),
Map.entry("ClassDataAbstractionCouplingCheck.excludeClassesRegexps", VERSION_7_7),
Map.entry("ClassDataAbstractionCouplingCheck.excludedClasses", VERSION_5_7),
Map.entry("ClassDataAbstractionCouplingCheck.excludedPackages", VERSION_7_7),
Map.entry("ClassDataAbstractionCouplingCheck.max", VERSION_3_4),
Map.entry("ClassFanOutComplexityCheck.excludeClassesRegexps", VERSION_7_7),
Map.entry("ClassFanOutComplexityCheck.excludedClasses", VERSION_5_7),
Map.entry("ClassFanOutComplexityCheck.excludedPackages", VERSION_7_7),
Map.entry("ClassFanOutComplexityCheck.max", VERSION_3_4),
Map.entry("NonEmptyAtclauseDescriptionCheck.javadocTokens", "7.3"),
Map.entry("FileTabCharacterCheck.fileExtensions", VERSION_5_0),
Map.entry("NewlineAtEndOfFileCheck.fileExtensions", "3.1"),
Map.entry("JavadocPackageCheck.fileExtensions", VERSION_5_0),
Map.entry("OrderedPropertiesCheck.fileExtensions", "8.22"),
Map.entry("UniquePropertiesCheck.fileExtensions", VERSION_5_7),
Map.entry("TranslationCheck.fileExtensions", VERSION_3_0),
Map.entry("LineLengthCheck.fileExtensions", VERSION_8_24),
// until https://github.com/checkstyle/checkstyle/issues/14052
Map.entry("JavadocBlockTagLocationCheck.violateExecutionOnNonTightHtml", VERSION_8_24),
Map.entry("JavadocLeadingAsteriskAlignCheck.violateExecutionOnNonTightHtml", "10.18"),
Map.entry("JavadocMissingLeadingAsteriskCheck.violateExecutionOnNonTightHtml", "8.38"),
Map.entry(
"RequireEmptyLineBeforeBlockTagGroupCheck.violateExecutionOnNonTightHtml",
VERSION_8_36),
Map.entry("ParenPadCheck.option", VERSION_3_0),
Map.entry("TypecastParenPadCheck.option", VERSION_3_2),
Map.entry("FileLengthCheck.fileExtensions", VERSION_5_0),
Map.entry("StaticVariableNameCheck.applyToPackage", VERSION_5_0),
Map.entry("StaticVariableNameCheck.applyToPrivate", VERSION_5_0),
Map.entry("StaticVariableNameCheck.applyToProtected", VERSION_5_0),
Map.entry("StaticVariableNameCheck.applyToPublic", VERSION_5_0),
Map.entry("StaticVariableNameCheck.format", VERSION_3_0),
Map.entry("TypeNameCheck.applyToPackage", VERSION_5_0),
Map.entry("TypeNameCheck.applyToPrivate", VERSION_5_0),
Map.entry("TypeNameCheck.applyToProtected", VERSION_5_0),
Map.entry("TypeNameCheck.applyToPublic", VERSION_5_0),
Map.entry("RegexpMultilineCheck.fileExtensions", VERSION_5_0),
Map.entry("RegexpOnFilenameCheck.fileExtensions", "6.15"),
Map.entry("RegexpSinglelineCheck.fileExtensions", VERSION_5_0),
Map.entry("ClassTypeParameterNameCheck.format", VERSION_5_0),
Map.entry("CatchParameterNameCheck.format", "6.14"),
Map.entry("LambdaParameterNameCheck.format", "8.11"),
Map.entry("IllegalIdentifierNameCheck.format", VERSION_8_36),
Map.entry("ConstantNameCheck.format", VERSION_3_0),
Map.entry("ConstantNameCheck.applyToPackage", VERSION_5_0),
Map.entry("ConstantNameCheck.applyToPrivate", VERSION_5_0),
Map.entry("ConstantNameCheck.applyToProtected", VERSION_5_0),
Map.entry("ConstantNameCheck.applyToPublic", VERSION_5_0),
Map.entry("InterfaceTypeParameterNameCheck.format", "5.8"),
Map.entry("LocalFinalVariableNameCheck.format", VERSION_3_0),
Map.entry("LocalVariableNameCheck.format", VERSION_3_0),
Map.entry("MemberNameCheck.format", VERSION_3_0),
Map.entry("MemberNameCheck.applyToPackage", VERSION_3_4),
Map.entry("MemberNameCheck.applyToPrivate", VERSION_3_4),
Map.entry("MemberNameCheck.applyToProtected", VERSION_3_4),
Map.entry("MemberNameCheck.applyToPublic", VERSION_3_4),
Map.entry("MethodNameCheck.format", VERSION_3_0),
Map.entry("MethodNameCheck.applyToPackage", VERSION_5_1),
Map.entry("MethodNameCheck.applyToPrivate", VERSION_5_1),
Map.entry("MethodNameCheck.applyToProtected", VERSION_5_1),
Map.entry("MethodNameCheck.applyToPublic", VERSION_5_1),
Map.entry("MethodTypeParameterNameCheck.format", VERSION_5_0),
Map.entry("ParameterNameCheck.format", VERSION_3_0),
Map.entry("PatternVariableNameCheck.format", VERSION_8_36),
Map.entry("RecordTypeParameterNameCheck.format", VERSION_8_36),
Map.entry("RecordComponentNameCheck.format", "8.40"),
Map.entry("TypeNameCheck.format", VERSION_3_0)
);
/** Map of all superclasses properties and their javadocs. */
private static final Map<String, DetailNode> SUPER_CLASS_PROPERTIES_JAVADOCS =
new HashMap<>();
/** Path to main source code folder. */
private static final String MAIN_FOLDER_PATH = Paths.get(
SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString();
/** List of files who are superclasses and contain certain properties that checks inherit. */
private static final List<File> MODULE_SUPER_CLASS_FILES = List.of(
new File(Paths.get(MAIN_FOLDER_PATH,
CHECKS, NAMING, "AbstractAccessControlNameCheck.java").toString()),
new File(Paths.get(MAIN_FOLDER_PATH,
CHECKS, NAMING, "AbstractNameCheck.java").toString()),
new File(Paths.get(MAIN_FOLDER_PATH,
CHECKS, "javadoc", "AbstractJavadocCheck.java").toString()),
new File(Paths.get(MAIN_FOLDER_PATH,
"api", "AbstractFileSetCheck.java").toString()),
new File(Paths.get(MAIN_FOLDER_PATH,
CHECKS, "header", "AbstractHeaderCheck.java").toString()),
new File(Paths.get(MAIN_FOLDER_PATH,
CHECKS, "metrics", "AbstractClassCouplingCheck.java").toString()),
new File(Paths.get(MAIN_FOLDER_PATH,
CHECKS, "whitespace", "AbstractParenPadCheck.java").toString())
);
/**
* Private utility constructor.
*/
private SiteUtil() {
}
/**
* Get string values of the message keys from the given check class.
*
* @param module class to examine.
* @return a set of checkstyle's module message keys.
* @throws MacroExecutionException if extraction of message keys fails.
*/
public static Set<String> getMessageKeys(Class<?> module)
throws MacroExecutionException {
final Set<Field> messageKeyFields = getCheckMessageKeys(module);
// We use a TreeSet to sort the message keys alphabetically
final Set<String> messageKeys = new TreeSet<>();
for (Field field : messageKeyFields) {
messageKeys.add(getFieldValue(field, module).toString());
}
return messageKeys;
}
/**
* Gets the check's messages keys.
*
* @param module class to examine.
* @return a set of checkstyle's module message fields.
* @throws MacroExecutionException if the attempt to read a protected class fails.
* @noinspection ChainOfInstanceofChecks
* @noinspectionreason ChainOfInstanceofChecks - We will deal with this at
* <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a>
*
*/
private static Set<Field> getCheckMessageKeys(Class<?> module)
throws MacroExecutionException {
try {
final Set<Field> checkstyleMessages = new HashSet<>();
// get all fields from current class
final Field[] fields = module.getDeclaredFields();
for (Field field : fields) {
if (field.getName().startsWith("MSG_")) {
checkstyleMessages.add(field);
}
}
// deep scan class through hierarchy
final Class<?> superModule = module.getSuperclass();
if (superModule != null) {
checkstyleMessages.addAll(getCheckMessageKeys(superModule));
}
// special cases that require additional classes
if (module == RegexpMultilineCheck.class) {
checkstyleMessages.addAll(getCheckMessageKeys(Class
.forName("com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector")));
}
else if (module == RegexpSinglelineCheck.class
|| module == RegexpSinglelineJavaCheck.class) {
checkstyleMessages.addAll(getCheckMessageKeys(Class
.forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector")));
}
return checkstyleMessages;
}
catch (ClassNotFoundException ex) {
final String message = String.format(Locale.ROOT, "Couldn't find class: %s",
module.getName());
throw new MacroExecutionException(message, ex);
}
}
/**
* Returns the value of the given field.
*
* @param field the field.
* @param instance the instance of the module.
* @return the value of the field.
* @throws MacroExecutionException if the value could not be retrieved.
*/
public static Object getFieldValue(Field field, Object instance)
throws MacroExecutionException {
try {
// required for package/private classes
field.trySetAccessible();
return field.get(instance);
}
catch (IllegalAccessException ex) {
throw new MacroExecutionException("Couldn't get field value", ex);
}
}
/**
* Returns the instance of the module with the given name.
*
* @param moduleName the name of the module.
* @return the instance of the module.
* @throws MacroExecutionException if the module could not be created.
*/
public static Object getModuleInstance(String moduleName) throws MacroExecutionException {
final ModuleFactory factory = getPackageObjectFactory();
try {
return factory.createModule(moduleName);
}
catch (CheckstyleException ex) {
throw new MacroExecutionException("Couldn't find class: " + moduleName, ex);
}
}
/**
* Returns the default PackageObjectFactory with the default package names.
*
* @return the default PackageObjectFactory.
* @throws MacroExecutionException if the PackageObjectFactory cannot be created.
*/
private static PackageObjectFactory getPackageObjectFactory() throws MacroExecutionException {
try {
final ClassLoader cl = ViolationMessagesMacro.class.getClassLoader();
final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
return new PackageObjectFactory(packageNames, cl);
}
catch (CheckstyleException ex) {
throw new MacroExecutionException("Couldn't load checkstyle modules", ex);
}
}
/**
* Construct a string with a leading newline character and followed by
* the given amount of spaces. We use this method only to match indentation in
* regular xdocs and have minimal diff when parsing the templates.
* This method exists until
* <a href="https://github.com/checkstyle/checkstyle/issues/13426">13426</a>
*
* @param amountOfSpaces the amount of spaces to add after the newline.
* @return the constructed string.
*/
public static String getNewlineAndIndentSpaces(int amountOfSpaces) {
return System.lineSeparator() + " ".repeat(amountOfSpaces);
}
/**
* Returns path to the template for the given module name or throws an exception if the
* template cannot be found.
*
* @param moduleName the module whose template we are looking for.
* @return path to the template.
* @throws MacroExecutionException if the template cannot be found.
*/
public static Path getTemplatePath(String moduleName) throws MacroExecutionException {
final String fileNamePattern = ".*[\\\\/]"
+ moduleName.toLowerCase(Locale.ROOT) + "\\..*";
return getXdocsTemplatesFilePaths()
.stream()
.filter(path -> path.toString().matches(fileNamePattern))
.findFirst()
.orElse(null);
}
/**
* Gets xdocs template file paths. These are files ending with .xml.template.
* This method will be changed to gather .xml once
* <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
*
* @return a set of xdocs template file paths.
* @throws MacroExecutionException if an I/O error occurs.
*/
public static Set<Path> getXdocsTemplatesFilePaths() throws MacroExecutionException {
final Path directory = Paths.get("src/xdocs");
try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
(path, attr) -> {
return attr.isRegularFile()
&& path.toString().endsWith(".xml.template");
})) {
return stream.collect(Collectors.toUnmodifiableSet());
}
catch (IOException ioException) {
throw new MacroExecutionException("Failed to find xdocs templates", ioException);
}
}
/**
* Returns the parent module name for the given module class. Returns either
* "TreeWalker" or "Checker". Returns null if the module class is null.
*
* @param moduleClass the module class.
* @return the parent module name as a string.
* @throws MacroExecutionException if the parent module cannot be found.
*/
public static String getParentModule(Class<?> moduleClass)
throws MacroExecutionException {
String parentModuleName = "";
Class<?> parentClass = moduleClass.getSuperclass();
while (parentClass != null) {
parentModuleName = CLASS_TO_PARENT_MODULE.get(parentClass);
if (parentModuleName != null) {
break;
}
parentClass = parentClass.getSuperclass();
}
// If parent class is not found, check interfaces
if (parentModuleName == null || parentModuleName.isEmpty()) {
final Class<?>[] interfaces = moduleClass.getInterfaces();
for (Class<?> interfaceClass : interfaces) {
parentModuleName = CLASS_TO_PARENT_MODULE.get(interfaceClass);
if (parentModuleName != null) {
break;
}
}
}
if (parentModuleName == null || parentModuleName.isEmpty()) {
final String message = String.format(Locale.ROOT,
"Failed to find parent module for %s", moduleClass.getSimpleName());
throw new MacroExecutionException(message);
}
return parentModuleName;
}
/**
* Get a set of properties for the given class that should be documented.
*
* @param clss the class to get the properties for.
* @param instance the instance of the module.
* @return a set of properties for the given class.
*/
public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) {
final Set<String> properties =
getProperties(clss).stream()
.filter(prop -> {
return !isGlobalProperty(clss, prop) && !isUndocumentedProperty(clss, prop);
})
.collect(Collectors.toCollection(HashSet::new));
properties.addAll(getNonExplicitProperties(instance, clss));
return new TreeSet<>(properties);
}
/**
* Get the javadocs of the properties of the module. If the property is not present in the
* module, then the javadoc of the property from the superclass(es) is used.
*
* @param properties the properties of the module.
* @param moduleName the name of the module.
* @param moduleFile the module file.
* @return the javadocs of the properties of the module.
* @throws MacroExecutionException if an error occurs during processing.
*/
public static Map<String, DetailNode> getPropertiesJavadocs(Set<String> properties,
String moduleName, File moduleFile)
throws MacroExecutionException {
// lazy initialization
if (SUPER_CLASS_PROPERTIES_JAVADOCS.isEmpty()) {
processSuperclasses();
}
processModule(moduleName, moduleFile);
final Map<String, DetailNode> unmodifiableJavadocs =
ClassAndPropertiesSettersJavadocScraper.getJavadocsForModuleOrProperty();
final Map<String, DetailNode> javadocs = new LinkedHashMap<>(unmodifiableJavadocs);
properties.forEach(property -> {
final DetailNode superClassPropertyJavadoc =
SUPER_CLASS_PROPERTIES_JAVADOCS.get(property);
if (superClassPropertyJavadoc != null) {
javadocs.putIfAbsent(property, superClassPropertyJavadoc);
}
});
assertAllPropertySetterJavadocsAreFound(properties, moduleName, javadocs);
return javadocs;
}
/**
* Assert that each property has a corresponding setter javadoc that is not null.
* 'tokens' and 'javadocTokens' are excluded from this check, because their
* description is different from the description of the setter.
*
* @param properties the properties of the module.
* @param moduleName the name of the module.
* @param javadocs the javadocs of the properties of the module.
* @throws MacroExecutionException if an error occurs during processing.
*/
private static void assertAllPropertySetterJavadocsAreFound(
Set<String> properties, String moduleName, Map<String, DetailNode> javadocs)
throws MacroExecutionException {
for (String property : properties) {
final boolean isPropertySetterJavadocFound = javadocs.containsKey(property)
|| TOKENS.equals(property) || JAVADOC_TOKENS.equals(property);
if (!isPropertySetterJavadocFound) {
final String message = String.format(Locale.ROOT,
"%s: Failed to find setter javadoc for property '%s'",
moduleName, property);
throw new MacroExecutionException(message);
}
}
}
/**
* Collect the properties setters javadocs of the superclasses.
*
* @throws MacroExecutionException if an error occurs during processing.
*/
private static void processSuperclasses() throws MacroExecutionException {
for (File superclassFile : MODULE_SUPER_CLASS_FILES) {
final String superclassName = CommonUtil
.getFileNameWithoutExtension(superclassFile.getName());
processModule(superclassName, superclassFile);
final Map<String, DetailNode> superclassJavadocs =
ClassAndPropertiesSettersJavadocScraper.getJavadocsForModuleOrProperty();
SUPER_CLASS_PROPERTIES_JAVADOCS.putAll(superclassJavadocs);
}
}
/**
* Scrape the Javadocs of the class and its properties setters with
* ClassAndPropertiesSettersJavadocScraper.
*
* @param moduleName the name of the module.
* @param moduleFile the module file.
* @throws MacroExecutionException if an error occurs during processing.
*/
private static void processModule(String moduleName, File moduleFile)
throws MacroExecutionException {
if (!moduleFile.isFile()) {
final String message = String.format(Locale.ROOT,
"File %s is not a file. Please check the 'modulePath' property.", moduleFile);
throw new MacroExecutionException(message);
}
ClassAndPropertiesSettersJavadocScraper.initialize(moduleName);
final Checker checker = new Checker();
checker.setModuleClassLoader(Checker.class.getClassLoader());
final DefaultConfiguration scraperCheckConfig =
new DefaultConfiguration(
ClassAndPropertiesSettersJavadocScraper.class.getName());
final DefaultConfiguration defaultConfiguration =
new DefaultConfiguration("configuration");
final DefaultConfiguration treeWalkerConfig =
new DefaultConfiguration(TreeWalker.class.getName());
defaultConfiguration.addProperty(CHARSET, StandardCharsets.UTF_8.name());
defaultConfiguration.addChild(treeWalkerConfig);
treeWalkerConfig.addChild(scraperCheckConfig);
try {
checker.configure(defaultConfiguration);
final List<File> filesToProcess = List.of(moduleFile);
checker.process(filesToProcess);
checker.destroy();
}
catch (CheckstyleException checkstyleException) {
final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName);
throw new MacroExecutionException(message, checkstyleException);
}
}
/**
* Get a set of properties for the given class.
*
* @param clss the class to get the properties for.
* @return a set of properties for the given class.
*/
public static Set<String> getProperties(Class<?> clss) {
final Set<String> result = new TreeSet<>();
final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss);
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
if (propertyDescriptor.getWriteMethod() != null) {
result.add(propertyDescriptor.getName());
}
}
return result;
}
/**
* Checks if the property is a global property. Global properties come from the base classes
* and are common to all checks. For example id, severity, tabWidth, etc.
*
* @param clss the class of the module.
* @param propertyName the name of the property.
* @return true if the property is a global property.
*/
private static boolean isGlobalProperty(Class<?> clss, String propertyName) {
return AbstractCheck.class.isAssignableFrom(clss)
&& CHECK_PROPERTIES.contains(propertyName)
|| AbstractJavadocCheck.class.isAssignableFrom(clss)
&& JAVADOC_CHECK_PROPERTIES.contains(propertyName)
|| AbstractFileSetCheck.class.isAssignableFrom(clss)
&& FILESET_PROPERTIES.contains(propertyName);
}
/**
* Checks if the property is supposed to be documented.
*
* @param clss the class of the module.
* @param propertyName the name of the property.
* @return true if the property is supposed to be documented.
*/
private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) {
return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName);
}
/**
* Gets properties that are not explicitly captured but should be documented if
* certain conditions are met.
*
* @param instance the instance of the module.
* @param clss the class of the module.
* @return the non explicit properties.
*/
private static Set<String> getNonExplicitProperties(
Object instance, Class<?> clss) {
final Set<String> result = new TreeSet<>();
if (AbstractCheck.class.isAssignableFrom(clss)) {
final AbstractCheck check = (AbstractCheck) instance;
final int[] acceptableTokens = check.getAcceptableTokens();
Arrays.sort(acceptableTokens);
final int[] defaultTokens = check.getDefaultTokens();
Arrays.sort(defaultTokens);
final int[] requiredTokens = check.getRequiredTokens();
Arrays.sort(requiredTokens);
if (!Arrays.equals(acceptableTokens, defaultTokens)
|| !Arrays.equals(acceptableTokens, requiredTokens)) {
result.add(TOKENS);
}
}
if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
result.add("violateExecutionOnNonTightHtml");
final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
Arrays.sort(acceptableJavadocTokens);
final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
Arrays.sort(defaultJavadocTokens);
final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
Arrays.sort(requiredJavadocTokens);
if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
|| !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
result.add(JAVADOC_TOKENS);
}
}
if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
result.add(FILE_EXTENSIONS);
}
return result;
}
/**
* Get the description of the property.
*
* @param propertyName the name of the property.
* @param javadoc the Javadoc of the property setter method.
* @param moduleName the name of the module.
* @return the description of the property.
* @throws MacroExecutionException if the description could not be extracted.
*/
public static String getPropertyDescription(
String propertyName, DetailNode javadoc, String moduleName)
throws MacroExecutionException {
final String description;
if (TOKENS.equals(propertyName)) {
description = "tokens to check";
}
else if (JAVADOC_TOKENS.equals(propertyName)) {
description = "javadoc tokens to check";
}
else {
final String descriptionString = SETTER_PATTERN.matcher(
DescriptionExtractor.getDescriptionFromJavadoc(javadoc, moduleName))
.replaceFirst("");
final String firstLetterCapitalized = descriptionString.substring(0, 1)
.toUpperCase(Locale.ROOT);
description = firstLetterCapitalized + descriptionString.substring(1);
}
return description;
}
/**
* Get the since version of the property.
*
* @param moduleName the name of the module.
* @param moduleJavadoc the Javadoc of the module.
* @param propertyName the name of the property.
* @param propertyJavadoc the Javadoc of the property setter method.
* @return the since version of the property.
* @throws MacroExecutionException if the since version could not be extracted.
*/
public static String getSinceVersion(String moduleName, DetailNode moduleJavadoc,
String propertyName, DetailNode propertyJavadoc)
throws MacroExecutionException {
final String sinceVersion;
final String superClassSinceVersion = SINCE_VERSION_FOR_INHERITED_PROPERTY
.get(moduleName + DOT + propertyName);
if (superClassSinceVersion != null) {
sinceVersion = superClassSinceVersion;
}
else if (TOKENS.equals(propertyName)
|| JAVADOC_TOKENS.equals(propertyName)) {
// Use module's since version for inherited properties
sinceVersion = getSinceVersionFromJavadoc(moduleJavadoc);
}
else {
sinceVersion = getSinceVersionFromJavadoc(propertyJavadoc);
}
if (sinceVersion == null) {
final String message = String.format(Locale.ROOT,
"Failed to find '@since' version for '%s' property"
+ " in '%s' and all parent classes.", propertyName, moduleName);
throw new MacroExecutionException(message);
}
return sinceVersion;
}
/**
* Extract the since version from the Javadoc.
*
* @param javadoc the Javadoc to extract the since version from.
* @return the since version of the setter.
*/
@Nullable
private static String getSinceVersionFromJavadoc(DetailNode javadoc) {
final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc);
return Optional.ofNullable(sinceJavadocTag)
.map(tag -> JavadocUtil.findFirstToken(tag, JavadocTokenTypes.DESCRIPTION))
.map(description -> JavadocUtil.findFirstToken(description, JavadocTokenTypes.TEXT))
.map(DetailNode::getText)
.orElse(null);
}
/**
* Find the since Javadoc tag node in the given Javadoc.
*
* @param javadoc the Javadoc to search.
* @return the since Javadoc tag node or null if not found.
*/
private static DetailNode getSinceJavadocTag(DetailNode javadoc) {
final DetailNode[] children = javadoc.getChildren();
DetailNode javadocTagWithSince = null;
for (final DetailNode child : children) {
if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) {
final DetailNode sinceNode = JavadocUtil.findFirstToken(
child, JavadocTokenTypes.SINCE_LITERAL);
if (sinceNode != null) {
javadocTagWithSince = child;
break;
}
}
}
return javadocTagWithSince;
}
/**
* Get the type of the property.
*
* @param field the field to get the type of.
* @param propertyName the name of the property.
* @param moduleName the name of the module.
* @param instance the instance of the module.
* @return the type of the property.
* @throws MacroExecutionException if an error occurs during getting the type.
*/
public static String getType(Field field, String propertyName,
String moduleName, Object instance)
throws MacroExecutionException {
final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance);
return Optional.ofNullable(field)
.map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
.map(propertyType -> propertyType.value().getDescription())
.orElseGet(fieldClass::getSimpleName);
}
/**
* Get the default value of the property.
*
* @param propertyName the name of the property.
* @param field the field to get the default value of.
* @param classInstance the instance of the class to get the default value of.
* @param moduleName the name of the module.
* @return the default value of the property.
* @throws MacroExecutionException if an error occurs during getting the default value.
* @noinspection IfStatementWithTooManyBranches
* @noinspectionreason IfStatementWithTooManyBranches - complex nature of getting properties
* from XML files requires giant if/else statement
*/
// -@cs[CyclomaticComplexity] Splitting would not make the code more readable
public static String getDefaultValue(String propertyName, Field field,
Object classInstance, String moduleName)
throws MacroExecutionException {
final Object value = getFieldValue(field, classInstance);
final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, classInstance);
String result = null;
if (CHARSET.equals(propertyName)) {
result = "the charset property of the parent"
+ " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module";
}
else if (classInstance instanceof PropertyCacheFile) {
result = "null (no cache file)";
}
else if (fieldClass == boolean.class) {
result = value.toString();
}
else if (fieldClass == int.class) {
result = value.toString();
}
else if (fieldClass == int[].class) {
result = getIntArrayPropertyValue(value);
}
else if (fieldClass == double[].class) {
result = removeSquareBrackets(Arrays.toString((double[]) value).replace(".0", ""));
if (result.isEmpty()) {
result = CURLY_BRACKETS;
}
}
else if (fieldClass == String[].class) {
result = getStringArrayPropertyValue(propertyName, value);
}
else if (fieldClass == URI.class || fieldClass == String.class) {
if (value != null) {
result = '"' + value.toString() + '"';
}
}
else if (fieldClass == Pattern.class) {
if (value != null) {
result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t")
.replace("\r", "\\r").replace("\f", "\\f") + '"';
}
}
else if (fieldClass == Pattern[].class) {
result = getPatternArrayPropertyValue(value);
}
else if (fieldClass.isEnum()) {
if (value != null) {
result = value.toString().toLowerCase(Locale.ENGLISH);
}
}
else if (fieldClass == AccessModifierOption[].class) {
result = removeSquareBrackets(Arrays.toString((Object[]) value));
}
else {
final String message = String.format(Locale.ROOT,
"Unknown property type: %s", fieldClass.getSimpleName());
throw new MacroExecutionException(message);
}
if (result == null) {
result = "null";
}
return result;
}
/**
* Gets the name of the bean property's default value for the Pattern array class.
*
* @param fieldValue The bean property's value
* @return String form of property's default value
*/
private static String getPatternArrayPropertyValue(Object fieldValue) {
Object value = fieldValue;
if (value instanceof Collection) {
final Collection<?> collection = (Collection<?>) value;
value = collection.stream()
.map(Pattern.class::cast)
.toArray(Pattern[]::new);
}
String result = "";
if (value != null && Array.getLength(value) > 0) {
result = removeSquareBrackets(
Arrays.stream((Pattern[]) value)
.map(Pattern::pattern)
.collect(Collectors.joining(COMMA_SPACE)));
}
if (result.isEmpty()) {
result = CURLY_BRACKETS;
}
return result;
}
/**
* Removes square brackets [ and ] from the given string.
*
* @param value the string to remove square brackets from.
* @return the string without square brackets.
*/
private static String removeSquareBrackets(String value) {
return value
.replace("[", "")
.replace("]", "");
}
/**
* Gets the name of the bean property's default value for the string array class.
*
* @param propertyName The bean property's name
* @param value The bean property's value
* @return String form of property's default value
*/
private static String getStringArrayPropertyValue(String propertyName, Object value) {
String result;
if (value == null) {
result = "";
}
else {
try (Stream<?> valuesStream = getValuesStream(value)) {
result = valuesStream
.map(String.class::cast)
.sorted()
.collect(Collectors.joining(COMMA_SPACE));
}
}
if (result.isEmpty()) {
if (FILE_EXTENSIONS.equals(propertyName)) {
result = "all files";
}
else {
result = CURLY_BRACKETS;
}
}
return result;
}
/**
* Generates a stream of values from the given value.
*
* @param value the value to generate the stream from.
* @return the stream of values.
*/
private static Stream<?> getValuesStream(Object value) {
final Stream<?> valuesStream;
if (value instanceof Collection) {
final Collection<?> collection = (Collection<?>) value;
valuesStream = collection.stream();
}
else {
final Object[] array = (Object[]) value;
valuesStream = Arrays.stream(array);
}
return valuesStream;
}
/**
* Returns the name of the bean property's default value for the int array class.
*
* @param value The bean property's value.
* @return String form of property's default value.
*/
private static String getIntArrayPropertyValue(Object value) {
try (IntStream stream = getIntStream(value)) {
String result = stream
.mapToObj(TokenUtil::getTokenName)
.sorted()
.collect(Collectors.joining(COMMA_SPACE));
if (result.isEmpty()) {
result = CURLY_BRACKETS;
}
return result;
}
}
/**
* Get the int stream from the given value.
*
* @param value the value to get the int stream from.
* @return the int stream.
*/
private static IntStream getIntStream(Object value) {
final IntStream stream;
if (value instanceof Collection) {
final Collection<?> collection = (Collection<?>) value;
stream = collection.stream()
.mapToInt(int.class::cast);
}
else if (value instanceof BitSet) {
stream = ((BitSet) value).stream();
}
else {
stream = Arrays.stream((int[]) value);
}
return stream;
}
/**
* Gets the class of the given field.
*
* @param field the field to get the class of.
* @param propertyName the name of the property.
* @param moduleName the name of the module.
* @param instance the instance of the module.
* @return the class of the field.
* @throws MacroExecutionException if an error occurs during getting the class.
*/
// -@cs[CyclomaticComplexity] Splitting would not make the code more readable
private static Class<?> getFieldClass(Field field, String propertyName,
String moduleName, Object instance)
throws MacroExecutionException {
Class<?> result = null;
if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
.contains(moduleName + DOT + propertyName)) {
result = getPropertyClass(propertyName, instance);
}
if (field != null && result == null) {
result = field.getType();
}
if (result == null) {
throw new MacroExecutionException(
"Could not find field " + propertyName + " in class " + moduleName);
}
if (field != null && (result == List.class || result == Set.class)) {
final ParameterizedType type = (ParameterizedType) field.getGenericType();
final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
if (parameterClass == Integer.class) {
result = int[].class;
}
else if (parameterClass == String.class) {
result = String[].class;
}
else if (parameterClass == Pattern.class) {
result = Pattern[].class;
}
else {
final String message = "Unknown parameterized type: "
+ parameterClass.getSimpleName();
throw new MacroExecutionException(message);
}
}
else if (result == BitSet.class) {
result = int[].class;
}
return result;
}
/**
* Gets the class of the given java property.
*
* @param propertyName the name of the property.
* @param instance the instance of the module.
* @return the class of the java property.
* @throws MacroExecutionException if an error occurs during getting the class.
*/
// -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field
public static Class<?> getPropertyClass(String propertyName, Object instance)
throws MacroExecutionException {
final Class<?> result;
try {
final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
propertyName);
result = descriptor.getPropertyType();
}
catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
throw new MacroExecutionException(exc.getMessage(), exc);
}
return result;
}
/**
* Get the difference between two lists of tokens.
*
* @param tokens the list of tokens to remove from.
* @param subtractions the tokens to remove.
* @return the difference between the two lists.
*/
public static List<Integer> getDifference(int[] tokens, int... subtractions) {
final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
.boxed()
.collect(Collectors.toUnmodifiableSet());
return Arrays.stream(tokens)
.boxed()
.filter(token -> !subtractionsSet.contains(token))
.collect(Collectors.toUnmodifiableList());
}
/**
* Gets the field with the given name from the given class.
*
* @param fieldClass the class to get the field from.
* @param propertyName the name of the field.
* @return the field we are looking for.
*/
public static Field getField(Class<?> fieldClass, String propertyName) {
Field result = null;
Class<?> currentClass = fieldClass;
while (!Object.class.equals(currentClass)) {
try {
result = currentClass.getDeclaredField(propertyName);
result.trySetAccessible();
break;
}
catch (NoSuchFieldException ignored) {
currentClass = currentClass.getSuperclass();
}
}
return result;
}
/**
* Constructs string with relative link to the provided document.
*
* @param moduleName the name of the module.
* @param document the path of the document.
* @return relative link to the document.
* @throws MacroExecutionException if link to the document cannot be constructed.
*/
public static String getLinkToDocument(String moduleName, String document)
throws MacroExecutionException {
final Path templatePath = getTemplatePath(moduleName.replace("Check", ""));
if (templatePath == null) {
throw new MacroExecutionException(
String.format(Locale.ROOT,
"Could not find template for %s", moduleName));
}
final Path templatePathParent = templatePath.getParent();
if (templatePathParent == null) {
throw new MacroExecutionException("Failed to get parent path for " + templatePath);
}
return templatePathParent
.relativize(Paths.get(SRC, "xdocs", document))
.toString()
.replace(".xml", ".html")
.replace('\\', '/');
}
/** Utility class for extracting description from a method's Javadoc. */
private static final class DescriptionExtractor {
/**
* Extracts the description from the javadoc detail node. Performs a DFS traversal on the
* detail node and extracts the text nodes.
*
* @param javadoc the Javadoc to extract the description from.
* @param moduleName the name of the module.
* @return the description of the setter.
* @throws MacroExecutionException if the description could not be extracted.
* @noinspection TooBroadScope
* @noinspectionreason TooBroadScope - complex nature of method requires large scope
*/
// -@cs[NPathComplexity] Splitting would not make the code more readable
// -@cs[CyclomaticComplexity] Splitting would not make the code more readable.
private static String getDescriptionFromJavadoc(DetailNode javadoc, String moduleName)
throws MacroExecutionException {
boolean isInCodeLiteral = false;
boolean isInHtmlElement = false;
boolean isInHrefAttribute = false;
final StringBuilder description = new StringBuilder(128);
final Deque<DetailNode> queue = new ArrayDeque<>();
final List<DetailNode> descriptionNodes = getDescriptionNodes(javadoc);
Lists.reverse(descriptionNodes).forEach(queue::push);
// Perform DFS traversal on description nodes
while (!queue.isEmpty()) {
final DetailNode node = queue.pop();
Lists.reverse(Arrays.asList(node.getChildren())).forEach(queue::push);
if (node.getType() == JavadocTokenTypes.HTML_TAG_NAME
&& "href".equals(node.getText())) {
isInHrefAttribute = true;
}
if (isInHrefAttribute && node.getType() == JavadocTokenTypes.ATTR_VALUE) {
final String href = node.getText();
if (href.contains(CHECKSTYLE_ORG_URL)) {
handleInternalLink(description, moduleName, href);
}
else {
description.append(href);
}
isInHrefAttribute = false;
continue;
}
if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
isInHtmlElement = true;
}
if (node.getType() == JavadocTokenTypes.END
&& node.getParent().getType() == JavadocTokenTypes.HTML_ELEMENT_END) {
description.append(node.getText());
isInHtmlElement = false;
}
if (node.getType() == JavadocTokenTypes.TEXT
// If a node has children, its text is not part of the description
|| isInHtmlElement && node.getChildren().length == 0
// Some HTML elements span multiple lines, so we avoid the asterisk
&& node.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
description.append(node.getText());
}
if (node.getType() == JavadocTokenTypes.CODE_LITERAL) {
isInCodeLiteral = true;
description.append("<code>");
}
if (isInCodeLiteral
&& node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
isInCodeLiteral = false;
description.append("</code>");
}
}
return description.toString().trim();
}
/**
* Converts the href value to a relative link to the document and appends it to the
* description.
*
* @param description the description to append the relative link to.
* @param moduleName the name of the module.
* @param value the href value.
* @throws MacroExecutionException if the relative link could not be created.
*/
private static void handleInternalLink(StringBuilder description,
String moduleName, String value)
throws MacroExecutionException {
String href = value;
href = href.replace(CHECKSTYLE_ORG_URL, "");
// Remove first and last characters, they are always double quotes
href = href.substring(1, href.length() - 1);
final String relativeHref = getLinkToDocument(moduleName, href);
final char doubleQuote = '\"';
description.append(doubleQuote).append(relativeHref).append(doubleQuote);
}
/**
* Extracts description nodes from javadoc.
*
* @param javadoc the Javadoc to extract the description from.
* @return the description nodes of the setter.
*/
private static List<DetailNode> getDescriptionNodes(DetailNode javadoc) {
final DetailNode[] children = javadoc.getChildren();
final List<DetailNode> descriptionNodes = new ArrayList<>();
for (final DetailNode child : children) {
if (isEndOfDescription(child)) {
break;
}
descriptionNodes.add(child);
}
return descriptionNodes;
}
/**
* Determines if the given child index is the end of the description. The end of the
* description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK, NEWLINE,
* LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the one below
* this line.
*
* @param child the child to check.
* @return true if the given child index is the end of the description.
*/
private static boolean isEndOfDescription(DetailNode child) {
final DetailNode nextSibling = JavadocUtil.getNextSibling(child);
final DetailNode secondNextSibling = JavadocUtil.getNextSibling(nextSibling);
final DetailNode thirdNextSibling = JavadocUtil.getNextSibling(secondNextSibling);
return child.getType() == JavadocTokenTypes.NEWLINE
&& nextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK
&& secondNextSibling.getType() == JavadocTokenTypes.NEWLINE
&& thirdNextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK;
}
}
}