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