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