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