View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.checkstyle;
20  
21  import java.io.File;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Locale;
27  
28  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
29  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
30  import com.puppycrawl.tools.checkstyle.api.Configuration;
31  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.maven.doxia.sink.Sink;
34  import org.apache.maven.doxia.tools.SiteTool;
35  import org.apache.maven.doxia.util.DoxiaUtils;
36  import org.apache.maven.plugins.checkstyle.exec.CheckstyleResults;
37  import org.apache.maven.project.MavenProject;
38  import org.apache.maven.reporting.AbstractMavenReportRenderer;
39  import org.codehaus.plexus.i18n.I18N;
40  
41  /**
42   * Generate a report based on CheckstyleResults.
43   *
44   *
45   */
46  public class CheckstyleReportRenderer extends AbstractMavenReportRenderer {
47      private static final int NO_TEXT = 0;
48      private static final int TEXT_SIMPLE = 1;
49      private static final int TEXT_TITLE = 2;
50      private static final int TEXT_ABBREV = 3;
51  
52      private final I18N i18n;
53  
54      private final Locale locale;
55  
56      private final MavenProject project;
57  
58      private final Configuration checkstyleConfig;
59  
60      private final boolean enableRulesSummary;
61  
62      private final boolean enableSeveritySummary;
63  
64      private final boolean enableFilesSummary;
65  
66      private final SiteTool siteTool;
67  
68      private String xrefLocation;
69  
70      private String xrefTestLocation;
71  
72      private List<File> testSourceDirectories = new ArrayList<>();
73  
74      private List<String> treeWalkerNames = Collections.singletonList("TreeWalker");
75  
76      private final String ruleset;
77  
78      private final CheckstyleResults results;
79  
80      public CheckstyleReportRenderer(
81              Sink sink,
82              I18N i18n,
83              Locale locale,
84              MavenProject project,
85              SiteTool siteTool,
86              String ruleset,
87              boolean enableRulesSummary,
88              boolean enableSeveritySummary,
89              boolean enableFilesSummary,
90              CheckstyleResults results) {
91          super(sink);
92          this.i18n = i18n;
93          this.locale = locale;
94          this.project = project;
95          this.siteTool = siteTool;
96          this.ruleset = ruleset;
97          this.enableRulesSummary = enableRulesSummary;
98          this.enableSeveritySummary = enableSeveritySummary;
99          this.enableFilesSummary = enableFilesSummary;
100         this.results = results;
101         this.checkstyleConfig = results.getConfiguration();
102     }
103 
104     @Override
105     public String getTitle() {
106         return getI18nString("title");
107     }
108 
109     /**
110      * @param key The key.
111      * @return The translated string.
112      */
113     private String getI18nString(String key) {
114         return i18n.getString("checkstyle-report", locale, "report.checkstyle." + key);
115     }
116 
117     protected void renderBody() {
118         startSection(getTitle());
119 
120         sink.paragraph();
121         sink.text(getI18nString("checkstylelink") + " ");
122         sink.link("https://checkstyle.org/");
123         sink.text("Checkstyle");
124         sink.link_();
125         String version = getCheckstyleVersion();
126         if (version != null) {
127             sink.text(" ");
128             sink.text(version);
129         }
130         sink.text(" ");
131         sink.text(String.format(getI18nString("ruleset"), ruleset));
132         sink.text(".");
133         sink.paragraph_();
134 
135         renderSeveritySummarySection();
136 
137         renderFilesSummarySection();
138 
139         renderRulesSummarySection();
140 
141         renderDetailsSection();
142 
143         endSection();
144     }
145 
146     /**
147      * Get the value of the specified attribute from the Checkstyle configuration.
148      * If parentConfigurations is non-null and non-empty, the parent
149      * configurations are searched if the attribute cannot be found in the
150      * current configuration. If the attribute is still not found, the
151      * specified default value will be returned.
152      *
153      * @param config The current Checkstyle configuration
154      * @param parentConfiguration The configuration of the parent of the current configuration
155      * @param attributeName The name of the attribute
156      * @param defaultValue The default value to use if the attribute cannot be found in any configuration
157      * @return The value of the specified attribute
158      */
159     private String getConfigAttribute(
160             Configuration config,
161             ChainedItem<Configuration> parentConfiguration,
162             String attributeName,
163             String defaultValue) {
164         String ret;
165         try {
166             ret = config.getAttribute(attributeName);
167         } catch (CheckstyleException e) {
168             // Try to find the attribute in a parent, if there are any
169             if (parentConfiguration != null) {
170                 ret = getConfigAttribute(
171                         parentConfiguration.value, parentConfiguration.parent, attributeName, defaultValue);
172             } else {
173                 ret = defaultValue;
174             }
175         }
176         return ret;
177     }
178 
179     /**
180      * Create the rules summary section of the report.
181      *
182      * @param results The results to summarize
183      */
184     private void renderRulesSummarySection() {
185         if (!enableRulesSummary) {
186             return;
187         }
188         if (checkstyleConfig == null) {
189             return;
190         }
191 
192         startSection(getI18nString("rules"));
193 
194         startTable();
195 
196         tableHeader(new String[] {
197             getI18nString("rule.category"),
198             getI18nString("rule"),
199             getI18nString("violations"),
200             getI18nString("column.severity")
201         });
202 
203         // Top level should be the checker.
204         if ("checker".equalsIgnoreCase(checkstyleConfig.getName())) {
205             String category = null;
206             for (ConfReference ref : sortConfiguration(results)) {
207                 renderRuleRow(ref, results, category);
208 
209                 category = ref.category;
210             }
211         } else {
212             tableRow(new String[] {getI18nString("norule")});
213         }
214 
215         endTable();
216 
217         endSection();
218     }
219 
220     /**
221      * Create a summary for one Checkstyle rule.
222      *
223      * @param ref The configuration reference for the row
224      * @param results The results to summarize
225      * @param previousCategory The previous row's category
226      */
227     private void renderRuleRow(ConfReference ref, CheckstyleResults results, String previousCategory) {
228         Configuration checkerConfig = ref.configuration;
229         ChainedItem<Configuration> parentConfiguration = ref.parentConfiguration;
230         String ruleName = checkerConfig.getName();
231 
232         sink.tableRow();
233 
234         // column 1: rule category
235         sink.tableCell();
236         String category = ref.category;
237         if (!category.equals(previousCategory)) {
238             sink.text(category);
239         }
240         sink.tableCell_();
241 
242         // column 2: Rule name + configured attributes
243         sink.tableCell();
244         if (!"extension".equals(category)) {
245             sink.link("https://checkstyle.org/config_" + category + ".html#" + ruleName);
246             sink.text(ruleName);
247             sink.link_();
248         } else {
249             sink.text(ruleName);
250         }
251 
252         List<String> attribnames = new ArrayList<>(Arrays.asList(checkerConfig.getAttributeNames()));
253         attribnames.remove("severity"); // special value (deserves unique column)
254         if (!attribnames.isEmpty()) {
255             sink.list();
256             for (String name : attribnames) {
257                 sink.listItem();
258 
259                 sink.text(name);
260 
261                 String value = getConfigAttribute(checkerConfig, null, name, "");
262                 // special case, Header.header and RegexpHeader.header
263                 if ("header".equals(name) && ("Header".equals(ruleName) || "RegexpHeader".equals(ruleName))) {
264                     String[] lines = StringUtils.split(value, "\\n");
265                     int linenum = 1;
266                     for (String line : lines) {
267                         sink.lineBreak();
268                         sink.rawText("<span style=\"color: gray\">");
269                         sink.text(linenum + ":");
270                         sink.rawText("</span>");
271                         sink.nonBreakingSpace();
272                         sink.monospaced();
273                         sink.text(line);
274                         sink.monospaced_();
275                         linenum++;
276                     }
277                 } else if ("headerFile".equals(name) && "RegexpHeader".equals(ruleName)) {
278                     sink.text(": ");
279                     sink.monospaced();
280                     sink.text("\"");
281                     // Make the headerFile value relative to ${basedir}
282                     String path =
283                             siteTool.getRelativePath(value, project.getBasedir().getAbsolutePath());
284                     sink.text(path.replace('\\', '/'));
285                     sink.text(value);
286                     sink.text("\"");
287                     sink.monospaced_();
288                 } else {
289                     sink.text(": ");
290                     sink.monospaced();
291                     sink.text("\"");
292                     sink.text(value);
293                     sink.text("\"");
294                     sink.monospaced_();
295                 }
296                 sink.listItem_();
297             }
298             sink.list_();
299         }
300 
301         sink.tableCell_();
302 
303         // column 3: rule violation count
304         sink.tableCell();
305         sink.text(String.valueOf(ref.violations));
306         sink.tableCell_();
307 
308         // column 4: severity
309         sink.tableCell();
310         // Grab the severity from the rule configuration, this time use error as default value
311         // Also pass along all parent configurations, so that we can try to find the severity there
312         String severity = getConfigAttribute(checkerConfig, parentConfiguration, "severity", "error");
313         iconSeverity(severity, TEXT_SIMPLE);
314         sink.tableCell_();
315 
316         sink.tableRow_();
317     }
318 
319     /**
320      * Check if a violation matches a rule.
321      *
322      * @param event the violation to check
323      * @param ruleName The name of the rule
324      * @param expectedMessage A message that, if it's not null, will be matched to the message from the violation
325      * @param expectedSeverity A severity that, if it's not null, will be matched to the severity from the violation
326      * @return The number of rule violations
327      */
328     public boolean matchRule(AuditEvent event, String ruleName, String expectedMessage, String expectedSeverity) {
329         if (!ruleName.equals(RuleUtil.getName(event))) {
330             return false;
331         }
332 
333         // check message too, for those that have a specific one.
334         // like GenericIllegalRegexp and Regexp
335         if (expectedMessage != null) {
336             // event.getMessage() uses java.text.MessageFormat in its implementation.
337             // Read MessageFormat Javadoc about single quote:
338             // http://java.sun.com/j2se/1.4.2/docs/api/java/text/MessageFormat.html
339             String msgWithoutSingleQuote = StringUtils.replace(expectedMessage, "'", "");
340 
341             if (!(expectedMessage.equals(event.getMessage()) || msgWithoutSingleQuote.equals(event.getMessage()))) {
342                 return false;
343             }
344         }
345         // Check the severity. This helps to distinguish between
346         // different configurations for the same rule, where each
347         // configuration has a different severity, like JavadocMethod.
348         // See also https://issues.apache.org/jira/browse/MCHECKSTYLE-41
349         if (expectedSeverity != null) {
350             if (!expectedSeverity.equals(event.getSeverityLevel().getName())) {
351                 return false;
352             }
353         }
354         return true;
355     }
356 
357     private void renderSeveritySummarySection() {
358         if (!enableSeveritySummary) {
359             return;
360         }
361 
362         startSection(getI18nString("summary"));
363 
364         startTable();
365 
366         sink.tableRow();
367         sink.tableHeaderCell();
368         sink.text(getI18nString("files"));
369         sink.tableHeaderCell_();
370 
371         sink.tableHeaderCell();
372         iconSeverity("info", TEXT_TITLE);
373         sink.tableHeaderCell_();
374 
375         sink.tableHeaderCell();
376         iconSeverity("warning", TEXT_TITLE);
377         sink.tableHeaderCell_();
378 
379         sink.tableHeaderCell();
380         iconSeverity("error", TEXT_TITLE);
381         sink.tableHeaderCell_();
382         sink.tableRow_();
383 
384         tableRow(new String[] {
385             String.valueOf(results.getFileCount()),
386             String.valueOf(results.getSeverityCount(SeverityLevel.INFO)),
387             String.valueOf(results.getSeverityCount(SeverityLevel.WARNING)),
388             String.valueOf(results.getSeverityCount(SeverityLevel.ERROR))
389         });
390 
391         endTable();
392 
393         endSection();
394     }
395 
396     private void renderFilesSummarySection() {
397         if (!enableFilesSummary) {
398             return;
399         }
400 
401         startSection(getI18nString("files"));
402 
403         startTable();
404 
405         sink.tableRow();
406         sink.tableHeaderCell();
407         sink.text(getI18nString("file"));
408         sink.tableHeaderCell_();
409         sink.tableHeaderCell();
410         iconSeverity("info", TEXT_ABBREV);
411         sink.tableHeaderCell_();
412         sink.tableHeaderCell();
413         iconSeverity("warning", TEXT_ABBREV);
414         sink.tableHeaderCell_();
415         sink.tableHeaderCell();
416         iconSeverity("error", TEXT_ABBREV);
417         sink.tableHeaderCell_();
418         sink.tableRow_();
419 
420         // Sort the files before writing them to the report
421         List<String> fileList = new ArrayList<>(results.getFiles().keySet());
422         Collections.sort(fileList);
423 
424         for (String filename : fileList) {
425             List<AuditEvent> violations = results.getFileViolations(filename);
426             if (violations.isEmpty()) {
427                 // skip files without violations
428                 continue;
429             }
430 
431             sink.tableRow();
432 
433             sink.tableCell();
434             sink.link("#" + DoxiaUtils.encodeId(filename));
435             sink.text(filename);
436             sink.link_();
437             sink.tableCell_();
438 
439             sink.tableCell();
440             sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.INFO)));
441             sink.tableCell_();
442 
443             sink.tableCell();
444             sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.WARNING)));
445             sink.tableCell_();
446 
447             sink.tableCell();
448             sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.ERROR)));
449             sink.tableCell_();
450 
451             sink.tableRow_();
452         }
453 
454         endTable();
455 
456         endSection();
457     }
458 
459     private void renderDetailsSection() {
460         startSection(getI18nString("details"));
461 
462         // Sort the files before writing their details to the report
463         List<String> fileList = new ArrayList<>(results.getFiles().keySet());
464         Collections.sort(fileList);
465 
466         for (String file : fileList) {
467             List<AuditEvent> violations = results.getFileViolations(file);
468 
469             if (violations.isEmpty()) {
470                 // skip files without violations
471                 continue;
472             }
473 
474             startSection(file);
475 
476             startTable();
477 
478             tableHeader(new String[] {
479                 getI18nString("column.severity"),
480                 getI18nString("rule.category"),
481                 getI18nString("rule"),
482                 getI18nString("column.message"),
483                 getI18nString("column.line")
484             });
485 
486             renderFileEvents(violations, file);
487 
488             endTable();
489 
490             endSection();
491         }
492 
493         endSection();
494     }
495 
496     private void renderFileEvents(List<AuditEvent> eventList, String filename) {
497         for (AuditEvent event : eventList) {
498             SeverityLevel level = event.getSeverityLevel();
499 
500             sink.tableRow();
501 
502             sink.tableCell();
503             iconSeverity(level.getName(), TEXT_SIMPLE);
504             sink.tableCell_();
505 
506             sink.tableCell();
507             String category = RuleUtil.getCategory(event);
508             if (category != null) {
509                 sink.text(category);
510             }
511             sink.tableCell_();
512 
513             sink.tableCell();
514             String ruleName = RuleUtil.getName(event);
515             if (ruleName != null) {
516                 sink.text(ruleName);
517             }
518             sink.tableCell_();
519 
520             sink.tableCell();
521             sink.text(event.getMessage());
522             sink.tableCell_();
523 
524             sink.tableCell();
525 
526             int line = event.getLine();
527             String effectiveXrefLocation = getEffectiveXrefLocation(eventList);
528             if (effectiveXrefLocation != null && line != 0) {
529                 sink.link(effectiveXrefLocation + "/" + filename.replaceAll("\\.java$", ".html") + "#L" + line);
530                 sink.text(String.valueOf(line));
531                 sink.link_();
532             } else if (line != 0) {
533                 sink.text(String.valueOf(line));
534             }
535             sink.tableCell_();
536 
537             sink.tableRow_();
538         }
539     }
540 
541     private String getEffectiveXrefLocation(List<AuditEvent> eventList) {
542         String absoluteFilename = eventList.get(0).getFileName();
543         if (isTestSource(absoluteFilename)) {
544             return getXrefTestLocation();
545         } else {
546             return getXrefLocation();
547         }
548     }
549 
550     private boolean isTestSource(final String absoluteFilename) {
551         for (File testSourceDirectory : testSourceDirectories) {
552             if (absoluteFilename.startsWith(testSourceDirectory.getAbsolutePath())) {
553                 return true;
554             }
555         }
556 
557         return false;
558     }
559 
560     public String getXrefLocation() {
561         return xrefLocation;
562     }
563 
564     public void setXrefLocation(String xrefLocation) {
565         this.xrefLocation = xrefLocation;
566     }
567 
568     public String getXrefTestLocation() {
569         return xrefTestLocation;
570     }
571 
572     public void setXrefTestLocation(String xrefTestLocation) {
573         this.xrefTestLocation = xrefTestLocation;
574     }
575 
576     public void setTestSourceDirectories(List<File> testSourceDirectories) {
577         this.testSourceDirectories = testSourceDirectories;
578     }
579 
580     public void setTreeWalkerNames(List<String> treeWalkerNames) {
581         this.treeWalkerNames = treeWalkerNames;
582     }
583 
584     /**
585      * Render an icon of given level with associated text.
586      * @param level one of <code>INFO</code>, <code>WARNING</code> or <code>ERROR</code> constants
587      * @param textType one of <code>NO_TEXT</code>, <code>TEXT_SIMPLE</code>, <code>TEXT_TITLE</code> or
588      * <code>TEXT_ABBREV</code> constants
589      */
590     private void iconSeverity(String level, int textType) {
591         sink.figureGraphics("images/icon_" + level + "_sml.gif");
592 
593         if (textType > NO_TEXT) {
594             sink.nonBreakingSpace();
595             String suffix;
596             switch (textType) {
597                 case TEXT_TITLE:
598                     suffix = "s";
599                     break;
600                 case TEXT_ABBREV:
601                     suffix = "s.abbrev";
602                     break;
603                 default:
604                     suffix = "";
605             }
606             sink.text(getI18nString(level + suffix));
607         }
608     }
609 
610     /**
611      * Get the effective Checkstyle version at runtime.
612      * @return the MANIFEST implementation version of Checkstyle API package (can be <code>null</code>)
613      */
614     private String getCheckstyleVersion() {
615         Package checkstyleApiPackage = Configuration.class.getPackage();
616 
617         return (checkstyleApiPackage == null) ? null : checkstyleApiPackage.getImplementationVersion();
618     }
619 
620     public List<ConfReference> sortConfiguration(CheckstyleResults results) {
621         List<ConfReference> result = new ArrayList<>();
622 
623         sortConfiguration(result, checkstyleConfig, null, results);
624 
625         Collections.sort(result);
626 
627         return result;
628     }
629 
630     private void sortConfiguration(
631             List<ConfReference> result,
632             Configuration config,
633             ChainedItem<Configuration> parent,
634             CheckstyleResults results) {
635         for (Configuration childConfig : config.getChildren()) {
636             String ruleName = childConfig.getName();
637 
638             if (treeWalkerNames.contains(ruleName)) {
639                 // special sub-case: TreeWalker is the parent of multiple rules, not an effective rule
640                 sortConfiguration(result, childConfig, new ChainedItem<>(config, parent), results);
641             } else {
642                 String fixedmessage = getConfigAttribute(childConfig, null, "message", null);
643                 // Grab the severity from the rule configuration. Do not set default value here as
644                 // it breaks our rule aggregate section entirely.  The counts are off but this is
645                 // not appropriate fix location per MCHECKSTYLE-365.
646                 String configSeverity = getConfigAttribute(childConfig, null, "severity", null);
647 
648                 // count rule violations
649                 long violations = 0;
650                 AuditEvent lastMatchedEvent = null;
651                 for (List<AuditEvent> errors : results.getFiles().values()) {
652                     for (AuditEvent event : errors) {
653                         if (matchRule(event, ruleName, fixedmessage, configSeverity)) {
654                             lastMatchedEvent = event;
655                             violations++;
656                         }
657                     }
658                 }
659 
660                 if (violations > 0) // forget rules without violations
661                 {
662                     String category = RuleUtil.getCategory(lastMatchedEvent);
663 
664                     result.add(new ConfReference(category, childConfig, parent, violations, result.size()));
665                 }
666             }
667         }
668     }
669 
670     private static class ConfReference implements Comparable<ConfReference> {
671         private final String category;
672         private final Configuration configuration;
673         private final ChainedItem<Configuration> parentConfiguration;
674         private final long violations;
675         private final int count;
676 
677         ConfReference(
678                 String category,
679                 Configuration configuration,
680                 ChainedItem<Configuration> parentConfiguration,
681                 long violations,
682                 int count) {
683             this.category = category;
684             this.configuration = configuration;
685             this.parentConfiguration = parentConfiguration;
686             this.violations = violations;
687             this.count = count;
688         }
689 
690         public int compareTo(ConfReference o) {
691             int compare = category.compareTo(o.category);
692             if (compare == 0) {
693                 compare = configuration.getName().compareTo(o.configuration.getName());
694             }
695             return (compare == 0) ? (o.count - count) : compare;
696         }
697     }
698 
699     private static class ChainedItem<T> {
700         private final ChainedItem<T> parent;
701 
702         private final T value;
703 
704         ChainedItem(T value, ChainedItem<T> parent) {
705             this.parent = parent;
706             this.value = value;
707         }
708     }
709 }