001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.site; 021 022import java.beans.PropertyDescriptor; 023import java.io.File; 024import java.io.IOException; 025import java.lang.module.ModuleDescriptor.Version; 026import java.lang.reflect.Array; 027import java.lang.reflect.Field; 028import java.lang.reflect.InvocationTargetException; 029import java.lang.reflect.ParameterizedType; 030import java.net.URI; 031import java.nio.file.Files; 032import java.nio.file.Path; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.BitSet; 036import java.util.Collection; 037import java.util.Collections; 038import java.util.HashSet; 039import java.util.List; 040import java.util.Locale; 041import java.util.Map; 042import java.util.Optional; 043import java.util.Set; 044import java.util.TreeMap; 045import java.util.TreeSet; 046import java.util.regex.Pattern; 047import java.util.stream.Collectors; 048import java.util.stream.IntStream; 049import java.util.stream.Stream; 050 051import org.apache.commons.beanutils.PropertyUtils; 052import org.apache.maven.doxia.macro.MacroExecutionException; 053 054import com.puppycrawl.tools.checkstyle.Checker; 055import com.puppycrawl.tools.checkstyle.DefaultConfiguration; 056import com.puppycrawl.tools.checkstyle.ModuleFactory; 057import com.puppycrawl.tools.checkstyle.PackageNamesLoader; 058import com.puppycrawl.tools.checkstyle.PackageObjectFactory; 059import com.puppycrawl.tools.checkstyle.PropertyCacheFile; 060import com.puppycrawl.tools.checkstyle.PropertyType; 061import com.puppycrawl.tools.checkstyle.TreeWalker; 062import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 063import com.puppycrawl.tools.checkstyle.XdocsPropertyType; 064import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 065import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 066import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter; 067import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 068import com.puppycrawl.tools.checkstyle.api.DetailNode; 069import com.puppycrawl.tools.checkstyle.api.Filter; 070import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 071import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck; 072import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck; 073import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck; 074import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck; 075import com.puppycrawl.tools.checkstyle.internal.annotation.PreserveOrder; 076import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraperUtil; 077import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 078import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 079import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 080 081/** 082 * Utility class for site generation. 083 */ 084public final class SiteUtil { 085 086 /** The string 'tokens'. */ 087 public static final String TOKENS = "tokens"; 088 /** The string 'javadocTokens'. */ 089 public static final String JAVADOC_TOKENS = "javadocTokens"; 090 /** The string 'violateExecutionOnNonTightHtml'. */ 091 public static final String VIOLATE_EXECUTION_ON_NON_TIGHT_HTML = 092 "violateExecutionOnNonTightHtml"; 093 /** The string '.'. */ 094 public static final String DOT = "."; 095 /** The string ','. */ 096 public static final String COMMA = ","; 097 /** The whitespace. */ 098 public static final String WHITESPACE = " "; 099 /** The string ', '. */ 100 public static final String COMMA_SPACE = COMMA + WHITESPACE; 101 /** The string 'TokenTypes'. */ 102 public static final String TOKEN_TYPES = "TokenTypes"; 103 /** The path to the TokenTypes.html file. */ 104 public static final String PATH_TO_TOKEN_TYPES = 105 "apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html"; 106 /** The path to the JavadocTokenTypes.html file. */ 107 public static final String PATH_TO_JAVADOC_TOKEN_TYPES = 108 "apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html"; 109 /** The string of JavaDoc module marking 'Since version'. */ 110 public static final String SINCE_VERSION = "Since version"; 111 /** The 'Check' pattern at the end of string. */ 112 public static final Pattern FINAL_CHECK = Pattern.compile("Check$"); 113 /** The string 'fileExtensions'. */ 114 public static final String FILE_EXTENSIONS = "fileExtensions"; 115 /** The string 'charset'. */ 116 public static final String CHARSET = "charset"; 117 118 /** Precompiled regex pattern to remove the "Setter to " prefix from strings. */ 119 private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to "); 120 121 /** The url of the checkstyle website. */ 122 private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/"; 123 /** The string 'checks'. */ 124 private static final String CHECKS = "checks"; 125 /** The string 'naming'. */ 126 private static final String NAMING = "naming"; 127 /** The string 'src'. */ 128 private static final String SRC = "src"; 129 /** Template file extension. */ 130 private static final String TEMPLATE_FILE_EXTENSION = ".xml.template"; 131 132 /** The precompiled pattern for a comma followed by a space. */ 133 private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", "); 134 135 /** The string '{}'. */ 136 private static final String EMPTY_CURLY_BRACES = "{}"; 137 138 /** The string 'null'. */ 139 private static final String NULL_STR = "null"; 140 141 /** Class name and their corresponding parent module name. */ 142 private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries( 143 Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()), 144 Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()), 145 Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()), 146 Map.entry(Filter.class, Checker.class.getSimpleName()), 147 Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName()) 148 ); 149 150 /** Set of properties that every check has. */ 151 private static final Set<String> CHECK_PROPERTIES = 152 getProperties(AbstractCheck.class); 153 154 /** Set of properties that every Javadoc check has. */ 155 private static final Set<String> JAVADOC_CHECK_PROPERTIES = 156 getProperties(AbstractJavadocCheck.class); 157 158 /** Set of properties that every FileSet check has. */ 159 private static final Set<String> FILESET_PROPERTIES = 160 getProperties(AbstractFileSetCheck.class); 161 162 /** 163 * Check and property name. 164 */ 165 private static final String HEADER_CHECK_HEADER = "HeaderCheck.header"; 166 167 /** 168 * Check and property name. 169 */ 170 private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header"; 171 172 /** 173 * The string 'api'. 174 */ 175 private static final String API = "api"; 176 177 /** Set of properties that are undocumented. Those are internal properties. */ 178 private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of( 179 "SuppressWithNearbyCommentFilter.fileContents", 180 "SuppressionCommentFilter.fileContents" 181 ); 182 183 /** Properties that can not be gathered from class instance. */ 184 private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of( 185 // static field (all upper case) 186 "SuppressWarningsHolder.aliasList", 187 // loads string into memory similar to file 188 HEADER_CHECK_HEADER, 189 REGEXP_HEADER_CHECK_HEADER, 190 // property is an int, but we cut off excess to accommodate old versions 191 "RedundantModifierCheck.jdkVersion", 192 // until https://github.com/checkstyle/checkstyle/issues/13376 193 "CustomImportOrderCheck.customImportOrderRules" 194 ); 195 196 /** Path to main source code folder. */ 197 private static final String MAIN_FOLDER_PATH = Path.of( 198 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString(); 199 200 /** List of files who are superclasses and contain certain properties that checks inherit. */ 201 private static final List<Path> MODULE_SUPER_CLASS_PATHS = List.of( 202 Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractAccessControlNameCheck.java"), 203 Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractNameCheck.java"), 204 Path.of(MAIN_FOLDER_PATH, CHECKS, "javadoc", "AbstractJavadocCheck.java"), 205 Path.of(MAIN_FOLDER_PATH, API, "AbstractFileSetCheck.java"), 206 Path.of(MAIN_FOLDER_PATH, API, "AbstractCheck.java"), 207 Path.of(MAIN_FOLDER_PATH, CHECKS, "header", "AbstractHeaderCheck.java"), 208 Path.of(MAIN_FOLDER_PATH, CHECKS, "metrics", "AbstractClassCouplingCheck.java"), 209 Path.of(MAIN_FOLDER_PATH, CHECKS, "whitespace", "AbstractParenPadCheck.java") 210 ); 211 212 /** 213 * Private utility constructor. 214 */ 215 private SiteUtil() { 216 } 217 218 /** 219 * Get string values of the message keys from the given check class. 220 * 221 * @param module class to examine. 222 * @return a set of checkstyle's module message keys. 223 * @throws MacroExecutionException if extraction of message keys fails. 224 */ 225 public static Set<String> getMessageKeys(Class<?> module) 226 throws MacroExecutionException { 227 final Set<Field> messageKeyFields = getCheckMessageKeysFields(module); 228 final Set<String> messageKeys = new TreeSet<>(); 229 for (Field field : messageKeyFields) { 230 messageKeys.add(getFieldValue(field, module).toString()); 231 } 232 return messageKeys; 233 } 234 235 /** 236 * Gets the check's messages keys. 237 * 238 * @param module class to examine. 239 * @return a set of checkstyle's module message fields. 240 * @throws MacroExecutionException if the attempt to read a protected class fails. 241 * @noinspection ChainOfInstanceofChecks 242 * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at 243 * <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a> 244 * 245 */ 246 private static Set<Field> getCheckMessageKeysFields(Class<?> module) 247 throws MacroExecutionException { 248 try { 249 final Set<Field> checkstyleMessages = new HashSet<>(); 250 251 // get all fields from current class 252 final Field[] fields = module.getDeclaredFields(); 253 254 for (Field field : fields) { 255 if (field.getName().startsWith("MSG_")) { 256 checkstyleMessages.add(field); 257 } 258 } 259 260 final Class<?> superModule = module.getSuperclass(); 261 if (superModule != null) { 262 checkstyleMessages.addAll(getCheckMessageKeysFields(superModule)); 263 } 264 265 if (module == RegexpMultilineCheck.class) { 266 checkstyleMessages.addAll(getCheckMessageKeysFields(Class.forName( 267 "com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector"))); 268 } 269 else if (module == RegexpSinglelineCheck.class 270 || module == RegexpSinglelineJavaCheck.class) { 271 checkstyleMessages.addAll(getCheckMessageKeysFields(Class 272 .forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector"))); 273 } 274 return checkstyleMessages; 275 } 276 catch (ClassNotFoundException exc) { 277 final String message = String.format(Locale.ROOT, "Couldn't find class: %s", 278 module.getName()); 279 throw new MacroExecutionException(message, exc); 280 } 281 } 282 283 /** 284 * Returns the value of the given field. 285 * 286 * @param field the field. 287 * @param instance the instance of the module. 288 * @return the value of the field. 289 * @throws MacroExecutionException if the value could not be retrieved. 290 */ 291 public static Object getFieldValue(Field field, Object instance) 292 throws MacroExecutionException { 293 try { 294 Object fieldValue = null; 295 296 if (field != null) { 297 // required for package/private classes 298 field.trySetAccessible(); 299 fieldValue = field.get(instance); 300 } 301 302 return fieldValue; 303 } 304 catch (IllegalAccessException exc) { 305 throw new MacroExecutionException("Couldn't get field value", exc); 306 } 307 } 308 309 /** 310 * Returns the instance of the module with the given name. 311 * 312 * @param moduleName the name of the module. 313 * @return the instance of the module. 314 * @throws MacroExecutionException if the module could not be created. 315 */ 316 public static Object getModuleInstance(String moduleName) throws MacroExecutionException { 317 final ModuleFactory factory = getPackageObjectFactory(); 318 try { 319 return factory.createModule(moduleName); 320 } 321 catch (CheckstyleException exc) { 322 throw new MacroExecutionException("Couldn't find class: " + moduleName, exc); 323 } 324 } 325 326 /** 327 * Returns the default PackageObjectFactory with the default package names. 328 * 329 * @return the default PackageObjectFactory. 330 * @throws MacroExecutionException if the PackageObjectFactory cannot be created. 331 */ 332 private static PackageObjectFactory getPackageObjectFactory() throws MacroExecutionException { 333 try { 334 final ClassLoader cl = ViolationMessagesMacro.class.getClassLoader(); 335 final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl); 336 return new PackageObjectFactory(packageNames, cl); 337 } 338 catch (CheckstyleException exc) { 339 throw new MacroExecutionException("Couldn't load checkstyle modules", exc); 340 } 341 } 342 343 /** 344 * Construct a string with a leading newline character and followed by 345 * the given amount of spaces. We use this method only to match indentation in 346 * regular xdocs and have minimal diff when parsing the templates. 347 * This method exists until 348 * <a href="https://github.com/checkstyle/checkstyle/issues/13426">13426</a> 349 * 350 * @param amountOfSpaces the amount of spaces to add after the newline. 351 * @return the constructed string. 352 */ 353 public static String getNewlineAndIndentSpaces(int amountOfSpaces) { 354 return System.lineSeparator() + WHITESPACE.repeat(amountOfSpaces); 355 } 356 357 /** 358 * Returns path to the template for the given module name or throws an exception if the 359 * template cannot be found. 360 * 361 * @param moduleName the module whose template we are looking for. 362 * @return path to the template. 363 * @throws MacroExecutionException if the template cannot be found. 364 */ 365 public static Path getTemplatePath(String moduleName) throws MacroExecutionException { 366 final String fileNamePattern = ".*[\\\\/]" 367 + moduleName.toLowerCase(Locale.ROOT) + "\\..*"; 368 return getXdocsTemplatesFilePaths() 369 .stream() 370 .filter(path -> path.toString().matches(fileNamePattern)) 371 .findFirst() 372 .orElse(null); 373 } 374 375 /** 376 * Gets xdocs template file paths. These are files ending with .xml.template. 377 * This method will be changed to gather .xml once 378 * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved. 379 * 380 * @return a set of xdocs template file paths. 381 * @throws MacroExecutionException if an I/O error occurs. 382 */ 383 public static Set<Path> getXdocsTemplatesFilePaths() throws MacroExecutionException { 384 final Path directory = Path.of("src/site/xdoc"); 385 try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE, 386 (path, attr) -> { 387 return attr.isRegularFile() 388 && path.toString().endsWith(TEMPLATE_FILE_EXTENSION); 389 })) { 390 return stream.collect(Collectors.toUnmodifiableSet()); 391 } 392 catch (IOException ioException) { 393 throw new MacroExecutionException("Failed to find xdocs templates", ioException); 394 } 395 } 396 397 /** 398 * Returns the parent module name for the given module class. Returns either 399 * "TreeWalker" or "Checker". Returns null if the module class is null. 400 * 401 * @param moduleClass the module class. 402 * @return the parent module name as a string. 403 * @throws MacroExecutionException if the parent module cannot be found. 404 */ 405 public static String getParentModule(Class<?> moduleClass) 406 throws MacroExecutionException { 407 String parentModuleName = ""; 408 Class<?> parentClass = moduleClass.getSuperclass(); 409 410 while (parentClass != null) { 411 parentModuleName = CLASS_TO_PARENT_MODULE.get(parentClass); 412 if (parentModuleName != null) { 413 break; 414 } 415 parentClass = parentClass.getSuperclass(); 416 } 417 418 // If parent class is not found, check interfaces 419 if (parentModuleName == null || parentModuleName.isEmpty()) { 420 final Class<?>[] interfaces = moduleClass.getInterfaces(); 421 for (Class<?> interfaceClass : interfaces) { 422 parentModuleName = CLASS_TO_PARENT_MODULE.get(interfaceClass); 423 if (parentModuleName != null) { 424 break; 425 } 426 } 427 } 428 if (parentModuleName == null || parentModuleName.isEmpty()) { 429 final String message = String.format(Locale.ROOT, 430 "Failed to find parent module for %s", moduleClass.getSimpleName()); 431 throw new MacroExecutionException(message); 432 } 433 return parentModuleName; 434 } 435 436 /** 437 * Get a set of properties for the given class that should be documented. 438 * 439 * @param clss the class to get the properties for. 440 * @param instance the instance of the module. 441 * @return a set of properties for the given class. 442 */ 443 public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) { 444 final Set<String> properties = 445 getProperties(clss).stream() 446 .filter(prop -> { 447 return !isGlobalProperty(clss, prop) 448 && !isUndocumentedProperty(clss, prop); 449 }) 450 .collect(Collectors.toCollection(HashSet::new)); 451 properties.addAll(getNonExplicitProperties(instance, clss)); 452 return new TreeSet<>(properties); 453 } 454 455 /** 456 * Gets the since version of the module. 457 * 458 * @param moduleClassName name of module class. 459 * @param modulePath module's path. 460 * @return since version of module. 461 * @throws MacroExecutionException if an error occurs during processing. 462 */ 463 public static String getModuleSinceVersion(String moduleClassName, Path modulePath) 464 throws MacroExecutionException { 465 processModule(moduleClassName, modulePath); 466 return JavadocScraperResultUtil.getModuleSinceVersion(); 467 } 468 469 /** 470 * Get the property details of the module. If the property is not present in the 471 * module, then the property details from the superclass(es) is used. 472 * 473 * <p>Superclass property data is built fresh on every call and never cached 474 * statically, to prevent stale data from a previous Maven execution in the 475 * same JVM from corrupting results.</p> 476 * 477 * @param properties the properties of the module. 478 * @param moduleName the name of the module. 479 * @param modulePath the module file path. 480 * @param instance the instance of the module. 481 * @return the property details of the module. 482 * @throws MacroExecutionException if an error occurs during processing. 483 */ 484 public static Map<String, PropertyDetails> buildPropertyDetails(Set<String> properties, 485 String moduleName, Path modulePath, 486 Object instance) 487 throws MacroExecutionException { 488 final Map<String, PropertyDetails> superClassPropertyData = buildSuperClassPropertyData(); 489 processModule(moduleName, modulePath, instance, properties); 490 491 final Map<String, PropertyDetails> currentPropertiesDetails = 492 new TreeMap<>(JavadocScraperResultUtil.getPropertiesDetails()); 493 494 for (String property : properties) { 495 if (!currentPropertiesDetails.containsKey(property)) { 496 processInheritedProperty(currentPropertiesDetails, property, 497 instance, moduleName, superClassPropertyData); 498 } 499 } 500 assertAllPropertiesAreFound(properties, moduleName, currentPropertiesDetails); 501 return Collections.unmodifiableMap(currentPropertiesDetails); 502 } 503 504 /** 505 * Processes an inherited property and adds its details to the provided map. 506 * 507 * @param detailsMap the map to add the property details to. 508 * @param property the name of the property. 509 * @param instance the module instance. 510 * @param moduleName the module name. 511 * @param superClassPropertyData the superclass property data built for this invocation. 512 * @throws MacroExecutionException if an error occurs. 513 */ 514 private static void processInheritedProperty( 515 Map<String, PropertyDetails> detailsMap, 516 String property, Object instance, 517 String moduleName, 518 Map<String, PropertyDetails> superClassPropertyData) 519 throws MacroExecutionException { 520 final String moduleSince = JavadocScraperResultUtil.getModuleSinceVersion(); 521 final PropertyDetails inherited = superClassPropertyData.get(property); 522 if (inherited != null) { 523 final String description = inherited.getDescription(); 524 final String inheritedSince = inherited.getSinceVersion(); 525 526 final String since; 527 if (inheritedSince.isEmpty() 528 || !moduleSince.isEmpty() 529 && isVersionAtLeast(moduleSince, inheritedSince)) { 530 if (moduleSince.isEmpty()) { 531 since = inheritedSince; 532 } 533 else { 534 since = moduleSince; 535 } 536 } 537 else { 538 since = inheritedSince; 539 } 540 final Field field = getField(instance.getClass(), property); 541 final PropertyDetails.Builder builder = new PropertyDetails.Builder() 542 .name(property) 543 .description(description) 544 .sinceVersion(since); 545 detailsMap.put(property, constructPropertyDetails(builder, 546 instance, field, property, moduleName)); 547 } 548 else if (TOKENS.equals(property) 549 || JAVADOC_TOKENS.equals(property) 550 || VIOLATE_EXECUTION_ON_NON_TIGHT_HTML.equals(property)) { 551 final String description = getPropertyDescriptionForXdoc(property, null, 552 moduleName); 553 final String since = getPropertySinceVersion(moduleSince, null); 554 final Field field = getField(instance.getClass(), property); 555 final PropertyDetails.Builder builder = new PropertyDetails.Builder() 556 .name(property) 557 .description(description) 558 .sinceVersion(since); 559 detailsMap.put(property, constructPropertyDetails(builder, 560 instance, field, property, moduleName)); 561 } 562 } 563 564 /** 565 * Assert that each property has a corresponding detail object. 566 * 567 * @param properties the properties of the module. 568 * @param moduleName the name of the module. 569 * @param details the details of the properties of the module. 570 * @throws MacroExecutionException if an error occurs during processing. 571 */ 572 private static void assertAllPropertiesAreFound( 573 Set<String> properties, String moduleName, Map<String, PropertyDetails> details) 574 throws MacroExecutionException { 575 for (String property : properties) { 576 if (!details.containsKey(property)) { 577 throw new MacroExecutionException(String.format(Locale.ROOT, 578 "%s: Missing documentation for property '%s'.", moduleName, property)); 579 } 580 } 581 } 582 583 /** 584 * Builds a fresh map of superclass property data by scraping each superclass file. 585 * This method is called once per {@link #buildPropertyDetails} invocation and returns 586 * a new local map — it never populates any static field. 587 * 588 * @return map of property name to PropertyDetails for all known superclasses. 589 * @throws MacroExecutionException if an error occurs during processing. 590 */ 591 private static Map<String, PropertyDetails> buildSuperClassPropertyData() 592 throws MacroExecutionException { 593 final Map<String, PropertyDetails> result = new TreeMap<>(); 594 for (Path superclassPath : MODULE_SUPER_CLASS_PATHS) { 595 final Path fileNamePath = superclassPath.getFileName(); 596 if (fileNamePath == null) { 597 throw new MacroExecutionException("Invalid superclass path: " + superclassPath); 598 } 599 final String superclassName = CommonUtil.getFileNameWithoutExtension( 600 fileNamePath.toString()); 601 602 final String pathString = superclassPath.toString().replace('\\', '/'); 603 final String marker = "com/puppycrawl/tools/checkstyle/"; 604 final String classPath = pathString.substring(pathString.indexOf(marker)); 605 final String classFullName = classPath 606 .substring(0, classPath.lastIndexOf(".java")) 607 .replace('/', '.'); 608 final Set<String> properties; 609 try { 610 final Class<?> superClass = Class.forName(classFullName); 611 final Set<String> setterProperties = new TreeSet<>(getProperties(superClass)); 612 if (AbstractFileSetCheck.class.isAssignableFrom(superClass)) { 613 setterProperties.add(FILE_EXTENSIONS); 614 } 615 if (AbstractJavadocCheck.class.isAssignableFrom(superClass)) { 616 setterProperties.add(VIOLATE_EXECUTION_ON_NON_TIGHT_HTML); 617 } 618 properties = setterProperties; 619 } 620 catch (ClassNotFoundException exc) { 621 throw new MacroExecutionException("Failed to find class: " + classFullName, exc); 622 } 623 624 processModule(superclassName, superclassPath, null, properties); 625 result.putAll(JavadocScraperResultUtil.getPropertiesDetails()); 626 } 627 return result; 628 } 629 630 /** 631 * Scrape the Javadocs of the class and its properties setters with 632 * ClassAndPropertiesSettersJavadocScraper. 633 * 634 * @param moduleName the name of the module. 635 * @param modulePath the module Path. 636 * @param instance the instance of the module. 637 * @param properties the properties of the module. 638 * @throws MacroExecutionException if an error occurs during processing. 639 */ 640 private static void processModule(String moduleName, Path modulePath, Object instance, 641 Set<String> properties) 642 throws MacroExecutionException { 643 final Path resolvedPath = Path.of("").toAbsolutePath() 644 .resolve(modulePath.toString().replace('\\', '/')) 645 .normalize(); 646 if (!Files.isRegularFile(resolvedPath)) { 647 final String message = String.format(Locale.ROOT, 648 "File %s is not a file. Please check the 'modulePath' property.", modulePath); 649 throw new MacroExecutionException(message); 650 } 651 ClassAndPropertiesSettersJavadocScraper.initialize(moduleName, instance, properties); 652 final Checker checker = new Checker(); 653 checker.setModuleClassLoader(Checker.class.getClassLoader()); 654 final DefaultConfiguration scraperCheckConfig = 655 new DefaultConfiguration( 656 ClassAndPropertiesSettersJavadocScraper.class.getName()); 657 final DefaultConfiguration defaultConfiguration = 658 new DefaultConfiguration("configuration"); 659 final DefaultConfiguration treeWalkerConfig = 660 new DefaultConfiguration(TreeWalker.class.getName()); 661 defaultConfiguration.addProperty(CHARSET, "UTF-8"); 662 defaultConfiguration.addChild(treeWalkerConfig); 663 treeWalkerConfig.addChild(scraperCheckConfig); 664 try { 665 checker.configure(defaultConfiguration); 666 final List<File> filesToProcess = List.of(resolvedPath.toFile()); 667 checker.process(filesToProcess); 668 checker.destroy(); 669 } 670 catch (CheckstyleException checkstyleException) { 671 final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName); 672 throw new MacroExecutionException(message, checkstyleException); 673 } 674 } 675 676 /** 677 * Scrape the Javadocs of the class and its properties setters. 678 * 679 * @param moduleName the name of the module. 680 * @param modulePath the module Path. 681 * @throws MacroExecutionException if an error occurs during processing. 682 */ 683 public static void processModule(String moduleName, Path modulePath) 684 throws MacroExecutionException { 685 final Object instance = getModuleInstance(moduleName); 686 final Set<String> properties = getPropertiesForDocumentation(instance.getClass(), 687 instance); 688 processModule(moduleName, modulePath, instance, properties); 689 } 690 691 /** 692 * Constructs a PropertyDetails object for the given property. 693 * 694 * @param builder the builder already containing name, description, and since version. 695 * @param instance the instance of the module. 696 * @param field the field of the property. 697 * @param propertyName the name of the property. 698 * @param moduleName the name of the module. 699 * @return the PropertyDetails object. 700 * @throws MacroExecutionException if an error occurs. 701 */ 702 public static PropertyDetails constructPropertyDetails(PropertyDetails.Builder builder, 703 Object instance, Field field, 704 String propertyName, String moduleName) 705 throws MacroExecutionException { 706 if (TOKENS.equals(propertyName)) { 707 configureTokensDetails(builder, (AbstractCheck) instance); 708 } 709 else if (JAVADOC_TOKENS.equals(propertyName)) { 710 configureJavadocTokensDetails(builder, (AbstractJavadocCheck) instance); 711 } 712 else { 713 configureOtherPropertyDetails(builder, instance, field, propertyName, moduleName); 714 } 715 return builder.build(); 716 } 717 718 /** 719 * Configures the tokens details for a property. 720 * 721 * @param builder the property details builder. 722 * @param check the check instance. 723 */ 724 private static void configureTokensDetails(PropertyDetails.Builder builder, 725 AbstractCheck check) { 726 final int[] requiredTokens = check.getRequiredTokens(); 727 final int[] acceptableTokens = check.getAcceptableTokens(); 728 final int[] defaultTokens = check.getDefaultTokens(); 729 final int[] allTokenIds = TokenUtil.getAllTokenIds(); 730 if (requiredTokens.length == 0 731 && Arrays.equals(acceptableTokens, allTokenIds)) { 732 builder.tokenPropertyType(PropertyDetails.TokenPropertyType.TOKEN_SET); 733 } 734 else { 735 builder.tokenPropertyType(PropertyDetails.TokenPropertyType.TOKEN_SUBSET); 736 builder.configurableTokens(getDifference(acceptableTokens, 737 requiredTokens).stream().map(TokenUtil::getTokenName).toList()); 738 } 739 if (Arrays.equals(defaultTokens, allTokenIds)) { 740 builder.defaultValueTokens(List.of(TOKEN_TYPES)); 741 } 742 else { 743 builder.defaultValueTokens(getDifference(defaultTokens, 744 requiredTokens).stream().map(TokenUtil::getTokenName).toList()); 745 } 746 } 747 748 /** 749 * Configures the javadoc tokens details for a property. 750 * 751 * @param builder the property details builder. 752 * @param check the javadoc check instance. 753 */ 754 private static void configureJavadocTokensDetails(PropertyDetails.Builder builder, 755 AbstractJavadocCheck check) { 756 builder.tokenPropertyType(PropertyDetails.TokenPropertyType.JAVADOC_TOKEN_SUBSET); 757 builder.configurableTokens(getDifference(check.getAcceptableJavadocTokens(), 758 check.getRequiredJavadocTokens()).stream() 759 .map(JavadocUtil::getTokenName).toList()); 760 builder.defaultValueTokens(getDifference(check.getDefaultJavadocTokens(), 761 check.getRequiredJavadocTokens()).stream() 762 .map(JavadocUtil::getTokenName).toList()); 763 } 764 765 /** 766 * Configures the details for properties other than tokens and javadoc tokens. 767 * 768 * @param builder the property details builder. 769 * @param instance the module instance. 770 * @param field the field of the property. 771 * @param propertyName the name of the property. 772 * @param moduleName the name of the module. 773 * @throws MacroExecutionException if an error occurs. 774 */ 775 private static void configureOtherPropertyDetails(PropertyDetails.Builder builder, 776 Object instance, Field field, 777 String propertyName, String moduleName) 778 throws MacroExecutionException { 779 final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance); 780 final String type; 781 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) { 782 type = "subset of tokens TokenTypes"; 783 } 784 else { 785 final String rawType = getType(field, propertyName, moduleName, instance); 786 type = simplifyTypeName(rawType); 787 } 788 builder.type(type); 789 790 String defaultValue; 791 if (field != null) { 792 defaultValue = getDefaultValue(propertyName, field, instance, moduleName); 793 } 794 else { 795 final Class<?> propertyClass = getPropertyClass(propertyName, instance); 796 if (propertyClass.isArray()) { 797 defaultValue = EMPTY_CURLY_BRACES; 798 } 799 else { 800 defaultValue = NULL_STR; 801 } 802 } 803 804 if (defaultValue.isEmpty() && fieldClass.isArray()) { 805 defaultValue = EMPTY_CURLY_BRACES; 806 } 807 808 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field) 809 && !EMPTY_CURLY_BRACES.equals(defaultValue)) { 810 builder.defaultValueTokens(Arrays.asList(COMMA_SPACE_PATTERN.split(defaultValue))); 811 } 812 else { 813 builder.defaultValue(defaultValue); 814 } 815 } 816 817 /** 818 * Get a set of properties for the given class. 819 * 820 * @param clss the class to get the properties for. 821 * @return a set of properties for the given class. 822 */ 823 public static Set<String> getProperties(Class<?> clss) { 824 final Set<String> result = new TreeSet<>(); 825 final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss); 826 827 for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { 828 if (propertyDescriptor.getWriteMethod() != null) { 829 result.add(propertyDescriptor.getName()); 830 } 831 } 832 833 return result; 834 } 835 836 /** 837 * Checks if the property is a global property. Global properties come from the base classes 838 * and are common to all checks. For example id, severity, tabWidth, etc. 839 * 840 * @param clss the class of the module. 841 * @param propertyName the name of the property. 842 * @return true if the property is a global property. 843 */ 844 private static boolean isGlobalProperty(Class<?> clss, String propertyName) { 845 return AbstractCheck.class.isAssignableFrom(clss) 846 && CHECK_PROPERTIES.contains(propertyName) 847 || AbstractJavadocCheck.class.isAssignableFrom(clss) 848 && JAVADOC_CHECK_PROPERTIES.contains(propertyName) 849 || AbstractFileSetCheck.class.isAssignableFrom(clss) 850 && FILESET_PROPERTIES.contains(propertyName); 851 } 852 853 /** 854 * Checks if the property is supposed to be documented. 855 * 856 * @param clss the class of the module. 857 * @param propertyName the name of the property. 858 * @return true if the property is supposed to be documented. 859 */ 860 private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) { 861 return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName); 862 } 863 864 /** 865 * Gets properties that are not explicitly captured but should be documented if 866 * certain conditions are met. 867 * 868 * @param instance the instance of the module. 869 * @param clss the class of the module. 870 * @return the non explicit properties. 871 */ 872 private static Set<String> getNonExplicitProperties( 873 Object instance, Class<?> clss) { 874 final Set<String> result = new TreeSet<>(); 875 if (AbstractCheck.class.isAssignableFrom(clss)) { 876 final AbstractCheck check = (AbstractCheck) instance; 877 878 final int[] acceptableTokens = check.getAcceptableTokens(); 879 Arrays.sort(acceptableTokens); 880 final int[] defaultTokens = check.getDefaultTokens(); 881 Arrays.sort(defaultTokens); 882 final int[] requiredTokens = check.getRequiredTokens(); 883 Arrays.sort(requiredTokens); 884 885 if (!Arrays.equals(acceptableTokens, defaultTokens) 886 || !Arrays.equals(acceptableTokens, requiredTokens)) { 887 result.add(TOKENS); 888 } 889 } 890 891 if (AbstractJavadocCheck.class.isAssignableFrom(clss)) { 892 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance; 893 result.add(VIOLATE_EXECUTION_ON_NON_TIGHT_HTML); 894 895 final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens(); 896 Arrays.sort(acceptableJavadocTokens); 897 final int[] defaultJavadocTokens = check.getDefaultJavadocTokens(); 898 Arrays.sort(defaultJavadocTokens); 899 final int[] requiredJavadocTokens = check.getRequiredJavadocTokens(); 900 Arrays.sort(requiredJavadocTokens); 901 902 if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens) 903 || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) { 904 result.add(JAVADOC_TOKENS); 905 } 906 } 907 908 if (AbstractFileSetCheck.class.isAssignableFrom(clss)) { 909 result.add(FILE_EXTENSIONS); 910 } 911 return result; 912 } 913 914 /** 915 * Get the description of the property. 916 * 917 * @param propertyName the name of the property. 918 * @param javadoc the Javadoc of the property setter method. 919 * @param moduleName the name of the module. 920 * @return the description of the property. 921 * @throws MacroExecutionException if the description could not be extracted. 922 */ 923 public static String getPropertyDescriptionForXdoc( 924 String propertyName, DetailNode javadoc, String moduleName) 925 throws MacroExecutionException { 926 final String description; 927 if (TOKENS.equals(propertyName)) { 928 description = "tokens to check"; 929 } 930 else if (JAVADOC_TOKENS.equals(propertyName)) { 931 description = "javadoc tokens to check"; 932 } 933 else if (VIOLATE_EXECUTION_ON_NON_TIGHT_HTML.equals(propertyName)) { 934 description = "Control when to print violations if the Javadoc being" 935 + " examined by this check violates the tight html rules defined at" 936 + " <a href=\"" + CHECKSTYLE_ORG_URL 937 + "writingjavadocchecks.html#Tight-HTML_rules\">" 938 + "Tight-HTML Rules</a>."; 939 } 940 else if (FILE_EXTENSIONS.equals(propertyName)) { 941 description = "Specify the file extensions of the files to process."; 942 } 943 else { 944 final String javadocDescription = 945 getDescriptionFromJavadocForXdoc(javadoc, moduleName); 946 final String descriptionString = SETTER_PATTERN.matcher(javadocDescription) 947 .replaceFirst(""); 948 949 if (descriptionString.isEmpty()) { 950 description = ""; 951 } 952 else { 953 final String firstLetterCapitalized = descriptionString.substring(0, 1) 954 .toUpperCase(Locale.ROOT); 955 description = firstLetterCapitalized + descriptionString.substring(1); 956 } 957 } 958 return description; 959 } 960 961 /** 962 * Get the since version of the property. 963 * 964 * <p>Note: the {@code moduleName} parameter has been removed because it was unused. 965 * All call sites have been updated accordingly.</p> 966 * 967 * @param moduleSince the since version of the module. 968 * @param propertyJavadoc the Javadoc of the property setter method. 969 * @return the since version of the property. 970 */ 971 public static String getPropertySinceVersion(String moduleSince, 972 DetailNode propertyJavadoc) { 973 final String sinceVersion; 974 975 final Optional<String> specifiedPropertyVersionInPropertyJavadoc = 976 getPropertyVersionFromItsJavadoc(propertyJavadoc); 977 978 if (specifiedPropertyVersionInPropertyJavadoc.isPresent()) { 979 sinceVersion = specifiedPropertyVersionInPropertyJavadoc.get(); 980 } 981 else { 982 String propertySetterSince = null; 983 if (propertyJavadoc != null) { 984 propertySetterSince = getSinceVersionFromJavadoc(propertyJavadoc); 985 } 986 987 if (propertySetterSince != null 988 && (moduleSince == null || moduleSince.isEmpty() 989 || isVersionAtLeast(propertySetterSince, moduleSince))) { 990 sinceVersion = propertySetterSince; 991 } 992 else { 993 sinceVersion = Optional.ofNullable(moduleSince).orElse(""); 994 } 995 } 996 997 return sinceVersion; 998 } 999 1000 /** 1001 * Extract the property since version from its Javadoc. 1002 * 1003 * @param propertyJavadoc the property Javadoc to extract the since version from. 1004 * @return the Optional of property version specified in its javadoc. 1005 */ 1006 private static Optional<String> getPropertyVersionFromItsJavadoc(DetailNode propertyJavadoc) { 1007 Optional<String> result = Optional.empty(); 1008 1009 if (propertyJavadoc != null) { 1010 final Optional<DetailNode> propertyJavadocTag = 1011 getPropertySinceJavadocTag(propertyJavadoc); 1012 1013 result = propertyJavadocTag 1014 .map(tag -> { 1015 return JavadocUtil.findFirstToken( 1016 tag, JavadocCommentsTokenTypes.DESCRIPTION); 1017 }) 1018 .map(description -> { 1019 return JavadocUtil.findFirstToken( 1020 description, JavadocCommentsTokenTypes.TEXT); 1021 }) 1022 .map(DetailNode::getText) 1023 .map(String::trim); 1024 } 1025 return result; 1026 } 1027 1028 /** 1029 * Find the propertySince Javadoc tag node in the given property Javadoc. 1030 * 1031 * @param javadoc the Javadoc to search. 1032 * @return the Optional of propertySince Javadoc tag node or null if not found. 1033 */ 1034 private static Optional<DetailNode> getPropertySinceJavadocTag(DetailNode javadoc) { 1035 Optional<DetailNode> propertySinceJavadocTag = Optional.empty(); 1036 if (javadoc != null) { 1037 DetailNode child = javadoc.getFirstChild(); 1038 1039 while (child != null) { 1040 if (child.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) { 1041 final DetailNode customBlockTag = JavadocUtil.findFirstToken( 1042 child, JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG); 1043 1044 if (customBlockTag != null 1045 && "propertySince".equals(JavadocUtil.findFirstToken( 1046 customBlockTag, 1047 JavadocCommentsTokenTypes.TAG_NAME).getText())) { 1048 propertySinceJavadocTag = Optional.of(customBlockTag); 1049 break; 1050 } 1051 } 1052 child = child.getNextSibling(); 1053 } 1054 } 1055 return propertySinceJavadocTag; 1056 } 1057 1058 /** 1059 * Gets all javadoc nodes of selected type. 1060 * 1061 * @param allNodes Nodes to choose from. 1062 * @param neededType the Javadoc token type to select. 1063 * @return the List of DetailNodes of selected type. 1064 */ 1065 public static List<DetailNode> getNodesOfSpecificType(DetailNode[] allNodes, int neededType) { 1066 return Arrays.stream(allNodes) 1067 .filter(child -> child.getType() == neededType) 1068 .toList(); 1069 } 1070 1071 /** 1072 * Extract the since version from the Javadoc. 1073 * 1074 * @param javadoc the Javadoc to extract the since version from. 1075 * @return the since version of the setter, or {@code null} if not found. 1076 */ 1077 private static String getSinceVersionFromJavadoc(DetailNode javadoc) { 1078 String result = null; 1079 1080 if (javadoc != null) { 1081 final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc); 1082 result = Optional.ofNullable(sinceJavadocTag) 1083 .map(tag -> { 1084 return JavadocUtil.findFirstToken( 1085 tag, JavadocCommentsTokenTypes.DESCRIPTION); 1086 }) 1087 .map(description -> { 1088 return JavadocUtil.findFirstToken( 1089 description, JavadocCommentsTokenTypes.TEXT); 1090 }) 1091 .map(DetailNode::getText) 1092 .map(String::trim) 1093 .orElse(null); 1094 } 1095 return result; 1096 } 1097 1098 /** 1099 * Find the since Javadoc tag node in the given Javadoc. 1100 * 1101 * @param javadoc the Javadoc to search. 1102 * @return the since Javadoc tag node or null if not found. 1103 */ 1104 private static DetailNode getSinceJavadocTag(DetailNode javadoc) { 1105 DetailNode javadocTagWithSince = null; 1106 1107 if (javadoc != null) { 1108 DetailNode child = javadoc.getFirstChild(); 1109 1110 while (child != null) { 1111 if (child.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) { 1112 final DetailNode sinceNode = JavadocUtil.findFirstToken( 1113 child, JavadocCommentsTokenTypes.SINCE_BLOCK_TAG); 1114 1115 if (sinceNode != null) { 1116 javadocTagWithSince = sinceNode; 1117 break; 1118 } 1119 } 1120 child = child.getNextSibling(); 1121 } 1122 } 1123 1124 return javadocTagWithSince; 1125 } 1126 1127 /** 1128 * Returns {@code true} if {@code actualVersion} >= {@code requiredVersion}. 1129 * Both versions have any trailing "-SNAPSHOT" stripped before comparison. 1130 * 1131 * @param actualVersion e.g. "8.3" or "8.3-SNAPSHOT" 1132 * @param requiredVersion e.g. "8.3" 1133 * @return {@code true} if actualVersion exists, and, numerically, is at least requiredVersion 1134 */ 1135 private static boolean isVersionAtLeast(String actualVersion, 1136 String requiredVersion) { 1137 final Version actualVersionParsed = Version.parse(actualVersion); 1138 final Version requiredVersionParsed = Version.parse(requiredVersion); 1139 1140 return actualVersionParsed.compareTo(requiredVersionParsed) >= 0; 1141 } 1142 1143 /** 1144 * Get the type of the property. 1145 * 1146 * @param field the field to get the type of. 1147 * @param propertyName the name of the property. 1148 * @param moduleName the name of the module. 1149 * @param instance the instance of the module. 1150 * @return the type of the property. 1151 * @throws MacroExecutionException if an error occurs during getting the type. 1152 */ 1153 public static String getType(Field field, String propertyName, 1154 String moduleName, Object instance) 1155 throws MacroExecutionException { 1156 final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance); 1157 return Optional.ofNullable(field) 1158 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class)) 1159 .filter(propertyType -> propertyType.value() != PropertyType.TOKEN_ARRAY) 1160 .map(propertyType -> propertyType.value().getDescription()) 1161 .orElseGet(fieldClass::getTypeName); 1162 } 1163 1164 /** 1165 * Get the default value of the property. 1166 * 1167 * @param propertyName the name of the property. 1168 * @param field the field to get the default value of. 1169 * @param classInstance the instance of the class to get the default value of. 1170 * @param moduleName the name of the module. 1171 * @return the default value of the property. 1172 * @throws MacroExecutionException if an error occurs during getting the default value. 1173 */ 1174 public static String getDefaultValue(String propertyName, Field field, 1175 Object classInstance, String moduleName) 1176 throws MacroExecutionException { 1177 1178 final String result; 1179 if (classInstance instanceof PropertyCacheFile) { 1180 result = "null (no cache file)"; 1181 } 1182 else { 1183 final Object value = getFieldValue(field, classInstance); 1184 final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, 1185 classInstance); 1186 1187 final String fieldValue = getFieldDefaultValue(field, fieldClass, value); 1188 result = Optional.ofNullable(fieldValue).orElse(NULL_STR); 1189 } 1190 1191 return result; 1192 } 1193 1194 /** 1195 * Gets the string representation of a field's default value based on its type. 1196 * Returns {@code null} if the field type is not recognized or the value is null. 1197 * 1198 * @param field the field to get the default value of. 1199 * @param fieldClass the class of the field. 1200 * @param value the current value of the field. 1201 * @return string form of the default value, or {@code null} if unrecognized. 1202 */ 1203 private static String getFieldDefaultValue(Field field, Class<?> fieldClass, Object value) { 1204 String result = getScalarFieldDefaultValue(fieldClass, value); 1205 if (result == null) { 1206 result = getArrayFieldDefaultValue(field, fieldClass, value); 1207 } 1208 return result; 1209 } 1210 1211 /** 1212 * Gets the default value string for scalar (non-array) field types. 1213 * Returns {@code null} if the field class is not a handled scalar type. 1214 * 1215 * @param fieldClass the class of the field. 1216 * @param value the current value of the field. 1217 * @return string form of the default value, or {@code null} if not a scalar type. 1218 */ 1219 private static String getScalarFieldDefaultValue(Class<?> fieldClass, Object value) { 1220 final String result; 1221 if (fieldClass == boolean.class 1222 || fieldClass == int.class 1223 || fieldClass == URI.class 1224 || fieldClass == String.class) { 1225 result = Optional.ofNullable(value).map(Object::toString).orElse(null); 1226 } 1227 else if (fieldClass == Pattern.class) { 1228 result = getPatternDefaultValue(value); 1229 } 1230 else if (fieldClass.isEnum()) { 1231 result = Optional.ofNullable(value) 1232 .map(object -> object.toString().toLowerCase(Locale.ENGLISH)) 1233 .orElse(null); 1234 } 1235 else { 1236 result = null; 1237 } 1238 return result; 1239 } 1240 1241 /** 1242 * Gets the default value string for array field types. 1243 * Returns {@code null} if the field class is not a handled array type. 1244 * 1245 * @param field the field (used for annotation checks). 1246 * @param fieldClass the class of the field. 1247 * @param value the current value of the field. 1248 * @return string form of the default value, or {@code null} if not an array type. 1249 */ 1250 private static String getArrayFieldDefaultValue(Field field, Class<?> fieldClass, 1251 Object value) { 1252 final String result; 1253 1254 if (fieldClass == int[].class 1255 || ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) { 1256 result = getIntArrayPropertyValue(value); 1257 } 1258 else { 1259 result = switch (fieldClass.getSimpleName()) { 1260 case "double[]" -> removeSquareBrackets( 1261 Arrays.toString((double[]) value).replace(".0", "")); 1262 case "String[]" -> getStringArrayPropertyValue(value, 1263 hasPreserveOrderAnnotation(field)); 1264 case "Pattern[]" -> getPatternArrayPropertyValue(value); 1265 case "AccessModifierOption[]" -> getAccessModifierDefaultValue(value); 1266 case null, default -> null; 1267 }; 1268 } 1269 1270 return result; 1271 } 1272 1273 /** 1274 * Gets the string representation of a Pattern field's default value. 1275 * 1276 * @param value the current value of the field. 1277 * @return string form of the Pattern default value, or {@code null} if value is null. 1278 */ 1279 private static String getPatternDefaultValue(Object value) { 1280 final String result; 1281 if (value == null) { 1282 result = null; 1283 } 1284 else { 1285 result = value.toString() 1286 .replace("\n", "\\n") 1287 .replace("\t", "\\t") 1288 .replace("\r", "\\r") 1289 .replace("\f", "\\f"); 1290 } 1291 return result; 1292 } 1293 1294 /** 1295 * Gets the string representation of an AccessModifierOption array field's default value. 1296 * 1297 * @param value the current value of the field. 1298 * @return string form of the default value. 1299 */ 1300 private static String getAccessModifierDefaultValue(Object value) { 1301 final String result; 1302 if (value != null && Array.getLength(value) > 0) { 1303 result = removeSquareBrackets(Arrays.toString((Object[]) value)); 1304 } 1305 else { 1306 result = ""; 1307 } 1308 return result; 1309 } 1310 1311 /** 1312 * Checks if a field has the {@code PreserveOrder} annotation. 1313 * 1314 * @param field the field to check 1315 * @return true if the field has {@code PreserveOrder} annotation, false otherwise 1316 */ 1317 private static boolean hasPreserveOrderAnnotation(Field field) { 1318 return field != null && field.isAnnotationPresent(PreserveOrder.class); 1319 } 1320 1321 /** 1322 * Gets the name of the bean property's default value for the Pattern array class. 1323 * 1324 * @param fieldValue The bean property's value 1325 * @return String form of property's default value 1326 */ 1327 private static String getPatternArrayPropertyValue(Object fieldValue) { 1328 Object value = fieldValue; 1329 if (value instanceof Collection<?> collection) { 1330 value = collection.stream() 1331 .map(Pattern.class::cast) 1332 .toArray(Pattern[]::new); 1333 } 1334 1335 String result = ""; 1336 if (value != null && Array.getLength(value) > 0) { 1337 result = removeSquareBrackets( 1338 Arrays.stream((Pattern[]) value) 1339 .map(Pattern::pattern) 1340 .collect(Collectors.joining(COMMA_SPACE))); 1341 } 1342 1343 return result; 1344 } 1345 1346 /** 1347 * Removes square brackets [ and ] from the given string. 1348 * 1349 * @param value the string to remove square brackets from. 1350 * @return the string without square brackets. 1351 */ 1352 private static String removeSquareBrackets(String value) { 1353 return value 1354 .replace("[", "") 1355 .replace("]", ""); 1356 } 1357 1358 /** 1359 * Gets the name of the bean property's default value for the string array class. 1360 * 1361 * @param value The bean property's value 1362 * @param preserveOrder whether to preserve the original order 1363 * @return String form of property's default value 1364 */ 1365 private static String getStringArrayPropertyValue(Object value, boolean preserveOrder) { 1366 final String result; 1367 if (value == null) { 1368 result = ""; 1369 } 1370 else { 1371 try (Stream<?> valuesStream = getValuesStream(value)) { 1372 final List<String> stringList = valuesStream 1373 .map(String.class::cast) 1374 .collect(Collectors.toCollection(ArrayList<String>::new)); 1375 1376 if (preserveOrder) { 1377 result = String.join(COMMA_SPACE, stringList); 1378 } 1379 else { 1380 result = stringList.stream() 1381 .sorted() 1382 .collect(Collectors.joining(COMMA_SPACE)); 1383 } 1384 } 1385 } 1386 return result; 1387 } 1388 1389 /** 1390 * Generates a stream of values from the given value. 1391 * 1392 * @param value the value to generate the stream from. 1393 * @return the stream of values. 1394 */ 1395 private static Stream<?> getValuesStream(Object value) { 1396 final Stream<?> valuesStream; 1397 if (value instanceof Collection<?> collection) { 1398 valuesStream = collection.stream(); 1399 } 1400 else { 1401 final Object[] array = (Object[]) value; 1402 valuesStream = Arrays.stream(array); 1403 } 1404 return valuesStream; 1405 } 1406 1407 /** 1408 * Returns the name of the bean property's default value for the int array class. 1409 * 1410 * @param value The bean property's value. 1411 * @return String form of property's default value. 1412 */ 1413 private static String getIntArrayPropertyValue(Object value) { 1414 try (IntStream stream = getIntStream(value)) { 1415 return stream 1416 .mapToObj(TokenUtil::getTokenName) 1417 .sorted() 1418 .collect(Collectors.joining(COMMA_SPACE)); 1419 } 1420 } 1421 1422 /** 1423 * Get the int stream from the given value. 1424 * 1425 * @param value the value to get the int stream from. 1426 * @return the int stream. 1427 * @throws IllegalArgumentException if parameter is null. 1428 */ 1429 private static IntStream getIntStream(Object value) { 1430 return switch (value) { 1431 case null -> throw new IllegalArgumentException("value is null"); 1432 case Collection<?> collection -> collection.stream() 1433 .mapToInt(Integer.class::cast); 1434 case BitSet set -> set.stream(); 1435 default -> Arrays.stream((int[]) value); 1436 }; 1437 } 1438 1439 /** 1440 * Gets the class of the given field. 1441 * 1442 * @param field the field to get the class of. 1443 * @param propertyName the name of the property. 1444 * @param moduleName the name of the module. 1445 * @param instance the instance of the module. 1446 * @return the class of the field. 1447 * @throws MacroExecutionException if an error occurs during getting the class. 1448 */ 1449 // -@cs[CyclomaticComplexity] Splitting would not make the code more readable 1450 // -@cs[ForbidWildcardAsReturnType] Implied by design to return different types 1451 public static Class<?> getFieldClass(Field field, String propertyName, 1452 String moduleName, Object instance) 1453 throws MacroExecutionException { 1454 Class<?> result = null; 1455 1456 if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD 1457 .contains(moduleName + DOT + propertyName)) { 1458 result = getPropertyClass(propertyName, instance); 1459 } 1460 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) { 1461 result = String[].class; 1462 } 1463 if (field != null && result == null) { 1464 result = field.getType(); 1465 } 1466 1467 if (result == null) { 1468 throw new MacroExecutionException( 1469 "Could not find field " + propertyName + " in class " + moduleName); 1470 } 1471 1472 if (field != null && (result == List.class || result == Set.class)) { 1473 result = getParameterizedTypeClass(field); 1474 } 1475 else if (result == BitSet.class) { 1476 result = int[].class; 1477 } 1478 1479 return result; 1480 } 1481 1482 /** 1483 * Gets the class of the parameterized type for the given field. 1484 * 1485 * @param field the field to get the parameterized type class of. 1486 * @return the class of the parameterized type. 1487 * @throws MacroExecutionException if an error occurs. 1488 */ 1489 private static Class<?> getParameterizedTypeClass(Field field) throws MacroExecutionException { 1490 final ParameterizedType type = (ParameterizedType) field.getGenericType(); 1491 final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0]; 1492 final Class<?> result; 1493 1494 if (parameterClass == Integer.class) { 1495 result = int[].class; 1496 } 1497 else if (parameterClass == String.class) { 1498 result = String[].class; 1499 } 1500 else if (parameterClass == Pattern.class) { 1501 result = Pattern[].class; 1502 } 1503 else { 1504 final String message = "Unknown parameterized type: " 1505 + parameterClass.getSimpleName(); 1506 throw new MacroExecutionException(message); 1507 } 1508 return result; 1509 } 1510 1511 /** 1512 * Gets the class of the given java property. 1513 * 1514 * @param propertyName the name of the property. 1515 * @param instance the instance of the module. 1516 * @return the class of the java property. 1517 * @throws MacroExecutionException if an error occurs during getting the class. 1518 */ 1519 // -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field 1520 public static Class<?> getPropertyClass(String propertyName, Object instance) 1521 throws MacroExecutionException { 1522 final Class<?> result; 1523 try { 1524 final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance, 1525 propertyName); 1526 result = descriptor.getPropertyType(); 1527 } 1528 catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) { 1529 throw new MacroExecutionException("Failed to retrieve property type", exc); 1530 } 1531 return result; 1532 } 1533 1534 /** 1535 * Get the difference between two lists of tokens. 1536 * 1537 * @param tokens the list of tokens to remove from. 1538 * @param subtractions the tokens to remove. 1539 * @return the difference between the two lists. 1540 */ 1541 public static List<Integer> getDifference(int[] tokens, int... subtractions) { 1542 final Set<Integer> subtractionsSet = Arrays.stream(subtractions) 1543 .boxed() 1544 .collect(Collectors.toUnmodifiableSet()); 1545 return Arrays.stream(tokens) 1546 .boxed() 1547 .filter(token -> !subtractionsSet.contains(token)) 1548 .toList(); 1549 } 1550 1551 /** 1552 * Gets the field with the given name from the given class. 1553 * 1554 * @param fieldClass the class to get the field from. 1555 * @param propertyName the name of the field. 1556 * @return the field we are looking for. 1557 */ 1558 public static Field getField(Class<?> fieldClass, String propertyName) { 1559 Field result = null; 1560 Class<?> currentClass = fieldClass; 1561 1562 while (currentClass != Object.class) { 1563 try { 1564 result = currentClass.getDeclaredField(propertyName); 1565 result.trySetAccessible(); 1566 break; 1567 } 1568 catch (NoSuchFieldException ignored) { 1569 currentClass = currentClass.getSuperclass(); 1570 } 1571 } 1572 1573 return result; 1574 } 1575 1576 /** 1577 * Constructs string with relative link to the provided document. 1578 * 1579 * @param moduleName the name of the module. 1580 * @param document the path of the document. 1581 * @return relative link to the document. 1582 * @throws MacroExecutionException if link to the document cannot be constructed. 1583 */ 1584 public static String getLinkToDocument(String moduleName, String document) 1585 throws MacroExecutionException { 1586 final Path templatePath = getTemplatePath(FINAL_CHECK.matcher(moduleName).replaceAll("")); 1587 if (templatePath == null) { 1588 throw new MacroExecutionException( 1589 String.format(Locale.ROOT, 1590 "Could not find template for %s", moduleName)); 1591 } 1592 final Path templatePathParent = templatePath.getParent(); 1593 if (templatePathParent == null) { 1594 throw new MacroExecutionException("Failed to get parent path for " + templatePath); 1595 } 1596 return templatePathParent 1597 .relativize(Path.of(SRC, "site/xdoc", document)) 1598 .toString() 1599 .replace(".xml", ".html") 1600 .replace('\\', '/'); 1601 } 1602 1603 /** 1604 * Get all templates whose content contains properties macro. 1605 * 1606 * @return templates whose content contains properties macro. 1607 * @throws CheckstyleException if file could not be read. 1608 * @throws MacroExecutionException if template file is not found. 1609 */ 1610 public static List<Path> getTemplatesThatContainPropertiesMacro() 1611 throws CheckstyleException, MacroExecutionException { 1612 final List<Path> result = new ArrayList<>(); 1613 final Set<Path> templatesPaths = getXdocsTemplatesFilePaths(); 1614 for (Path templatePath: templatesPaths) { 1615 final String content = getFileContents(templatePath); 1616 final String propertiesMacroDefinition = "<macro name=\"properties\""; 1617 if (content.contains(propertiesMacroDefinition)) { 1618 result.add(templatePath); 1619 } 1620 } 1621 return result; 1622 } 1623 1624 /** 1625 * Get file contents as string. 1626 * 1627 * @param pathToFile path to file. 1628 * @return file contents as string. 1629 * @throws CheckstyleException if file could not be read. 1630 */ 1631 private static String getFileContents(Path pathToFile) throws CheckstyleException { 1632 final String content; 1633 try { 1634 content = Files.readString(pathToFile); 1635 } 1636 catch (IOException ioException) { 1637 final String message = String.format(Locale.ROOT, "Failed to read file: %s", 1638 pathToFile); 1639 throw new CheckstyleException(message, ioException); 1640 } 1641 return content; 1642 } 1643 1644 /** 1645 * Get the module name from the file. The module name is the file name without the extension. 1646 * 1647 * @param file file to extract the module name from. 1648 * @return module name. 1649 */ 1650 public static String getModuleName(File file) { 1651 final String fullFileName = file.getName(); 1652 return CommonUtil.getFileNameWithoutExtension(fullFileName); 1653 } 1654 1655 /** 1656 * Extracts the description from the javadoc detail node. Performs a DFS traversal on the 1657 * detail node and extracts the text nodes. This description is additionally processed to 1658 * fit Xdoc format. 1659 * 1660 * @param javadoc the Javadoc to extract the description from. 1661 * @param moduleName the name of the module. 1662 * @return the description of the setter. 1663 * @throws MacroExecutionException if the description could not be extracted. 1664 */ 1665 // -@cs[NPathComplexity] Splitting would not make the code more readable 1666 // -@cs[CyclomaticComplexity] Splitting would not make the code more readable. 1667 // -@cs[ExecutableStatementCount] Splitting would not make the code more readable. 1668 private static String getDescriptionFromJavadocForXdoc(DetailNode javadoc, String moduleName) 1669 throws MacroExecutionException { 1670 final List<DetailNode> descriptionNodes = getFirstJavadocParagraphNodes(javadoc); 1671 final StringBuilder description = new StringBuilder(128); 1672 1673 if (!descriptionNodes.isEmpty()) { 1674 DetailNode node = descriptionNodes.getFirst(); 1675 final DetailNode endNode = descriptionNodes.getLast(); 1676 1677 final DescriptionTraversalState state = new DescriptionTraversalState(); 1678 1679 while (node != null) { 1680 processDescriptionNode(node, description, state, moduleName); 1681 1682 DetailNode toVisit = node.getFirstChild(); 1683 while (node != endNode && toVisit == null) { 1684 toVisit = node.getNextSibling(); 1685 node = node.getParent(); 1686 } 1687 1688 node = toVisit; 1689 } 1690 } 1691 1692 return description.toString().trim(); 1693 } 1694 1695 /** 1696 * Processes a single node during description extraction and updates the state. 1697 * Delegates href-attribute handling and non-href node handling to separate helpers 1698 * to keep cyclomatic complexity within limits. 1699 * 1700 * @param node the current node being visited. 1701 * @param description the description buffer to append to. 1702 * @param state the mutable traversal state. 1703 * @param moduleName the name of the module (used for internal link resolution). 1704 * @throws MacroExecutionException if an internal link cannot be resolved. 1705 */ 1706 private static void processDescriptionNode(DetailNode node, 1707 StringBuilder description, 1708 DescriptionTraversalState state, 1709 String moduleName) 1710 throws MacroExecutionException { 1711 if (node.getType() == JavadocCommentsTokenTypes.TAG_ATTR_NAME 1712 && "href".equals(node.getText())) { 1713 state.inHrefAttribute = true; 1714 } 1715 if (state.inHrefAttribute && node.getType() 1716 == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE) { 1717 processHrefAttributeValue(node, description, state, moduleName); 1718 } 1719 else { 1720 processNonHrefNode(node, description, state); 1721 } 1722 } 1723 1724 /** 1725 * Handles an ATTRIBUTE_VALUE node that belongs to an href attribute. 1726 * 1727 * @param node the ATTRIBUTE_VALUE node. 1728 * @param description the description buffer to append to. 1729 * @param state the mutable traversal state. 1730 * @param moduleName the name of the module (used for internal link resolution). 1731 * @throws MacroExecutionException if an internal link cannot be resolved. 1732 */ 1733 private static void processHrefAttributeValue(DetailNode node, 1734 StringBuilder description, 1735 DescriptionTraversalState state, 1736 String moduleName) 1737 throws MacroExecutionException { 1738 final String href = node.getText(); 1739 if (href.contains(CHECKSTYLE_ORG_URL)) { 1740 final String internalHref = href.replace(CHECKSTYLE_ORG_URL, ""); 1741 final String path = internalHref.substring(1, internalHref.length() - 1); 1742 final String relativeHref = getLinkToDocument(moduleName, path); 1743 1744 description.append('\"').append(relativeHref).append('\"'); 1745 } 1746 else { 1747 description.append(href); 1748 } 1749 state.inHrefAttribute = false; 1750 } 1751 1752 /** 1753 * Handles all nodes that are not an href ATTRIBUTE_VALUE, updating HTML-element 1754 * tracking, text content, and inline-tag (code/literal) tracking. 1755 * 1756 * @param node the current node. 1757 * @param description the description buffer to append to. 1758 * @param state the mutable traversal state. 1759 */ 1760 private static void processNonHrefNode(DetailNode node, 1761 StringBuilder description, 1762 DescriptionTraversalState state) { 1763 processHtmlElementTracking(node, description, state); 1764 processTextContent(node, description, state); 1765 processInlineTagTracking(node, description, state); 1766 } 1767 1768 /** 1769 * Updates HTML-element open/close tracking and appends closing tag text. 1770 * 1771 * @param node the current node. 1772 * @param description the description buffer to append to. 1773 * @param state the mutable traversal state. 1774 */ 1775 private static void processHtmlElementTracking(DetailNode node, 1776 StringBuilder description, 1777 DescriptionTraversalState state) { 1778 if (node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) { 1779 state.inHtmlElement = true; 1780 } 1781 if (node.getType() == JavadocCommentsTokenTypes.TAG_CLOSE 1782 && node.getParent().getType() 1783 == JavadocCommentsTokenTypes.HTML_TAG_END) { 1784 description.append(node.getText()); 1785 state.inHtmlElement = false; 1786 } 1787 } 1788 1789 /** 1790 * Appends text content from the node, escaping special characters when inside 1791 * a {@code @code} or {@code @literal} inline tag. 1792 * 1793 * @param node the current node. 1794 * @param description the description buffer to append to. 1795 * @param state the mutable traversal state. 1796 */ 1797 private static void processTextContent(DetailNode node, 1798 StringBuilder description, 1799 DescriptionTraversalState state) { 1800 if (isTextContent(node, state.inHtmlElement)) { 1801 if (state.inCodeLiteral || state.inLiteralTag) { 1802 description.append(node.getText().trim() 1803 .replace("&", "&") 1804 .replace("<", "<") 1805 .replace(">", ">")); 1806 } 1807 else { 1808 description.append(node.getText()); 1809 } 1810 } 1811 } 1812 1813 /** 1814 * Updates {@code @code} and {@code @literal} inline-tag tracking and appends 1815 * the opening/closing {@code <code>} HTML tags as needed. 1816 * 1817 * @param node the current node. 1818 * @param description the description buffer to append to. 1819 * @param state the mutable traversal state. 1820 */ 1821 private static void processInlineTagTracking(DetailNode node, 1822 StringBuilder description, 1823 DescriptionTraversalState state) { 1824 if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME 1825 && node.getParent().getType() 1826 == JavadocCommentsTokenTypes.CODE_INLINE_TAG) { 1827 state.inCodeLiteral = true; 1828 description.append("<code>"); 1829 } 1830 if (state.inCodeLiteral 1831 && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) { 1832 state.inCodeLiteral = false; 1833 description.append("</code>"); 1834 } 1835 if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME 1836 && node.getParent().getType() 1837 == JavadocCommentsTokenTypes.LITERAL_INLINE_TAG) { 1838 state.inLiteralTag = true; 1839 } 1840 if (state.inLiteralTag 1841 && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) { 1842 state.inLiteralTag = false; 1843 } 1844 } 1845 1846 /** 1847 * Checks whether the node contains text content that should be written to the description. 1848 * 1849 * @param node the node to check. 1850 * @param isInHtmlElement whether we are inside an HTML element. 1851 * @return true if the node contains text content to write. 1852 */ 1853 private static boolean isTextContent(DetailNode node, boolean isInHtmlElement) { 1854 return node.getType() == JavadocCommentsTokenTypes.TEXT 1855 || isInHtmlElement && node.getFirstChild() == null 1856 && node.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK; 1857 } 1858 1859 /** 1860 * Get 1st paragraph from the Javadoc with no additional processing. 1861 * 1862 * @param javadoc the Javadoc to extract first paragraph from. 1863 * @return first paragraph of javadoc. 1864 */ 1865 public static String getFirstParagraphFromJavadoc(DetailNode javadoc) { 1866 final String result; 1867 final List<DetailNode> firstParagraphNodes = getFirstJavadocParagraphNodes(javadoc); 1868 if (firstParagraphNodes.isEmpty()) { 1869 result = ""; 1870 } 1871 else { 1872 final DetailNode startNode = firstParagraphNodes.getFirst(); 1873 final DetailNode endNode = firstParagraphNodes.getLast(); 1874 result = JavadocMetadataScraperUtil.constructSubTreeText(startNode, endNode); 1875 } 1876 return result; 1877 } 1878 1879 /** 1880 * Extracts first paragraph nodes from javadoc. 1881 * 1882 * @param javadoc the Javadoc to extract the description from. 1883 * @return the first paragraph nodes of the setter. 1884 */ 1885 public static List<DetailNode> getFirstJavadocParagraphNodes(DetailNode javadoc) { 1886 final List<DetailNode> firstParagraphNodes = new ArrayList<>(); 1887 1888 if (javadoc != null) { 1889 for (DetailNode child = javadoc.getFirstChild(); 1890 child != null; child = child.getNextSibling()) { 1891 if (isEndOfFirstJavadocParagraph(child)) { 1892 break; 1893 } 1894 firstParagraphNodes.add(child); 1895 } 1896 } 1897 return firstParagraphNodes; 1898 } 1899 1900 /** 1901 * Determines if the given child index is the end of the first Javadoc paragraph. The end 1902 * of the description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK, 1903 * NEWLINE, LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the 1904 * one below this line. 1905 * 1906 * @param child the child to check. 1907 * @return true if the given child index is the end of the first javadoc paragraph. 1908 */ 1909 public static boolean isEndOfFirstJavadocParagraph(DetailNode child) { 1910 final DetailNode nextSibling = child.getNextSibling(); 1911 boolean result = false; 1912 if (nextSibling != null) { 1913 final DetailNode secondNextSibling = nextSibling.getNextSibling(); 1914 if (secondNextSibling != null) { 1915 final DetailNode thirdNextSibling = secondNextSibling.getNextSibling(); 1916 if (thirdNextSibling != null) { 1917 result = child.getType() == JavadocCommentsTokenTypes.NEWLINE 1918 && nextSibling.getType() 1919 == JavadocCommentsTokenTypes.LEADING_ASTERISK 1920 && secondNextSibling.getType() 1921 == JavadocCommentsTokenTypes.NEWLINE 1922 && thirdNextSibling.getType() 1923 == JavadocCommentsTokenTypes.LEADING_ASTERISK; 1924 } 1925 } 1926 } 1927 return result; 1928 } 1929 1930 /** 1931 * Simplifies type name just to the name of the class, rather than entire package. 1932 * 1933 * @param fullTypeName full type name. 1934 * @return simplified type name, that is, name of the class. 1935 */ 1936 public static String simplifyTypeName(String fullTypeName) { 1937 final int simplifiedStartIndex; 1938 1939 if (fullTypeName.contains("$")) { 1940 simplifiedStartIndex = fullTypeName.lastIndexOf('$') + 1; 1941 } 1942 else { 1943 simplifiedStartIndex = fullTypeName.lastIndexOf('.') + 1; 1944 } 1945 1946 return fullTypeName.substring(simplifiedStartIndex); 1947 } 1948 1949 /** 1950 * Mutable state bag used during DFS traversal in 1951 * {@link #getDescriptionFromJavadocForXdoc(DetailNode, String)}. 1952 * Extracting these flags into a dedicated class reduces the cyclomatic complexity 1953 * of the traversal method without changing any logic. 1954 */ 1955 private static final class DescriptionTraversalState { 1956 /** Whether we are currently inside a {@code @code ...} inline tag. */ 1957 private boolean inCodeLiteral; 1958 /** Whether we are currently inside a {@code {@literal ...}} inline tag. */ 1959 private boolean inLiteralTag; 1960 /** Whether we are currently inside an HTML element. */ 1961 private boolean inHtmlElement; 1962 /** Whether the next ATTRIBUTE_VALUE token is the value of an href attribute. */ 1963 private boolean inHrefAttribute; 1964 } 1965}