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