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