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