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