View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.io.IOException;
23  import java.nio.file.FileVisitResult;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.SimpleFileVisitor;
27  import java.nio.file.attribute.BasicFileAttributes;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.TreeMap;
33  import java.util.regex.Pattern;
34  
35  import javax.annotation.Nullable;
36  
37  import org.apache.maven.doxia.macro.AbstractMacro;
38  import org.apache.maven.doxia.macro.Macro;
39  import org.apache.maven.doxia.macro.MacroExecutionException;
40  import org.apache.maven.doxia.macro.MacroRequest;
41  import org.apache.maven.doxia.sink.Sink;
42  import org.codehaus.plexus.component.annotations.Component;
43  
44  import com.puppycrawl.tools.checkstyle.api.DetailNode;
45  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
46  
47  /**
48   * Macro to generate table rows for all Checkstyle modules.
49   * Includes every Check.java file that has a Javadoc.
50   * Uses href path structure based on src/site/xdoc/checks.
51   */
52  @Component(role = Macro.class, hint = "allCheckSummaries")
53  public class AllCheckSummaries extends AbstractMacro {
54  
55      /**
56       * Matches HTML anchor tags and captures their inner text.
57       * Used to strip <a> elements while keeping their display text.
58       */
59      private static final Pattern LINK_PATTERN = Pattern.compile("<a[^>]*>([^<]*)</a>");
60  
61      /**
62       * Matches common HTML tags such as paragraph, div, span, strong, and em.
63       * Used to remove formatting tags from the Javadoc HTML content.
64       */
65      private static final Pattern TAG_PATTERN =
66              Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>");
67  
68      /**
69       * Matches one or more whitespace characters.
70       * Used to normalize spacing in sanitized text.
71       */
72      private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+");
73  
74      /**
75       * Matches '&amp;' characters that are not part of a valid HTML entity.
76       */
77      private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)");
78  
79      /** Path component for source directory. */
80      private static final String SRC = "src";
81  
82      /** Path component for checks directory. */
83      private static final String CHECKS = "checks";
84  
85      /** Root path for Java check files. */
86      private static final Path JAVA_CHECKS_ROOT = Path.of(
87              SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS);
88  
89      /** Root path for site check XML files. */
90      private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS);
91  
92      /** Maximum line width considering indentation. */
93      private static final int MAX_LINE_WIDTH = 86;
94  
95      /** XML file extension. */
96      private static final String XML_EXTENSION = ".xml";
97  
98      /** HTML file extension. */
99      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("&amp;");
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 }