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