001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.site; 021 022import java.io.IOException; 023import java.nio.file.FileVisitResult; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.nio.file.SimpleFileVisitor; 027import java.nio.file.attribute.BasicFileAttributes; 028import java.util.ArrayList; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.TreeMap; 033import java.util.regex.Pattern; 034 035import javax.annotation.Nullable; 036 037import org.apache.maven.doxia.macro.AbstractMacro; 038import org.apache.maven.doxia.macro.Macro; 039import org.apache.maven.doxia.macro.MacroExecutionException; 040import org.apache.maven.doxia.macro.MacroRequest; 041import org.apache.maven.doxia.sink.Sink; 042import org.codehaus.plexus.component.annotations.Component; 043 044import com.puppycrawl.tools.checkstyle.api.DetailNode; 045import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 046 047/** 048 * Macro to generate table rows for all Checkstyle modules. 049 * Includes every Check.java file that has a Javadoc. 050 * Uses href path structure based on src/site/xdoc/checks. 051 */ 052@Component(role = Macro.class, hint = "allCheckSummaries") 053public class AllCheckSummaries extends AbstractMacro { 054 055 /** 056 * Matches HTML anchor tags and captures their inner text. 057 * Used to strip <a> elements while keeping their display text. 058 */ 059 private static final Pattern LINK_PATTERN = Pattern.compile("<a[^>]*>([^<]*)</a>"); 060 061 /** 062 * Matches common HTML tags such as paragraph, div, span, strong, and em. 063 * Used to remove formatting tags from the Javadoc HTML content. 064 */ 065 private static final Pattern TAG_PATTERN = 066 Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>"); 067 068 /** 069 * Matches one or more whitespace characters. 070 * Used to normalize spacing in sanitized text. 071 */ 072 private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+"); 073 074 /** 075 * Matches '&' characters that are not part of a valid HTML entity. 076 */ 077 private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)"); 078 079 /** Path component for source directory. */ 080 private static final String SRC = "src"; 081 082 /** Path component for checks directory. */ 083 private static final String CHECKS = "checks"; 084 085 /** Root path for Java check files. */ 086 private static final Path JAVA_CHECKS_ROOT = Path.of( 087 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS); 088 089 /** Root path for site check XML files. */ 090 private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS); 091 092 /** Maximum line width considering indentation. */ 093 private static final int MAX_LINE_WIDTH = 86; 094 095 /** XML file extension. */ 096 private static final String XML_EXTENSION = ".xml"; 097 098 /** HTML file extension. */ 099 private static final String HTML_EXTENSION = ".html"; 100 101 /** TD opening tag. */ 102 private static final String TD_TAG = "<td>"; 103 104 /** TD closing tag. */ 105 private static final String TD_CLOSE_TAG = "</td>"; 106 107 @Override 108 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 109 final Map<String, String> xmlHrefMap = buildXmlHtmlMap(); 110 final Map<String, CheckInfo> infos = new TreeMap<>(); 111 112 processCheckFiles(infos, xmlHrefMap); 113 114 final StringBuilder normalRows = new StringBuilder(4096); 115 final StringBuilder holderRows = new StringBuilder(512); 116 117 buildTableRows(infos, normalRows, holderRows); 118 119 sink.rawText(normalRows.toString()); 120 121 if (!holderRows.isEmpty()) { 122 appendHolderSection(sink, holderRows); 123 } 124 } 125 126 /** 127 * Scans Java sources and populates info map with modules having Javadoc. 128 * 129 * @param infos map of collected module info 130 * @param xmlHrefMap map of XML-to-HTML hrefs 131 * @throws MacroExecutionException if file walk fails 132 */ 133 private static void processCheckFiles(Map<String, CheckInfo> infos, 134 Map<String, String> xmlHrefMap) 135 throws MacroExecutionException { 136 try { 137 final List<Path> checkFiles = new ArrayList<>(); 138 Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() { 139 @Override 140 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 141 if (isCheckOrHolderFile(file)) { 142 checkFiles.add(file); 143 } 144 return FileVisitResult.CONTINUE; 145 } 146 }); 147 148 checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap)); 149 } 150 catch (IOException | IllegalStateException exception) { 151 throw new MacroExecutionException("Failed to discover checks", exception); 152 } 153 } 154 155 /** 156 * Checks if a path is a Check or Holder Java file. 157 * 158 * @param path the path to check 159 * @return true if the path is a Check or Holder file, false otherwise 160 */ 161 private static boolean isCheckOrHolderFile(Path path) { 162 final boolean result; 163 if (Files.isRegularFile(path)) { 164 final Path fileName = path.getFileName(); 165 if (fileName == null) { 166 result = false; 167 } 168 else { 169 final String name = fileName.toString(); 170 result = name.endsWith("Check.java") || name.endsWith("Holder.java"); 171 } 172 } 173 else { 174 result = false; 175 } 176 return result; 177 } 178 179 /** 180 * Processes a single check class file and extracts metadata. 181 * 182 * @param path the check class file 183 * @param infos map of results 184 * @param xmlHrefMap map of XML hrefs 185 * @throws IllegalArgumentException if macro execution fails 186 */ 187 private static void processCheckFile(Path path, Map<String, CheckInfo> infos, 188 Map<String, String> xmlHrefMap) { 189 try { 190 final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString()); 191 final boolean isHolder = moduleName.endsWith("Holder"); 192 final String simpleName; 193 if (isHolder) { 194 simpleName = moduleName; 195 } 196 else { 197 simpleName = moduleName.substring(0, moduleName.length() - "Check".length()); 198 } 199 final DetailNode javadoc = SiteUtil.getModuleJavadoc(moduleName, path); 200 if (javadoc != null) { 201 final String description = getDescriptionIfPresent(javadoc); 202 if (description != null) { 203 final String summary = createSummary(description); 204 final String category = extractCategory(path); 205 final String href = resolveHref(xmlHrefMap, category, simpleName); 206 addCheckInfo(infos, simpleName, href, summary, isHolder); 207 } 208 } 209 210 } 211 catch (MacroExecutionException exceptionThrown) { 212 throw new IllegalArgumentException(exceptionThrown); 213 } 214 } 215 216 /** 217 * Returns the module description if present and non-empty. 218 * 219 * @param javadoc the parsed Javadoc node 220 * @return the description text, or {@code null} if not present 221 */ 222 @Nullable 223 private static String getDescriptionIfPresent(DetailNode javadoc) { 224 String result = null; 225 final String desc = getModuleDescriptionSafe(javadoc); 226 if (desc != null && !desc.isEmpty()) { 227 result = desc; 228 } 229 return result; 230 } 231 232 /** 233 * Produces a concise, sanitized summary from the full Javadoc description. 234 * 235 * @param description full Javadoc text 236 * @return sanitized first sentence of the description 237 */ 238 private static String createSummary(String description) { 239 return sanitizeAndFirstSentence(description); 240 } 241 242 /** 243 * Extracts category name from the given Java source path. 244 * 245 * @param path source path of the class 246 * @return category name string 247 */ 248 private static String extractCategory(Path path) { 249 return extractCategoryFromJavaPath(path); 250 } 251 252 /** 253 * Adds a new {@link CheckInfo} record to the provided map. 254 * 255 * @param infos map to update 256 * @param simpleName simple class name 257 * @param href documentation href 258 * @param summary short summary of the check 259 * @param isHolder true if the check is a holder module 260 */ 261 private static void addCheckInfo(Map<String, CheckInfo> infos, 262 String simpleName, 263 String href, 264 String summary, 265 boolean isHolder) { 266 infos.put(simpleName, new CheckInfo(simpleName, href, summary, isHolder)); 267 } 268 269 /** 270 * Retrieves Javadoc description node safely. 271 * 272 * @param javadoc DetailNode root 273 * @return module description or null 274 */ 275 @Nullable 276 private static String getModuleDescriptionSafe(DetailNode javadoc) { 277 String result = null; 278 if (javadoc != null) { 279 try { 280 if (ModuleJavadocParsingUtil 281 .getModuleSinceVersionTagStartNode(javadoc) != null) { 282 result = ModuleJavadocParsingUtil.getModuleDescription(javadoc); 283 } 284 } 285 catch (IllegalStateException exception) { 286 result = null; 287 } 288 } 289 return result; 290 } 291 292 /** 293 * Builds HTML rows for both normal and holder check modules. 294 * 295 * @param infos map of collected module info 296 * @param normalRows builder for normal check rows 297 * @param holderRows builder for holder check rows 298 */ 299 private static void buildTableRows(Map<String, CheckInfo> infos, 300 StringBuilder normalRows, 301 StringBuilder holderRows) { 302 appendRows(infos, normalRows, holderRows); 303 finalizeRows(normalRows, holderRows); 304 } 305 306 /** 307 * Iterates over collected check info entries and appends corresponding rows. 308 * 309 * @param infos map of check info entries 310 * @param normalRows builder for normal check rows 311 * @param holderRows builder for holder check rows 312 */ 313 private static void appendRows(Map<String, CheckInfo> infos, 314 StringBuilder normalRows, 315 StringBuilder holderRows) { 316 for (CheckInfo info : infos.values()) { 317 final String row = buildTableRow(info); 318 if (info.isHolder) { 319 holderRows.append(row); 320 } 321 else { 322 normalRows.append(row); 323 } 324 } 325 } 326 327 /** 328 * Removes leading newlines from the generated table row builders. 329 * 330 * @param normalRows builder for normal check rows 331 * @param holderRows builder for holder check rows 332 */ 333 private static void finalizeRows(StringBuilder normalRows, StringBuilder holderRows) { 334 removeLeadingNewline(normalRows); 335 removeLeadingNewline(holderRows); 336 } 337 338 /** 339 * Builds a single table row for a check module. 340 * 341 * @param info check module information 342 * @return the HTML table row as a string 343 */ 344 private static String buildTableRow(CheckInfo info) { 345 final String ind10 = ModuleJavadocParsingUtil.INDENT_LEVEL_10; 346 final String ind12 = ModuleJavadocParsingUtil.INDENT_LEVEL_12; 347 final String ind14 = ModuleJavadocParsingUtil.INDENT_LEVEL_14; 348 final String ind16 = ModuleJavadocParsingUtil.INDENT_LEVEL_16; 349 350 return ind10 + "<tr>" 351 + ind12 + TD_TAG 352 + ind14 353 + "<a href=\"" 354 + info.link 355 + "\">" 356 + ind16 + info.simpleName 357 + ind14 + "</a>" 358 + ind12 + TD_CLOSE_TAG 359 + ind12 + TD_TAG 360 + ind14 + wrapSummary(info.summary) 361 + ind12 + TD_CLOSE_TAG 362 + ind10 + "</tr>"; 363 } 364 365 /** 366 * Removes leading newline characters from a StringBuilder. 367 * 368 * @param builder the StringBuilder to process 369 */ 370 private static void removeLeadingNewline(StringBuilder builder) { 371 while (!builder.isEmpty() && Character.isWhitespace(builder.charAt(0))) { 372 builder.delete(0, 1); 373 } 374 } 375 376 /** 377 * Appends the Holder Checks HTML section. 378 * 379 * @param sink the output sink 380 * @param holderRows the holder rows content 381 */ 382 private static void appendHolderSection(Sink sink, StringBuilder holderRows) { 383 final String holderSection = buildHolderSectionHtml(holderRows); 384 sink.rawText(holderSection); 385 } 386 387 /** 388 * Builds the HTML for the Holder Checks section. 389 * 390 * @param holderRows the holder rows content 391 * @return the complete HTML section as a string 392 */ 393 private static String buildHolderSectionHtml(StringBuilder holderRows) { 394 return ModuleJavadocParsingUtil.INDENT_LEVEL_8 395 + "</table>" 396 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 397 + "</div>" 398 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 399 + "</section>" 400 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 401 + "<section name=\"Holder Checks\">" 402 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 403 + "<p>" 404 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 405 + "These checks aren't normal checks and are usually" 406 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 407 + "associated with a specialized filter to gather" 408 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 409 + "information the filter can't get on its own." 410 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 411 + "</p>" 412 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 413 + "<div class=\"wrapper\">" 414 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 415 + "<table>" 416 + ModuleJavadocParsingUtil.INDENT_LEVEL_10 417 + holderRows; 418 } 419 420 /** 421 * Builds map of XML file names to HTML documentation paths. 422 * 423 * @return map of lowercase check names to hrefs 424 */ 425 private static Map<String, String> buildXmlHtmlMap() { 426 final Map<String, String> map = new TreeMap<>(); 427 if (Files.exists(SITE_CHECKS_ROOT)) { 428 try { 429 final List<Path> xmlFiles = new ArrayList<>(); 430 Files.walkFileTree(SITE_CHECKS_ROOT, new SimpleFileVisitor<>() { 431 @Override 432 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 433 if (isValidXmlFile(file)) { 434 xmlFiles.add(file); 435 } 436 return FileVisitResult.CONTINUE; 437 } 438 }); 439 440 xmlFiles.forEach(path -> addXmlHtmlMapping(path, map)); 441 } 442 catch (IOException ignored) { 443 // ignore 444 } 445 } 446 return map; 447 } 448 449 /** 450 * Checks if a path is a valid XML file for processing. 451 * 452 * @param path the path to check 453 * @return true if the path is a valid XML file, false otherwise 454 */ 455 private static boolean isValidXmlFile(Path path) { 456 final boolean result; 457 if (Files.isRegularFile(path) 458 && path.toString().endsWith(XML_EXTENSION)) { 459 final Path fileName = path.getFileName(); 460 result = fileName != null 461 && !("index" + XML_EXTENSION) 462 .equalsIgnoreCase(fileName.toString()); 463 } 464 else { 465 result = false; 466 } 467 return result; 468 } 469 470 /** 471 * Adds XML-to-HTML mapping entry to map. 472 * 473 * @param path the XML file path 474 * @param map the mapping to update 475 */ 476 private static void addXmlHtmlMapping(Path path, Map<String, String> map) { 477 final Path fileName = path.getFileName(); 478 if (fileName != null) { 479 final String fileNameString = fileName.toString(); 480 final int extensionLength = 4; 481 final String base = fileNameString.substring(0, 482 fileNameString.length() - extensionLength) 483 .toLowerCase(Locale.ROOT); 484 final Path relativePath = SITE_CHECKS_ROOT.relativize(path); 485 final String relativePathString = relativePath.toString(); 486 final String rel = relativePathString 487 .replace('\\', '/') 488 .replace(XML_EXTENSION, HTML_EXTENSION); 489 map.put(base, CHECKS + "/" + rel); 490 } 491 } 492 493 /** 494 * Resolves the href for a given check module. 495 * 496 * @param xmlMap map of XML file names to HTML paths 497 * @param category the category of the check 498 * @param simpleName simple name of the check 499 * @return the resolved href for the check 500 */ 501 private static String resolveHref(Map<String, String> xmlMap, String category, 502 String simpleName) { 503 final String lower = simpleName.toLowerCase(Locale.ROOT); 504 final String href = xmlMap.get(lower); 505 final String result; 506 if (href != null) { 507 result = href + "#" + simpleName; 508 } 509 else { 510 result = String.format(Locale.ROOT, "%s/%s/%s.html#%s", 511 CHECKS, category, lower, simpleName); 512 } 513 return result; 514 } 515 516 /** 517 * Extracts category path from a Java file path. 518 * 519 * @param javaPath the Java source file path 520 * @return the category path extracted from the Java path 521 */ 522 private static String extractCategoryFromJavaPath(Path javaPath) { 523 final Path rel = JAVA_CHECKS_ROOT.relativize(javaPath); 524 final Path parent = rel.getParent(); 525 final String result; 526 if (parent == null) { 527 result = ""; 528 } 529 else { 530 result = parent.toString().replace('\\', '/'); 531 } 532 return result; 533 } 534 535 /** 536 * Sanitizes HTML and extracts first sentence. 537 * 538 * @param html the HTML string to process 539 * @return the sanitized first sentence 540 */ 541 private static String sanitizeAndFirstSentence(String html) { 542 final String result; 543 if (html == null || html.isEmpty()) { 544 result = ""; 545 } 546 else { 547 String cleaned = LINK_PATTERN.matcher(html).replaceAll("$1"); 548 cleaned = TAG_PATTERN.matcher(cleaned).replaceAll(""); 549 cleaned = SPACE_PATTERN.matcher(cleaned).replaceAll(" ").trim(); 550 cleaned = AMP_PATTERN.matcher(cleaned).replaceAll("&"); 551 result = extractFirstSentence(cleaned); 552 } 553 return result; 554 } 555 556 /** 557 * Extracts first sentence from plain text. 558 * 559 * @param text the text to process 560 * @return the first sentence extracted from the text 561 */ 562 private static String extractFirstSentence(String text) { 563 String result = ""; 564 if (text != null && !text.isEmpty()) { 565 int end = -1; 566 for (int index = 0; index < text.length(); index++) { 567 if (text.charAt(index) == '.' 568 && (index == text.length() - 1 569 || Character.isWhitespace(text.charAt(index + 1)) 570 || text.charAt(index + 1) == '<')) { 571 end = index; 572 break; 573 } 574 } 575 if (end == -1) { 576 result = text.trim(); 577 } 578 else { 579 result = text.substring(0, end + 1).trim(); 580 } 581 } 582 return result; 583 } 584 585 /** 586 * Wraps long summaries to avoid exceeding line width. 587 * 588 * @param text the text to wrap 589 * @return the wrapped text 590 */ 591 private static String wrapSummary(String text) { 592 final String result; 593 if (text == null || text.isEmpty()) { 594 result = ""; 595 } 596 else if (text.length() <= MAX_LINE_WIDTH) { 597 result = text; 598 } 599 else { 600 result = performWrapping(text); 601 } 602 return result; 603 } 604 605 /** 606 * Performs wrapping of summary text. 607 * 608 * @param text the text to wrap 609 * @return the wrapped text 610 */ 611 private static String performWrapping(String text) { 612 final int textLength = text.length(); 613 final StringBuilder result = new StringBuilder(textLength + 100); 614 int pos = 0; 615 final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14; 616 boolean firstLine = true; 617 618 while (pos < textLength) { 619 final int end = Math.min(pos + MAX_LINE_WIDTH, textLength); 620 if (end >= textLength) { 621 if (!firstLine) { 622 result.append(indent); 623 } 624 result.append(text.substring(pos)); 625 break; 626 } 627 int breakPos = text.lastIndexOf(' ', end); 628 if (breakPos <= pos) { 629 breakPos = end; 630 } 631 if (!firstLine) { 632 result.append(indent); 633 } 634 result.append(text, pos, breakPos); 635 pos = breakPos + 1; 636 firstLine = false; 637 } 638 return result.toString(); 639 } 640 641 /** 642 * Data holder for each Check module entry. 643 */ 644 private static final class CheckInfo { 645 /** Simple name of the check. */ 646 private final String simpleName; 647 /** Documentation link. */ 648 private final String link; 649 /** Short summary text. */ 650 private final String summary; 651 /** Whether the module is a holder type. */ 652 private final boolean isHolder; 653 654 /** 655 * Constructs an info record. 656 * 657 * @param simpleName check simple name 658 * @param link documentation link 659 * @param summary module summary 660 * @param isHolder whether holder 661 * @noinspection unused 662 * @noinspectionreason moduleName parameter is required for consistent API 663 * but not used in this implementation 664 */ 665 private CheckInfo(String simpleName, String link, 666 String summary, boolean isHolder) { 667 this.simpleName = simpleName; 668 this.link = link; 669 this.summary = summary; 670 this.isHolder = isHolder; 671 } 672 } 673}