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