001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.module.ModuleDescriptor.Version; 026import java.lang.reflect.Array; 027import java.lang.reflect.Field; 028import java.lang.reflect.InvocationTargetException; 029import java.lang.reflect.ParameterizedType; 030import java.net.URI; 031import java.nio.charset.StandardCharsets; 032import java.nio.file.Files; 033import java.nio.file.Path; 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.Objects; 047import java.util.Optional; 048import java.util.Set; 049import java.util.TreeSet; 050import java.util.regex.Pattern; 051import java.util.stream.Collectors; 052import java.util.stream.IntStream; 053import java.util.stream.Stream; 054 055import javax.annotation.Nullable; 056 057import org.apache.commons.beanutils.PropertyUtils; 058import org.apache.maven.doxia.macro.MacroExecutionException; 059 060import com.google.common.collect.Lists; 061import com.puppycrawl.tools.checkstyle.Checker; 062import com.puppycrawl.tools.checkstyle.DefaultConfiguration; 063import com.puppycrawl.tools.checkstyle.ModuleFactory; 064import com.puppycrawl.tools.checkstyle.PackageNamesLoader; 065import com.puppycrawl.tools.checkstyle.PackageObjectFactory; 066import com.puppycrawl.tools.checkstyle.PropertyCacheFile; 067import com.puppycrawl.tools.checkstyle.PropertyType; 068import com.puppycrawl.tools.checkstyle.TreeWalker; 069import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 070import com.puppycrawl.tools.checkstyle.XdocsPropertyType; 071import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 072import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 073import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter; 074import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 075import com.puppycrawl.tools.checkstyle.api.DetailNode; 076import com.puppycrawl.tools.checkstyle.api.Filter; 077import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 078import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck; 079import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 080import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck; 081import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck; 082import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck; 083import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraper; 084import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 085import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 086import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 087 088/** 089 * Utility class for site generation. 090 */ 091public final class SiteUtil { 092 093 /** The string 'tokens'. */ 094 public static final String TOKENS = "tokens"; 095 /** The string 'javadocTokens'. */ 096 public static final String JAVADOC_TOKENS = "javadocTokens"; 097 /** The string '.'. */ 098 public static final String DOT = "."; 099 /** The string ','. */ 100 public static final String COMMA = ","; 101 /** The whitespace. */ 102 public static final String WHITESPACE = " "; 103 /** The string ', '. */ 104 public static final String COMMA_SPACE = COMMA + WHITESPACE; 105 /** The string 'TokenTypes'. */ 106 public static final String TOKEN_TYPES = "TokenTypes"; 107 /** The path to the TokenTypes.html file. */ 108 public static final String PATH_TO_TOKEN_TYPES = 109 "apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html"; 110 /** The path to the JavadocTokenTypes.html file. */ 111 public static final String PATH_TO_JAVADOC_TOKEN_TYPES = 112 "apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html"; 113 /** The string of JavaDoc module marking 'Since version'. */ 114 public static final String SINCE_VERSION = "Since version"; 115 /** The 'Check' pattern at the end of string. */ 116 public static final Pattern FINAL_CHECK = Pattern.compile("Check$"); 117 /** The string 'fileExtensions'. */ 118 public static final String FILE_EXTENSIONS = "fileExtensions"; 119 /** The string 'charset'. */ 120 public static final String CHARSET = "charset"; 121 /** The url of the checkstyle website. */ 122 private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/"; 123 /** The string 'checks'. */ 124 private static final String CHECKS = "checks"; 125 /** The string 'naming'. */ 126 private static final String NAMING = "naming"; 127 /** The string 'src'. */ 128 private static final String SRC = "src"; 129 /** Template file extension. */ 130 private static final String TEMPLATE_FILE_EXTENSION = ".xml.template"; 131 132 /** Precompiled regex pattern to remove the "Setter to " prefix from strings. */ 133 private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to "); 134 135 /** Class name and their corresponding parent module name. */ 136 private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries( 137 Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()), 138 Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()), 139 Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()), 140 Map.entry(Filter.class, Checker.class.getSimpleName()), 141 Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName()) 142 ); 143 144 /** Set of properties that every check has. */ 145 private static final Set<String> CHECK_PROPERTIES = 146 getProperties(AbstractCheck.class); 147 148 /** Set of properties that every Javadoc check has. */ 149 private static final Set<String> JAVADOC_CHECK_PROPERTIES = 150 getProperties(AbstractJavadocCheck.class); 151 152 /** Set of properties that every FileSet check has. */ 153 private static final Set<String> FILESET_PROPERTIES = 154 getProperties(AbstractFileSetCheck.class); 155 156 /** 157 * Check and property name. 158 */ 159 private static final String HEADER_CHECK_HEADER = "HeaderCheck.header"; 160 161 /** 162 * Check and property name. 163 */ 164 private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header"; 165 166 /** Set of properties that are undocumented. Those are internal properties. */ 167 private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of( 168 "SuppressWithNearbyCommentFilter.fileContents", 169 "SuppressionCommentFilter.fileContents" 170 ); 171 172 /** Properties that can not be gathered from class instance. */ 173 private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of( 174 // static field (all upper case) 175 "SuppressWarningsHolder.aliasList", 176 // loads string into memory similar to file 177 HEADER_CHECK_HEADER, 178 REGEXP_HEADER_CHECK_HEADER, 179 // property is an int, but we cut off excess to accommodate old versions 180 "RedundantModifierCheck.jdkVersion", 181 // until https://github.com/checkstyle/checkstyle/issues/13376 182 "CustomImportOrderCheck.customImportOrderRules" 183 ); 184 185 /** Map of all superclasses properties and their javadocs. */ 186 private static final Map<String, DetailNode> SUPER_CLASS_PROPERTIES_JAVADOCS = 187 new HashMap<>(); 188 189 /** Path to main source code folder. */ 190 private static final String MAIN_FOLDER_PATH = Path.of( 191 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString(); 192 193 /** List of files who are superclasses and contain certain properties that checks inherit. */ 194 private static final List<Path> MODULE_SUPER_CLASS_PATHS = List.of( 195 Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractAccessControlNameCheck.java"), 196 Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractNameCheck.java"), 197 Path.of(MAIN_FOLDER_PATH, CHECKS, "javadoc", "AbstractJavadocCheck.java"), 198 Path.of(MAIN_FOLDER_PATH, "api", "AbstractFileSetCheck.java"), 199 Path.of(MAIN_FOLDER_PATH, CHECKS, "header", "AbstractHeaderCheck.java"), 200 Path.of(MAIN_FOLDER_PATH, CHECKS, "metrics", "AbstractClassCouplingCheck.java"), 201 Path.of(MAIN_FOLDER_PATH, CHECKS, "whitespace", "AbstractParenPadCheck.java") 202 ); 203 204 /** 205 * Private utility constructor. 206 */ 207 private SiteUtil() { 208 } 209 210 /** 211 * Get string values of the message keys from the given check class. 212 * 213 * @param module class to examine. 214 * @return a set of checkstyle's module message keys. 215 * @throws MacroExecutionException if extraction of message keys fails. 216 */ 217 public static Set<String> getMessageKeys(Class<?> module) 218 throws MacroExecutionException { 219 final Set<Field> messageKeyFields = getCheckMessageKeysFields(module); 220 // We use a TreeSet to sort the message keys alphabetically 221 final Set<String> messageKeys = new TreeSet<>(); 222 for (Field field : messageKeyFields) { 223 messageKeys.add(getFieldValue(field, module).toString()); 224 } 225 return messageKeys; 226 } 227 228 /** 229 * Gets the check's messages keys. 230 * 231 * @param module class to examine. 232 * @return a set of checkstyle's module message fields. 233 * @throws MacroExecutionException if the attempt to read a protected class fails. 234 * @noinspection ChainOfInstanceofChecks 235 * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at 236 * <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a> 237 * 238 */ 239 private static Set<Field> getCheckMessageKeysFields(Class<?> module) 240 throws MacroExecutionException { 241 try { 242 final Set<Field> checkstyleMessages = new HashSet<>(); 243 244 // get all fields from current class 245 final Field[] fields = module.getDeclaredFields(); 246 247 for (Field field : fields) { 248 if (field.getName().startsWith("MSG_")) { 249 checkstyleMessages.add(field); 250 } 251 } 252 253 // deep scan class through hierarchy 254 final Class<?> superModule = module.getSuperclass(); 255 256 if (superModule != null) { 257 checkstyleMessages.addAll(getCheckMessageKeysFields(superModule)); 258 } 259 260 // special cases that require additional classes 261 if (module == RegexpMultilineCheck.class) { 262 checkstyleMessages.addAll(getCheckMessageKeysFields(Class 263 .forName("com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector"))); 264 } 265 else if (module == RegexpSinglelineCheck.class 266 || module == RegexpSinglelineJavaCheck.class) { 267 checkstyleMessages.addAll(getCheckMessageKeysFields(Class 268 .forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector"))); 269 } 270 271 return checkstyleMessages; 272 } 273 catch (ClassNotFoundException exc) { 274 final String message = String.format(Locale.ROOT, "Couldn't find class: %s", 275 module.getName()); 276 throw new MacroExecutionException(message, exc); 277 } 278 } 279 280 /** 281 * Returns the value of the given field. 282 * 283 * @param field the field. 284 * @param instance the instance of the module. 285 * @return the value of the field. 286 * @throws MacroExecutionException if the value could not be retrieved. 287 */ 288 public static Object getFieldValue(Field field, Object instance) 289 throws MacroExecutionException { 290 try { 291 Object fieldValue = null; 292 293 if (field != null) { 294 // required for package/private classes 295 field.trySetAccessible(); 296 fieldValue = field.get(instance); 297 } 298 299 return fieldValue; 300 } 301 catch (IllegalAccessException exc) { 302 throw new MacroExecutionException("Couldn't get field value", exc); 303 } 304 } 305 306 /** 307 * Returns the instance of the module with the given name. 308 * 309 * @param moduleName the name of the module. 310 * @return the instance of the module. 311 * @throws MacroExecutionException if the module could not be created. 312 */ 313 public static Object getModuleInstance(String moduleName) throws MacroExecutionException { 314 final ModuleFactory factory = getPackageObjectFactory(); 315 try { 316 return factory.createModule(moduleName); 317 } 318 catch (CheckstyleException exc) { 319 throw new MacroExecutionException("Couldn't find class: " + moduleName, exc); 320 } 321 } 322 323 /** 324 * Returns the default PackageObjectFactory with the default package names. 325 * 326 * @return the default PackageObjectFactory. 327 * @throws MacroExecutionException if the PackageObjectFactory cannot be created. 328 */ 329 private static PackageObjectFactory getPackageObjectFactory() throws MacroExecutionException { 330 try { 331 final ClassLoader cl = ViolationMessagesMacro.class.getClassLoader(); 332 final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl); 333 return new PackageObjectFactory(packageNames, cl); 334 } 335 catch (CheckstyleException exc) { 336 throw new MacroExecutionException("Couldn't load checkstyle modules", exc); 337 } 338 } 339 340 /** 341 * Construct a string with a leading newline character and followed by 342 * the given amount of spaces. We use this method only to match indentation in 343 * regular xdocs and have minimal diff when parsing the templates. 344 * This method exists until 345 * <a href="https://github.com/checkstyle/checkstyle/issues/13426">13426</a> 346 * 347 * @param amountOfSpaces the amount of spaces to add after the newline. 348 * @return the constructed string. 349 */ 350 public static String getNewlineAndIndentSpaces(int amountOfSpaces) { 351 return System.lineSeparator() + WHITESPACE.repeat(amountOfSpaces); 352 } 353 354 /** 355 * Returns path to the template for the given module name or throws an exception if the 356 * template cannot be found. 357 * 358 * @param moduleName the module whose template we are looking for. 359 * @return path to the template. 360 * @throws MacroExecutionException if the template cannot be found. 361 */ 362 public static Path getTemplatePath(String moduleName) throws MacroExecutionException { 363 final String fileNamePattern = ".*[\\\\/]" 364 + moduleName.toLowerCase(Locale.ROOT) + "\\..*"; 365 return getXdocsTemplatesFilePaths() 366 .stream() 367 .filter(path -> path.toString().matches(fileNamePattern)) 368 .findFirst() 369 .orElse(null); 370 } 371 372 /** 373 * Gets xdocs template file paths. These are files ending with .xml.template. 374 * This method will be changed to gather .xml once 375 * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved. 376 * 377 * @return a set of xdocs template file paths. 378 * @throws MacroExecutionException if an I/O error occurs. 379 */ 380 public static Set<Path> getXdocsTemplatesFilePaths() throws MacroExecutionException { 381 final Path directory = Path.of("src/site/xdoc"); 382 try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE, 383 (path, attr) -> { 384 return attr.isRegularFile() 385 && path.toString().endsWith(TEMPLATE_FILE_EXTENSION); 386 })) { 387 return stream.collect(Collectors.toUnmodifiableSet()); 388 } 389 catch (IOException ioException) { 390 throw new MacroExecutionException("Failed to find xdocs templates", ioException); 391 } 392 } 393 394 /** 395 * Returns the parent module name for the given module class. Returns either 396 * "TreeWalker" or "Checker". Returns null if the module class is null. 397 * 398 * @param moduleClass the module class. 399 * @return the parent module name as a string. 400 * @throws MacroExecutionException if the parent module cannot be found. 401 */ 402 public static String getParentModule(Class<?> moduleClass) 403 throws MacroExecutionException { 404 String parentModuleName = ""; 405 Class<?> parentClass = moduleClass.getSuperclass(); 406 407 while (parentClass != null) { 408 parentModuleName = CLASS_TO_PARENT_MODULE.get(parentClass); 409 if (parentModuleName != null) { 410 break; 411 } 412 parentClass = parentClass.getSuperclass(); 413 } 414 415 // If parent class is not found, check interfaces 416 if (parentModuleName == null || parentModuleName.isEmpty()) { 417 final Class<?>[] interfaces = moduleClass.getInterfaces(); 418 for (Class<?> interfaceClass : interfaces) { 419 parentModuleName = CLASS_TO_PARENT_MODULE.get(interfaceClass); 420 if (parentModuleName != null) { 421 break; 422 } 423 } 424 } 425 426 if (parentModuleName == null || parentModuleName.isEmpty()) { 427 final String message = String.format(Locale.ROOT, 428 "Failed to find parent module for %s", moduleClass.getSimpleName()); 429 throw new MacroExecutionException(message); 430 } 431 432 return parentModuleName; 433 } 434 435 /** 436 * Get a set of properties for the given class that should be documented. 437 * 438 * @param clss the class to get the properties for. 439 * @param instance the instance of the module. 440 * @return a set of properties for the given class. 441 */ 442 public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) { 443 final Set<String> properties = 444 getProperties(clss).stream() 445 .filter(prop -> { 446 return !isGlobalProperty(clss, prop) && !isUndocumentedProperty(clss, prop); 447 }) 448 .collect(Collectors.toCollection(HashSet::new)); 449 properties.addAll(getNonExplicitProperties(instance, clss)); 450 return new TreeSet<>(properties); 451 } 452 453 /** 454 * Gets the javadoc of module class. 455 * 456 * @param moduleClassName name of module class. 457 * @param modulePath module's path. 458 * @return javadoc of module. 459 * @throws MacroExecutionException if an error occurs during processing. 460 */ 461 public static DetailNode getModuleJavadoc(String moduleClassName, Path modulePath) 462 throws MacroExecutionException { 463 464 processModule(moduleClassName, modulePath); 465 return JavadocScraperResultUtil.getModuleJavadocNode(); 466 } 467 468 /** 469 * Get the javadocs of the properties of the module. If the property is not present in the 470 * module, then the javadoc of the property from the superclass(es) is used. 471 * 472 * @param properties the properties of the module. 473 * @param moduleName the name of the module. 474 * @param modulePath the module file path. 475 * @return the javadocs of the properties of the module. 476 * @throws MacroExecutionException if an error occurs during processing. 477 */ 478 public static Map<String, DetailNode> getPropertiesJavadocs(Set<String> properties, 479 String moduleName, Path modulePath) 480 throws MacroExecutionException { 481 // lazy initialization 482 if (SUPER_CLASS_PROPERTIES_JAVADOCS.isEmpty()) { 483 processSuperclasses(); 484 } 485 486 processModule(moduleName, modulePath); 487 488 final Map<String, DetailNode> unmodifiablePropertiesJavadocs = 489 JavadocScraperResultUtil.getPropertiesJavadocNode(); 490 final Map<String, DetailNode> propertiesJavadocs = 491 new LinkedHashMap<>(unmodifiablePropertiesJavadocs); 492 493 properties.forEach(property -> { 494 final DetailNode superClassPropertyJavadoc = 495 SUPER_CLASS_PROPERTIES_JAVADOCS.get(property); 496 if (superClassPropertyJavadoc != null) { 497 propertiesJavadocs.putIfAbsent(property, superClassPropertyJavadoc); 498 } 499 }); 500 501 assertAllPropertySetterJavadocsAreFound(properties, moduleName, propertiesJavadocs); 502 503 return propertiesJavadocs; 504 } 505 506 /** 507 * Assert that each property has a corresponding setter javadoc that is not null. 508 * 'tokens' and 'javadocTokens' are excluded from this check, because their 509 * description is different from the description of the setter. 510 * 511 * @param properties the properties of the module. 512 * @param moduleName the name of the module. 513 * @param javadocs the javadocs of the properties of the module. 514 * @throws MacroExecutionException if an error occurs during processing. 515 */ 516 private static void assertAllPropertySetterJavadocsAreFound( 517 Set<String> properties, String moduleName, Map<String, DetailNode> javadocs) 518 throws MacroExecutionException { 519 for (String property : properties) { 520 final boolean isDocumented = javadocs.containsKey(property) 521 || SUPER_CLASS_PROPERTIES_JAVADOCS.containsKey(property) 522 || TOKENS.equals(property) || JAVADOC_TOKENS.equals(property); 523 if (!isDocumented) { 524 throw new MacroExecutionException(String.format(Locale.ROOT, 525 "%s: Missing documentation for property '%s'. Check superclasses.", 526 moduleName, property)); 527 } 528 } 529 } 530 531 /** 532 * Collect the properties setters javadocs of the superclasses. 533 * 534 * @throws MacroExecutionException if an error occurs during processing. 535 */ 536 private static void processSuperclasses() throws MacroExecutionException { 537 for (Path superclassPath : MODULE_SUPER_CLASS_PATHS) { 538 final Path fileNamePath = superclassPath.getFileName(); 539 if (fileNamePath == null) { 540 throw new MacroExecutionException("Invalid superclass path: " + superclassPath); 541 } 542 final String superclassName = CommonUtil.getFileNameWithoutExtension( 543 fileNamePath.toString()); 544 processModule(superclassName, superclassPath); 545 final Map<String, DetailNode> superclassPropertiesJavadocs = 546 JavadocScraperResultUtil.getPropertiesJavadocNode(); 547 SUPER_CLASS_PROPERTIES_JAVADOCS.putAll(superclassPropertiesJavadocs); 548 } 549 } 550 551 /** 552 * Scrape the Javadocs of the class and its properties setters with 553 * ClassAndPropertiesSettersJavadocScraper. 554 * 555 * @param moduleName the name of the module. 556 * @param modulePath the module Path. 557 * @throws MacroExecutionException if an error occurs during processing. 558 */ 559 private static void processModule(String moduleName, Path modulePath) 560 throws MacroExecutionException { 561 if (!Files.isRegularFile(modulePath)) { 562 final String message = String.format(Locale.ROOT, 563 "File %s is not a file. Please check the 'modulePath' property.", modulePath); 564 throw new MacroExecutionException(message); 565 } 566 ClassAndPropertiesSettersJavadocScraper.initialize(moduleName); 567 final Checker checker = new Checker(); 568 checker.setModuleClassLoader(Checker.class.getClassLoader()); 569 final DefaultConfiguration scraperCheckConfig = 570 new DefaultConfiguration( 571 ClassAndPropertiesSettersJavadocScraper.class.getName()); 572 final DefaultConfiguration defaultConfiguration = 573 new DefaultConfiguration("configuration"); 574 final DefaultConfiguration treeWalkerConfig = 575 new DefaultConfiguration(TreeWalker.class.getName()); 576 defaultConfiguration.addProperty(CHARSET, StandardCharsets.UTF_8.name()); 577 defaultConfiguration.addChild(treeWalkerConfig); 578 treeWalkerConfig.addChild(scraperCheckConfig); 579 try { 580 checker.configure(defaultConfiguration); 581 final List<File> filesToProcess = List.of(modulePath.toFile()); 582 checker.process(filesToProcess); 583 checker.destroy(); 584 } 585 catch (CheckstyleException checkstyleException) { 586 final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName); 587 throw new MacroExecutionException(message, checkstyleException); 588 } 589 } 590 591 /** 592 * Get a set of properties for the given class. 593 * 594 * @param clss the class to get the properties for. 595 * @return a set of properties for the given class. 596 */ 597 public static Set<String> getProperties(Class<?> clss) { 598 final Set<String> result = new TreeSet<>(); 599 final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss); 600 601 for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { 602 if (propertyDescriptor.getWriteMethod() != null) { 603 result.add(propertyDescriptor.getName()); 604 } 605 } 606 607 return result; 608 } 609 610 /** 611 * Checks if the property is a global property. Global properties come from the base classes 612 * and are common to all checks. For example id, severity, tabWidth, etc. 613 * 614 * @param clss the class of the module. 615 * @param propertyName the name of the property. 616 * @return true if the property is a global property. 617 */ 618 private static boolean isGlobalProperty(Class<?> clss, String propertyName) { 619 return AbstractCheck.class.isAssignableFrom(clss) 620 && CHECK_PROPERTIES.contains(propertyName) 621 || AbstractJavadocCheck.class.isAssignableFrom(clss) 622 && JAVADOC_CHECK_PROPERTIES.contains(propertyName) 623 || AbstractFileSetCheck.class.isAssignableFrom(clss) 624 && FILESET_PROPERTIES.contains(propertyName); 625 } 626 627 /** 628 * Checks if the property is supposed to be documented. 629 * 630 * @param clss the class of the module. 631 * @param propertyName the name of the property. 632 * @return true if the property is supposed to be documented. 633 */ 634 private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) { 635 return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName); 636 } 637 638 /** 639 * Gets properties that are not explicitly captured but should be documented if 640 * certain conditions are met. 641 * 642 * @param instance the instance of the module. 643 * @param clss the class of the module. 644 * @return the non explicit properties. 645 */ 646 private static Set<String> getNonExplicitProperties( 647 Object instance, Class<?> clss) { 648 final Set<String> result = new TreeSet<>(); 649 if (AbstractCheck.class.isAssignableFrom(clss)) { 650 final AbstractCheck check = (AbstractCheck) instance; 651 652 final int[] acceptableTokens = check.getAcceptableTokens(); 653 Arrays.sort(acceptableTokens); 654 final int[] defaultTokens = check.getDefaultTokens(); 655 Arrays.sort(defaultTokens); 656 final int[] requiredTokens = check.getRequiredTokens(); 657 Arrays.sort(requiredTokens); 658 659 if (!Arrays.equals(acceptableTokens, defaultTokens) 660 || !Arrays.equals(acceptableTokens, requiredTokens)) { 661 result.add(TOKENS); 662 } 663 } 664 665 if (AbstractJavadocCheck.class.isAssignableFrom(clss)) { 666 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance; 667 result.add("violateExecutionOnNonTightHtml"); 668 669 final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens(); 670 Arrays.sort(acceptableJavadocTokens); 671 final int[] defaultJavadocTokens = check.getDefaultJavadocTokens(); 672 Arrays.sort(defaultJavadocTokens); 673 final int[] requiredJavadocTokens = check.getRequiredJavadocTokens(); 674 Arrays.sort(requiredJavadocTokens); 675 676 if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens) 677 || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) { 678 result.add(JAVADOC_TOKENS); 679 } 680 } 681 682 if (AbstractFileSetCheck.class.isAssignableFrom(clss)) { 683 result.add(FILE_EXTENSIONS); 684 } 685 return result; 686 } 687 688 /** 689 * Get the description of the property. 690 * 691 * @param propertyName the name of the property. 692 * @param javadoc the Javadoc of the property setter method. 693 * @param moduleName the name of the module. 694 * @return the description of the property. 695 * @throws MacroExecutionException if the description could not be extracted. 696 */ 697 public static String getPropertyDescriptionForXdoc( 698 String propertyName, DetailNode javadoc, String moduleName) 699 throws MacroExecutionException { 700 final String description; 701 if (TOKENS.equals(propertyName)) { 702 description = "tokens to check"; 703 } 704 else if (JAVADOC_TOKENS.equals(propertyName)) { 705 description = "javadoc tokens to check"; 706 } 707 else { 708 final String descriptionString = SETTER_PATTERN.matcher( 709 getDescriptionFromJavadocForXdoc(javadoc, moduleName)) 710 .replaceFirst(""); 711 712 final String firstLetterCapitalized = descriptionString.substring(0, 1) 713 .toUpperCase(Locale.ROOT); 714 description = firstLetterCapitalized + descriptionString.substring(1); 715 } 716 return description; 717 } 718 719 /** 720 * Get the since version of the property. 721 * 722 * @param moduleName the name of the module. 723 * @param moduleJavadoc the Javadoc of the module. 724 * @param propertyName the name of the property. 725 * @param propertyJavadoc the Javadoc of the property setter method. 726 * @return the since version of the property. 727 * @throws MacroExecutionException if the module since version could not be extracted. 728 */ 729 public static String getPropertySinceVersion(String moduleName, DetailNode moduleJavadoc, 730 String propertyName, DetailNode propertyJavadoc) 731 throws MacroExecutionException { 732 final String sinceVersion; 733 734 final Optional<String> specifiedPropertyVersionInPropertyJavadoc = 735 getPropertyVersionFromItsJavadoc(propertyJavadoc); 736 737 final Optional<String> specifiedPropertyVersionInModule = 738 getSpecifiedPropertyVersionInModule(propertyName, moduleJavadoc); 739 740 if (specifiedPropertyVersionInPropertyJavadoc.isPresent()) { 741 sinceVersion = specifiedPropertyVersionInPropertyJavadoc.get(); 742 } 743 else if (specifiedPropertyVersionInModule.isPresent()) { 744 sinceVersion = specifiedPropertyVersionInModule.get(); 745 } 746 else { 747 final String moduleSince = getSinceVersionFromJavadoc(moduleJavadoc); 748 749 if (moduleSince == null) { 750 throw new MacroExecutionException( 751 "Missing @since on module " + moduleName); 752 } 753 754 String propertySetterSince = null; 755 if (propertyJavadoc != null) { 756 propertySetterSince = getSinceVersionFromJavadoc(propertyJavadoc); 757 } 758 759 if (propertySetterSince != null 760 && isVersionAtLeast(propertySetterSince, moduleSince)) { 761 sinceVersion = propertySetterSince; 762 } 763 else { 764 sinceVersion = moduleSince; 765 } 766 } 767 768 return sinceVersion; 769 } 770 771 /** 772 * Extract the property since version from its Javadoc. 773 * 774 * @param propertyJavadoc the property Javadoc to extract the since version from. 775 * @return the Optional of property version specified in its javadoc. 776 */ 777 @Nullable 778 private static Optional<String> getPropertyVersionFromItsJavadoc(DetailNode propertyJavadoc) { 779 final Optional<DetailNode> propertyJavadocTag = 780 getPropertySinceJavadocTag(propertyJavadoc); 781 782 return propertyJavadocTag 783 .map(tag -> JavadocUtil.findFirstToken(tag, JavadocTokenTypes.DESCRIPTION)) 784 .map(description -> JavadocUtil.findFirstToken(description, JavadocTokenTypes.TEXT)) 785 .map(DetailNode::getText); 786 } 787 788 /** 789 * Find the propertySince Javadoc tag node in the given property Javadoc. 790 * 791 * @param javadoc the Javadoc to search. 792 * @return the Optional of propertySince Javadoc tag node or null if not found. 793 */ 794 private static Optional<DetailNode> getPropertySinceJavadocTag(DetailNode javadoc) { 795 Optional<DetailNode> propertySinceJavadocTag = Optional.empty(); 796 797 final Optional<DetailNode[]> propertyJavadocNodes = Optional.ofNullable(javadoc) 798 .map(DetailNode::getChildren); 799 800 if (propertyJavadocNodes.isPresent()) { 801 for (final DetailNode child : propertyJavadocNodes.get()) { 802 if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) { 803 final DetailNode customName = JavadocUtil.findFirstToken( 804 child, JavadocTokenTypes.CUSTOM_NAME); 805 if (customName != null && "@propertySince".equals(customName.getText())) { 806 propertySinceJavadocTag = Optional.of(child); 807 break; 808 } 809 } 810 } 811 } 812 813 return propertySinceJavadocTag; 814 } 815 816 /** 817 * Gets the specifically indicated version of module's property from the javadoc of module. 818 * 819 * @param propertyName the name of property. 820 * @param moduleJavadoc the javadoc of module. 821 * @return the specific since version of module's property. 822 * @throws MacroExecutionException if the module since version could not be extracted. 823 */ 824 private static Optional<String> getSpecifiedPropertyVersionInModule(String propertyName, 825 DetailNode moduleJavadoc) 826 throws MacroExecutionException { 827 Optional<String> specifiedVersion = Optional.empty(); 828 829 final Optional<DetailNode> propertyNodeFromModuleJavadoc = 830 getPropertyJavadocNodeInModule(propertyName, moduleJavadoc); 831 832 if (propertyNodeFromModuleJavadoc.isPresent()) { 833 final List<DetailNode> propertyModuleTextNodes = getNodesOfSpecificType( 834 propertyNodeFromModuleJavadoc.get().getChildren(), JavadocTokenTypes.TEXT); 835 836 final Optional<String> sinceVersionLine = propertyModuleTextNodes.stream() 837 .map(DetailNode::getText) 838 .filter(text -> text.startsWith(WHITESPACE + SINCE_VERSION)) 839 .findFirst(); 840 841 if (sinceVersionLine.isPresent()) { 842 final String sinceVersionText = sinceVersionLine.get(); 843 final int sinceVersionIndex = sinceVersionText.indexOf('.') - 1; 844 845 if (sinceVersionIndex > 0) { 846 specifiedVersion = Optional.of(sinceVersionText.substring(sinceVersionIndex)); 847 } 848 else { 849 throw new MacroExecutionException(sinceVersionText 850 + " has no valid version, at least one '.' is expected."); 851 } 852 } 853 } 854 855 return specifiedVersion; 856 } 857 858 /** 859 * Gets the javadoc node part of the property from the javadoc of the module. 860 * 861 * @param propertyName the name of property. 862 * @param moduleJavadoc the javadoc of module. 863 * @return the Optional of javadoc node part of the property. 864 */ 865 public static Optional<DetailNode> getPropertyJavadocNodeInModule(String propertyName, 866 DetailNode moduleJavadoc) { 867 final List<DetailNode> htmlElementNodes = getNodesOfSpecificType( 868 moduleJavadoc.getChildren(), JavadocTokenTypes.HTML_ELEMENT); 869 870 final List<DetailNode> ulTags = htmlElementNodes.stream() 871 .map(JavadocUtil::getFirstChild) 872 .filter(child -> { 873 final boolean isHtmlTag = child.getType() == JavadocTokenTypes.HTML_TAG; 874 final DetailNode htmlTagNameNode = JavadocUtil.findFirstToken( 875 JavadocUtil.getFirstChild(child), JavadocTokenTypes.HTML_TAG_NAME); 876 877 return isHtmlTag && "ul".equals(htmlTagNameNode.getText()); 878 }) 879 .toList(); 880 final DetailNode[] childrenOfUlTags = ulTags.stream() 881 .flatMap(ulTag -> Arrays.stream(ulTag.getChildren())) 882 .toArray(DetailNode[]::new); 883 final List<DetailNode> innerHtmlElementsOfUlTags = 884 getNodesOfSpecificType(childrenOfUlTags, JavadocTokenTypes.HTML_ELEMENT); 885 886 final List<DetailNode> liTags = innerHtmlElementsOfUlTags.stream() 887 .map(JavadocUtil::getFirstChild) 888 .filter(tag -> tag.getType() == JavadocTokenTypes.LI) 889 .toList(); 890 891 final List<DetailNode> liTagsInlineTexts = liTags.stream() 892 .map(liTag -> JavadocUtil.findFirstToken(liTag, JavadocTokenTypes.JAVADOC_INLINE_TAG)) 893 .filter(Objects::nonNull) 894 .map(inlineTag -> JavadocUtil.findFirstToken(inlineTag, JavadocTokenTypes.TEXT)) 895 .toList(); 896 897 return liTagsInlineTexts.stream() 898 .filter(text -> text.getText().equals(propertyName)) 899 .map(textNode -> textNode.getParent().getParent()) 900 .findFirst(); 901 902 } 903 904 /** 905 * Gets all javadoc nodes of selected type. 906 * 907 * @param allNodes Nodes to choose from. 908 * @param neededType the Javadoc token type to select. 909 * @return the List of DetailNodes of selected type. 910 */ 911 public static List<DetailNode> getNodesOfSpecificType(DetailNode[] allNodes, int neededType) { 912 return Arrays.stream(allNodes) 913 .filter(child -> child.getType() == neededType) 914 .toList(); 915 } 916 917 /** 918 * Extract the since version from the Javadoc. 919 * 920 * @param javadoc the Javadoc to extract the since version from. 921 * @return the since version of the setter. 922 */ 923 @Nullable 924 private static String getSinceVersionFromJavadoc(DetailNode javadoc) { 925 final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc); 926 return Optional.ofNullable(sinceJavadocTag) 927 .map(tag -> JavadocUtil.findFirstToken(tag, JavadocTokenTypes.DESCRIPTION)) 928 .map(description -> JavadocUtil.findFirstToken(description, JavadocTokenTypes.TEXT)) 929 .map(DetailNode::getText) 930 .orElse(null); 931 } 932 933 /** 934 * Find the since Javadoc tag node in the given Javadoc. 935 * 936 * @param javadoc the Javadoc to search. 937 * @return the since Javadoc tag node or null if not found. 938 */ 939 private static DetailNode getSinceJavadocTag(DetailNode javadoc) { 940 final DetailNode[] children = javadoc.getChildren(); 941 DetailNode javadocTagWithSince = null; 942 for (final DetailNode child : children) { 943 if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) { 944 final DetailNode sinceNode = JavadocUtil.findFirstToken( 945 child, JavadocTokenTypes.SINCE_LITERAL); 946 if (sinceNode != null) { 947 javadocTagWithSince = child; 948 break; 949 } 950 } 951 } 952 return javadocTagWithSince; 953 } 954 955 /** 956 * Returns {@code true} if {@code actualVersion} ≥ {@code requiredVersion}. 957 * Both versions have any trailing "-SNAPSHOT" stripped before comparison. 958 * 959 * @param actualVersion e.g. "8.3" or "8.3-SNAPSHOT" 960 * @param requiredVersion e.g. "8.3" 961 * @return {@code true} if actualVersion exists, and, numerically, is at least requiredVersion 962 */ 963 private static boolean isVersionAtLeast(String actualVersion, 964 String requiredVersion) { 965 final Version actualVersionParsed = Version.parse(actualVersion); 966 final Version requiredVersionParsed = Version.parse(requiredVersion); 967 968 return actualVersionParsed.compareTo(requiredVersionParsed) >= 0; 969 } 970 971 /** 972 * Get the type of the property. 973 * 974 * @param field the field to get the type of. 975 * @param propertyName the name of the property. 976 * @param moduleName the name of the module. 977 * @param instance the instance of the module. 978 * @return the type of the property. 979 * @throws MacroExecutionException if an error occurs during getting the type. 980 */ 981 public static String getType(Field field, String propertyName, 982 String moduleName, Object instance) 983 throws MacroExecutionException { 984 final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance); 985 return Optional.ofNullable(field) 986 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class)) 987 .filter(propertyType -> propertyType.value() != PropertyType.TOKEN_ARRAY) 988 .map(propertyType -> propertyType.value().getDescription()) 989 .orElseGet(fieldClass::getTypeName); 990 } 991 992 /** 993 * Get the default value of the property. 994 * 995 * @param propertyName the name of the property. 996 * @param field the field to get the default value of. 997 * @param classInstance the instance of the class to get the default value of. 998 * @param moduleName the name of the module. 999 * @return the default value of the property. 1000 * @throws MacroExecutionException if an error occurs during getting the default value. 1001 * @noinspection IfStatementWithTooManyBranches 1002 * @noinspectionreason IfStatementWithTooManyBranches - complex nature of getting properties 1003 * from XML files requires giant if/else statement 1004 */ 1005 // -@cs[CyclomaticComplexity] Splitting would not make the code more readable 1006 public static String getDefaultValue(String propertyName, Field field, 1007 Object classInstance, String moduleName) 1008 throws MacroExecutionException { 1009 final Object value = getFieldValue(field, classInstance); 1010 final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, classInstance); 1011 String result = null; 1012 1013 if (classInstance instanceof PropertyCacheFile) { 1014 result = "null (no cache file)"; 1015 } 1016 else if (fieldClass == boolean.class 1017 || fieldClass == int.class 1018 || fieldClass == URI.class 1019 || fieldClass == String.class) { 1020 if (value != null) { 1021 result = value.toString(); 1022 } 1023 } 1024 else if (fieldClass == int[].class 1025 || ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) { 1026 result = getIntArrayPropertyValue(value); 1027 } 1028 else if (fieldClass == double[].class) { 1029 result = removeSquareBrackets(Arrays.toString((double[]) value).replace(".0", "")); 1030 } 1031 else if (fieldClass == String[].class) { 1032 result = getStringArrayPropertyValue(value); 1033 } 1034 else if (fieldClass == Pattern.class) { 1035 if (value != null) { 1036 result = value.toString().replace("\n", "\\n").replace("\t", "\\t") 1037 .replace("\r", "\\r").replace("\f", "\\f"); 1038 } 1039 } 1040 else if (fieldClass == Pattern[].class) { 1041 result = getPatternArrayPropertyValue(value); 1042 } 1043 else if (fieldClass.isEnum()) { 1044 if (value != null) { 1045 result = value.toString().toLowerCase(Locale.ENGLISH); 1046 } 1047 } 1048 else if (fieldClass == AccessModifierOption[].class) { 1049 result = removeSquareBrackets(Arrays.toString((Object[]) value)); 1050 } 1051 1052 if (result == null) { 1053 result = "null"; 1054 } 1055 1056 return result; 1057 } 1058 1059 /** 1060 * Gets the name of the bean property's default value for the Pattern array class. 1061 * 1062 * @param fieldValue The bean property's value 1063 * @return String form of property's default value 1064 */ 1065 private static String getPatternArrayPropertyValue(Object fieldValue) { 1066 Object value = fieldValue; 1067 if (value instanceof Collection<?> collection) { 1068 value = collection.stream() 1069 .map(Pattern.class::cast) 1070 .toArray(Pattern[]::new); 1071 } 1072 1073 String result = ""; 1074 if (value != null && Array.getLength(value) > 0) { 1075 result = removeSquareBrackets( 1076 Arrays.stream((Pattern[]) value) 1077 .map(Pattern::pattern) 1078 .collect(Collectors.joining(COMMA_SPACE))); 1079 } 1080 1081 return result; 1082 } 1083 1084 /** 1085 * Removes square brackets [ and ] from the given string. 1086 * 1087 * @param value the string to remove square brackets from. 1088 * @return the string without square brackets. 1089 */ 1090 private static String removeSquareBrackets(String value) { 1091 return value 1092 .replace("[", "") 1093 .replace("]", ""); 1094 } 1095 1096 /** 1097 * Gets the name of the bean property's default value for the string array class. 1098 * 1099 * @param value The bean property's value 1100 * @return String form of property's default value 1101 */ 1102 private static String getStringArrayPropertyValue(Object value) { 1103 final String result; 1104 if (value == null) { 1105 result = ""; 1106 } 1107 else { 1108 try (Stream<?> valuesStream = getValuesStream(value)) { 1109 result = valuesStream 1110 .map(String.class::cast) 1111 .sorted() 1112 .collect(Collectors.joining(COMMA_SPACE)); 1113 } 1114 } 1115 1116 return result; 1117 } 1118 1119 /** 1120 * Generates a stream of values from the given value. 1121 * 1122 * @param value the value to generate the stream from. 1123 * @return the stream of values. 1124 */ 1125 private static Stream<?> getValuesStream(Object value) { 1126 final Stream<?> valuesStream; 1127 if (value instanceof Collection<?> collection) { 1128 valuesStream = collection.stream(); 1129 } 1130 else { 1131 final Object[] array = (Object[]) value; 1132 valuesStream = Arrays.stream(array); 1133 } 1134 return valuesStream; 1135 } 1136 1137 /** 1138 * Returns the name of the bean property's default value for the int array class. 1139 * 1140 * @param value The bean property's value. 1141 * @return String form of property's default value. 1142 */ 1143 private static String getIntArrayPropertyValue(Object value) { 1144 try (IntStream stream = getIntStream(value)) { 1145 return stream 1146 .mapToObj(TokenUtil::getTokenName) 1147 .sorted() 1148 .collect(Collectors.joining(COMMA_SPACE)); 1149 } 1150 } 1151 1152 /** 1153 * Get the int stream from the given value. 1154 * 1155 * @param value the value to get the int stream from. 1156 * @return the int stream. 1157 */ 1158 private static IntStream getIntStream(Object value) { 1159 final IntStream stream; 1160 if (value instanceof Collection<?> collection) { 1161 stream = collection.stream() 1162 .mapToInt(int.class::cast); 1163 } 1164 else if (value instanceof BitSet) { 1165 stream = ((BitSet) value).stream(); 1166 } 1167 else { 1168 stream = Arrays.stream((int[]) value); 1169 } 1170 return stream; 1171 } 1172 1173 /** 1174 * Gets the class of the given field. 1175 * 1176 * @param field the field to get the class of. 1177 * @param propertyName the name of the property. 1178 * @param moduleName the name of the module. 1179 * @param instance the instance of the module. 1180 * @return the class of the field. 1181 * @throws MacroExecutionException if an error occurs during getting the class. 1182 */ 1183 // -@cs[CyclomaticComplexity] Splitting would not make the code more readable 1184 // -@cs[ForbidWildcardAsReturnType] Implied by design to return different types 1185 public static Class<?> getFieldClass(Field field, String propertyName, 1186 String moduleName, Object instance) 1187 throws MacroExecutionException { 1188 Class<?> result = null; 1189 1190 if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD 1191 .contains(moduleName + DOT + propertyName)) { 1192 result = getPropertyClass(propertyName, instance); 1193 } 1194 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) { 1195 result = String[].class; 1196 } 1197 if (field != null && result == null) { 1198 result = field.getType(); 1199 } 1200 1201 if (result == null) { 1202 throw new MacroExecutionException( 1203 "Could not find field " + propertyName + " in class " + moduleName); 1204 } 1205 1206 if (field != null && (result == List.class || result == Set.class)) { 1207 final ParameterizedType type = (ParameterizedType) field.getGenericType(); 1208 final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0]; 1209 1210 if (parameterClass == Integer.class) { 1211 result = int[].class; 1212 } 1213 else if (parameterClass == String.class) { 1214 result = String[].class; 1215 } 1216 else if (parameterClass == Pattern.class) { 1217 result = Pattern[].class; 1218 } 1219 else { 1220 final String message = "Unknown parameterized type: " 1221 + parameterClass.getSimpleName(); 1222 throw new MacroExecutionException(message); 1223 } 1224 } 1225 else if (result == BitSet.class) { 1226 result = int[].class; 1227 } 1228 1229 return result; 1230 } 1231 1232 /** 1233 * Gets the class of the given java property. 1234 * 1235 * @param propertyName the name of the property. 1236 * @param instance the instance of the module. 1237 * @return the class of the java property. 1238 * @throws MacroExecutionException if an error occurs during getting the class. 1239 */ 1240 // -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field 1241 public static Class<?> getPropertyClass(String propertyName, Object instance) 1242 throws MacroExecutionException { 1243 final Class<?> result; 1244 try { 1245 final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance, 1246 propertyName); 1247 result = descriptor.getPropertyType(); 1248 } 1249 catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) { 1250 throw new MacroExecutionException(exc.getMessage(), exc); 1251 } 1252 return result; 1253 } 1254 1255 /** 1256 * Get the difference between two lists of tokens. 1257 * 1258 * @param tokens the list of tokens to remove from. 1259 * @param subtractions the tokens to remove. 1260 * @return the difference between the two lists. 1261 */ 1262 public static List<Integer> getDifference(int[] tokens, int... subtractions) { 1263 final Set<Integer> subtractionsSet = Arrays.stream(subtractions) 1264 .boxed() 1265 .collect(Collectors.toUnmodifiableSet()); 1266 return Arrays.stream(tokens) 1267 .boxed() 1268 .filter(token -> !subtractionsSet.contains(token)) 1269 .toList(); 1270 } 1271 1272 /** 1273 * Gets the field with the given name from the given class. 1274 * 1275 * @param fieldClass the class to get the field from. 1276 * @param propertyName the name of the field. 1277 * @return the field we are looking for. 1278 */ 1279 public static Field getField(Class<?> fieldClass, String propertyName) { 1280 Field result = null; 1281 Class<?> currentClass = fieldClass; 1282 1283 while (!Object.class.equals(currentClass)) { 1284 try { 1285 result = currentClass.getDeclaredField(propertyName); 1286 result.trySetAccessible(); 1287 break; 1288 } 1289 catch (NoSuchFieldException ignored) { 1290 currentClass = currentClass.getSuperclass(); 1291 } 1292 } 1293 1294 return result; 1295 } 1296 1297 /** 1298 * Constructs string with relative link to the provided document. 1299 * 1300 * @param moduleName the name of the module. 1301 * @param document the path of the document. 1302 * @return relative link to the document. 1303 * @throws MacroExecutionException if link to the document cannot be constructed. 1304 */ 1305 public static String getLinkToDocument(String moduleName, String document) 1306 throws MacroExecutionException { 1307 final Path templatePath = getTemplatePath(FINAL_CHECK.matcher(moduleName).replaceAll("")); 1308 if (templatePath == null) { 1309 throw new MacroExecutionException( 1310 String.format(Locale.ROOT, 1311 "Could not find template for %s", moduleName)); 1312 } 1313 final Path templatePathParent = templatePath.getParent(); 1314 if (templatePathParent == null) { 1315 throw new MacroExecutionException("Failed to get parent path for " + templatePath); 1316 } 1317 return templatePathParent 1318 .relativize(Path.of(SRC, "site/xdoc", document)) 1319 .toString() 1320 .replace(".xml", ".html") 1321 .replace('\\', '/'); 1322 } 1323 1324 /** 1325 * Get all templates whose content contains properties macro. 1326 * 1327 * @return templates whose content contains properties macro. 1328 * @throws CheckstyleException if file could not be read. 1329 * @throws MacroExecutionException if template file is not found. 1330 */ 1331 public static List<Path> getTemplatesThatContainPropertiesMacro() 1332 throws CheckstyleException, MacroExecutionException { 1333 final List<Path> result = new ArrayList<>(); 1334 final Set<Path> templatesPaths = getXdocsTemplatesFilePaths(); 1335 for (Path templatePath: templatesPaths) { 1336 final String content = getFileContents(templatePath); 1337 final String propertiesMacroDefinition = "<macro name=\"properties\""; 1338 if (content.contains(propertiesMacroDefinition)) { 1339 result.add(templatePath); 1340 } 1341 } 1342 return result; 1343 } 1344 1345 /** 1346 * Get file contents as string. 1347 * 1348 * @param pathToFile path to file. 1349 * @return file contents as string. 1350 * @throws CheckstyleException if file could not be read. 1351 */ 1352 private static String getFileContents(Path pathToFile) throws CheckstyleException { 1353 final String content; 1354 try { 1355 content = Files.readString(pathToFile, StandardCharsets.UTF_8); 1356 } 1357 catch (IOException ioException) { 1358 final String message = String.format(Locale.ROOT, "Failed to read file: %s", 1359 pathToFile); 1360 throw new CheckstyleException(message, ioException); 1361 } 1362 return content; 1363 } 1364 1365 /** 1366 * Get the module name from the file. The module name is the file name without the extension. 1367 * 1368 * @param file file to extract the module name from. 1369 * @return module name. 1370 */ 1371 public static String getModuleName(File file) { 1372 final String fullFileName = file.getName(); 1373 return CommonUtil.getFileNameWithoutExtension(fullFileName); 1374 } 1375 1376 /** 1377 * Extracts the description from the javadoc detail node. Performs a DFS traversal on the 1378 * detail node and extracts the text nodes. This description is additionally processed to 1379 * fit Xdoc format. 1380 * 1381 * @param javadoc the Javadoc to extract the description from. 1382 * @param moduleName the name of the module. 1383 * @return the description of the setter. 1384 * @throws MacroExecutionException if the description could not be extracted. 1385 * @noinspection TooBroadScope 1386 * @noinspectionreason TooBroadScope - complex nature of method requires large scope 1387 */ 1388 // -@cs[NPathComplexity] Splitting would not make the code more readable 1389 // -@cs[CyclomaticComplexity] Splitting would not make the code more readable. 1390 private static String getDescriptionFromJavadocForXdoc(DetailNode javadoc, String moduleName) 1391 throws MacroExecutionException { 1392 boolean isInCodeLiteral = false; 1393 boolean isInHtmlElement = false; 1394 boolean isInHrefAttribute = false; 1395 final StringBuilder description = new StringBuilder(128); 1396 final Deque<DetailNode> queue = new ArrayDeque<>(); 1397 final List<DetailNode> descriptionNodes = getFirstJavadocParagraphNodes(javadoc); 1398 Lists.reverse(descriptionNodes).forEach(queue::push); 1399 1400 // Perform DFS traversal on description nodes 1401 while (!queue.isEmpty()) { 1402 final DetailNode node = queue.pop(); 1403 Lists.reverse(Arrays.asList(node.getChildren())).forEach(queue::push); 1404 1405 if (node.getType() == JavadocTokenTypes.HTML_TAG_NAME 1406 && "href".equals(node.getText())) { 1407 isInHrefAttribute = true; 1408 } 1409 if (isInHrefAttribute && node.getType() == JavadocTokenTypes.ATTR_VALUE) { 1410 final String href = node.getText(); 1411 if (href.contains(CHECKSTYLE_ORG_URL)) { 1412 DescriptionExtractor.handleInternalLink(description, moduleName, href); 1413 } 1414 else { 1415 description.append(href); 1416 } 1417 1418 isInHrefAttribute = false; 1419 continue; 1420 } 1421 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) { 1422 isInHtmlElement = true; 1423 } 1424 if (node.getType() == JavadocTokenTypes.END 1425 && node.getParent().getType() == JavadocTokenTypes.HTML_ELEMENT_END) { 1426 description.append(node.getText()); 1427 isInHtmlElement = false; 1428 } 1429 if (node.getType() == JavadocTokenTypes.TEXT 1430 // If a node has children, its text is not part of the description 1431 || isInHtmlElement && node.getChildren().length == 0 1432 // Some HTML elements span multiple lines, so we avoid the asterisk 1433 && node.getType() != JavadocTokenTypes.LEADING_ASTERISK) { 1434 description.append(node.getText()); 1435 } 1436 if (node.getType() == JavadocTokenTypes.CODE_LITERAL) { 1437 isInCodeLiteral = true; 1438 description.append("<code>"); 1439 } 1440 if (isInCodeLiteral 1441 && node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG_END) { 1442 isInCodeLiteral = false; 1443 description.append("</code>"); 1444 } 1445 } 1446 return description.toString().trim(); 1447 } 1448 1449 /** 1450 * Get 1st paragraph from the Javadoc with no additional processing. 1451 * 1452 * @param javadoc the Javadoc to extract first paragraph from. 1453 * @return first paragraph of javadoc. 1454 */ 1455 public static String getFirstParagraphFromJavadoc(DetailNode javadoc) { 1456 final Deque<DetailNode> stack = new ArrayDeque<>(); 1457 final List<DetailNode> firstParagraphNodes = getFirstJavadocParagraphNodes(javadoc); 1458 Lists.reverse(firstParagraphNodes).forEach(stack::push); 1459 final StringBuilder result = new StringBuilder(1024); 1460 while (!stack.isEmpty()) { 1461 final DetailNode detailNode = stack.pop(); 1462 1463 Lists.reverse(Arrays.asList(detailNode.getChildren())).forEach(stack::push); 1464 1465 String childText = detailNode.getText(); 1466 1467 if (detailNode.getParent().getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { 1468 childText = JavadocMetadataScraper.adjustCodeInlineTagChildToHtml(detailNode); 1469 } 1470 1471 // Regular expression for detecting ANTLR tokens(for e.g. CLASS_DEF). 1472 final Pattern tokenTextPattern = Pattern.compile("([A-Z_]{2,})+"); 1473 if (detailNode.getType() != JavadocTokenTypes.LEADING_ASTERISK 1474 && !tokenTextPattern.matcher(childText).matches()) { 1475 result.append(childText); 1476 } 1477 } 1478 return result.toString().trim(); 1479 } 1480 1481 /** 1482 * Extracts first paragraph nodes from javadoc. 1483 * 1484 * @param javadoc the Javadoc to extract the description from. 1485 * @return the first paragraph nodes of the setter. 1486 */ 1487 public static List<DetailNode> getFirstJavadocParagraphNodes(DetailNode javadoc) { 1488 final DetailNode[] children = javadoc.getChildren(); 1489 final List<DetailNode> firstParagraphNodes = new ArrayList<>(); 1490 for (final DetailNode child : children) { 1491 if (isEndOfFirstJavadocParagraph(child)) { 1492 break; 1493 } 1494 firstParagraphNodes.add(child); 1495 } 1496 return firstParagraphNodes; 1497 } 1498 1499 /** 1500 * Determines if the given child index is the end of the first Javadoc paragraph. The end 1501 * of the description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK, 1502 * NEWLINE, LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the 1503 * one below this line. 1504 * 1505 * @param child the child to check. 1506 * @return true if the given child index is the end of the first javadoc paragraph. 1507 */ 1508 public static boolean isEndOfFirstJavadocParagraph(DetailNode child) { 1509 final DetailNode nextSibling = JavadocUtil.getNextSibling(child); 1510 final DetailNode secondNextSibling = JavadocUtil.getNextSibling(nextSibling); 1511 final DetailNode thirdNextSibling = JavadocUtil.getNextSibling(secondNextSibling); 1512 1513 return child.getType() == JavadocTokenTypes.NEWLINE 1514 && nextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK 1515 && secondNextSibling.getType() == JavadocTokenTypes.NEWLINE 1516 && thirdNextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK; 1517 } 1518 1519 /** 1520 * Simplifies type name just to the name of the class, rather than entire package. 1521 * 1522 * @param fullTypeName full type name. 1523 * @return simplified type name, that is, name of the class. 1524 */ 1525 public static String simplifyTypeName(String fullTypeName) { 1526 final int simplifiedStartIndex; 1527 1528 if (fullTypeName.contains("$")) { 1529 simplifiedStartIndex = fullTypeName.lastIndexOf('$') + 1; 1530 } 1531 else { 1532 simplifiedStartIndex = fullTypeName.lastIndexOf('.') + 1; 1533 } 1534 1535 return fullTypeName.substring(simplifiedStartIndex); 1536 } 1537 1538 /** Utility class for extracting description from a method's Javadoc. */ 1539 private static final class DescriptionExtractor { 1540 1541 /** 1542 * Converts the href value to a relative link to the document and appends it to the 1543 * description. 1544 * 1545 * @param description the description to append the relative link to. 1546 * @param moduleName the name of the module. 1547 * @param value the href value. 1548 * @throws MacroExecutionException if the relative link could not be created. 1549 */ 1550 private static void handleInternalLink(StringBuilder description, 1551 String moduleName, String value) 1552 throws MacroExecutionException { 1553 String href = value; 1554 href = href.replace(CHECKSTYLE_ORG_URL, ""); 1555 // Remove first and last characters, they are always double quotes 1556 href = href.substring(1, href.length() - 1); 1557 1558 final String relativeHref = getLinkToDocument(moduleName, href); 1559 final char doubleQuote = '\"'; 1560 description.append(doubleQuote).append(relativeHref).append(doubleQuote); 1561 } 1562 } 1563}