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