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(
246                     "https://checkstyle.org/checks/" + category + "/" + ruleName.toLowerCase(Locale.ENGLISH) + ".html");
247             sink.text(ruleName);
248             sink.link_();
249         } else {
250             sink.text(ruleName);
251         }
252 
253         List<String> attribnames = new ArrayList<>(Arrays.asList(checkerConfig.getAttributeNames()));
254         attribnames.remove("severity"); // special value (deserves unique column)
255         if (!attribnames.isEmpty()) {
256             sink.list();
257             for (String name : attribnames) {
258                 sink.listItem();
259 
260                 sink.text(name);
261 
262                 String value = getConfigAttribute(checkerConfig, null, name, "");
263                 // special case, Header.header and RegexpHeader.header
264                 if ("header".equals(name) && ("Header".equals(ruleName) || "RegexpHeader".equals(ruleName))) {
265                     String[] lines = StringUtils.split(value, "\\n");
266                     int linenum = 1;
267                     for (String line : lines) {
268                         sink.lineBreak();
269                         sink.rawText("<span style=\"color: gray\">");
270                         sink.text(linenum + ":");
271                         sink.rawText("</span>");
272                         sink.nonBreakingSpace();
273                         sink.monospaced();
274                         sink.text(line);
275                         sink.monospaced_();
276                         linenum++;
277                     }
278                 } else if ("headerFile".equals(name) && "RegexpHeader".equals(ruleName)) {
279                     sink.text(": ");
280                     sink.monospaced();
281                     sink.text("\"");
282                     // Make the headerFile value relative to ${basedir}
283                     String path =
284                             siteTool.getRelativePath(value, project.getBasedir().getAbsolutePath());
285                     sink.text(path.replace('\\', '/'));
286                     sink.text(value);
287                     sink.text("\"");
288                     sink.monospaced_();
289                 } else {
290                     sink.text(": ");
291                     sink.monospaced();
292                     sink.text("\"");
293                     sink.text(value);
294                     sink.text("\"");
295                     sink.monospaced_();
296                 }
297                 sink.listItem_();
298             }
299             sink.list_();
300         }
301 
302         sink.tableCell_();
303 
304         // column 3: rule violation count
305         sink.tableCell();
306         sink.text(String.valueOf(ref.violations));
307         sink.tableCell_();
308 
309         // column 4: severity
310         sink.tableCell();
311         // Grab the severity from the rule configuration, this time use error as default value
312         // Also pass along all parent configurations, so that we can try to find the severity there
313         String severity = getConfigAttribute(checkerConfig, parentConfiguration, "severity", "error");
314         iconSeverity(severity, TEXT_SIMPLE);
315         sink.tableCell_();
316 
317         sink.tableRow_();
318     }
319 
320     /**
321      * Check if a violation matches a rule.
322      *
323      * @param event the violation to check
324      * @param ruleName The name of the rule
325      * @param expectedMessage A message that, if it's not null, will be matched to the message from the violation
326      * @param expectedSeverity A severity that, if it's not null, will be matched to the severity from the violation
327      * @return The number of rule violations
328      */
329     public boolean matchRule(AuditEvent event, String ruleName, String expectedMessage, String expectedSeverity) {
330         if (!ruleName.equals(RuleUtil.getName(event))) {
331             return false;
332         }
333 
334         // check message too, for those that have a specific one.
335         // like GenericIllegalRegexp and Regexp
336         if (expectedMessage != null) {
337             // event.getMessage() uses java.text.MessageFormat in its implementation.
338             // Read MessageFormat Javadoc about single quote:
339             // http://java.sun.com/j2se/1.4.2/docs/api/java/text/MessageFormat.html
340             String msgWithoutSingleQuote = StringUtils.replace(expectedMessage, "'", "");
341 
342             if (!(expectedMessage.equals(event.getMessage()) || msgWithoutSingleQuote.equals(event.getMessage()))) {
343                 return false;
344             }
345         }
346         // Check the severity. This helps to distinguish between
347         // different configurations for the same rule, where each
348         // configuration has a different severity, like JavadocMethod.
349         // See also https://issues.apache.org/jira/browse/MCHECKSTYLE-41
350         if (expectedSeverity != null) {
351             if (!expectedSeverity.equals(event.getSeverityLevel().getName())) {
352                 return false;
353             }
354         }
355         return true;
356     }
357 
358     private void renderSeveritySummarySection() {
359         if (!enableSeveritySummary) {
360             return;
361         }
362 
363         startSection(getI18nString("summary"));
364 
365         startTable();
366 
367         sink.tableRow();
368         sink.tableHeaderCell();
369         sink.text(getI18nString("files"));
370         sink.tableHeaderCell_();
371 
372         sink.tableHeaderCell();
373         iconSeverity("info", TEXT_TITLE);
374         sink.tableHeaderCell_();
375 
376         sink.tableHeaderCell();
377         iconSeverity("warning", TEXT_TITLE);
378         sink.tableHeaderCell_();
379 
380         sink.tableHeaderCell();
381         iconSeverity("error", TEXT_TITLE);
382         sink.tableHeaderCell_();
383         sink.tableRow_();
384 
385         tableRow(new String[] {
386             String.valueOf(results.getFileCount()),
387             String.valueOf(results.getSeverityCount(SeverityLevel.INFO)),
388             String.valueOf(results.getSeverityCount(SeverityLevel.WARNING)),
389             String.valueOf(results.getSeverityCount(SeverityLevel.ERROR))
390         });
391 
392         endTable();
393 
394         endSection();
395     }
396 
397     private void renderFilesSummarySection() {
398         if (!enableFilesSummary) {
399             return;
400         }
401 
402         startSection(getI18nString("files"));
403 
404         startTable();
405 
406         sink.tableRow();
407         sink.tableHeaderCell();
408         sink.text(getI18nString("file"));
409         sink.tableHeaderCell_();
410         sink.tableHeaderCell();
411         iconSeverity("info", TEXT_ABBREV);
412         sink.tableHeaderCell_();
413         sink.tableHeaderCell();
414         iconSeverity("warning", TEXT_ABBREV);
415         sink.tableHeaderCell_();
416         sink.tableHeaderCell();
417         iconSeverity("error", TEXT_ABBREV);
418         sink.tableHeaderCell_();
419         sink.tableRow_();
420 
421         // Sort the files before writing them to the report
422         List<String> fileList = new ArrayList<>(results.getFiles().keySet());
423         Collections.sort(fileList);
424 
425         for (String filename : fileList) {
426             List<AuditEvent> violations = results.getFileViolations(filename);
427             if (violations.isEmpty()) {
428                 // skip files without violations
429                 continue;
430             }
431 
432             sink.tableRow();
433 
434             sink.tableCell();
435             sink.link("#" + DoxiaUtils.encodeId(filename));
436             sink.text(filename);
437             sink.link_();
438             sink.tableCell_();
439 
440             sink.tableCell();
441             sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.INFO)));
442             sink.tableCell_();
443 
444             sink.tableCell();
445             sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.WARNING)));
446             sink.tableCell_();
447 
448             sink.tableCell();
449             sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.ERROR)));
450             sink.tableCell_();
451 
452             sink.tableRow_();
453         }
454 
455         endTable();
456 
457         endSection();
458     }
459 
460     private void renderDetailsSection() {
461         startSection(getI18nString("details"));
462 
463         // Sort the files before writing their details to the report
464         List<String> fileList = new ArrayList<>(results.getFiles().keySet());
465         Collections.sort(fileList);
466 
467         for (String file : fileList) {
468             List<AuditEvent> violations = results.getFileViolations(file);
469 
470             if (violations.isEmpty()) {
471                 // skip files without violations
472                 continue;
473             }
474 
475             startSection(file);
476 
477             startTable();
478 
479             tableHeader(new String[] {
480                 getI18nString("column.severity"),
481                 getI18nString("rule.category"),
482                 getI18nString("rule"),
483                 getI18nString("column.message"),
484                 getI18nString("column.line")
485             });
486 
487             renderFileEvents(violations, file);
488 
489             endTable();
490 
491             endSection();
492         }
493 
494         endSection();
495     }
496 
497     private void renderFileEvents(List<AuditEvent> eventList, String filename) {
498         for (AuditEvent event : eventList) {
499             SeverityLevel level = event.getSeverityLevel();
500 
501             sink.tableRow();
502 
503             sink.tableCell();
504             iconSeverity(level.getName(), TEXT_SIMPLE);
505             sink.tableCell_();
506 
507             sink.tableCell();
508             String category = RuleUtil.getCategory(event);
509             if (category != null) {
510                 sink.text(category);
511             }
512             sink.tableCell_();
513 
514             sink.tableCell();
515             String ruleName = RuleUtil.getName(event);
516             if (ruleName != null) {
517                 sink.text(ruleName);
518             }
519             sink.tableCell_();
520 
521             sink.tableCell();
522             sink.text(event.getMessage());
523             sink.tableCell_();
524 
525             sink.tableCell();
526 
527             int line = event.getLine();
528             String effectiveXrefLocation = getEffectiveXrefLocation(eventList);
529             if (effectiveXrefLocation != null && line != 0) {
530                 sink.link(effectiveXrefLocation + "/" + filename.replaceAll("\\.java$", ".html") + "#L" + line);
531                 sink.text(String.valueOf(line));
532                 sink.link_();
533             } else if (line != 0) {
534                 sink.text(String.valueOf(line));
535             }
536             sink.tableCell_();
537 
538             sink.tableRow_();
539         }
540     }
541 
542     private String getEffectiveXrefLocation(List<AuditEvent> eventList) {
543         String absoluteFilename = eventList.get(0).getFileName();
544         if (isTestSource(absoluteFilename)) {
545             return getXrefTestLocation();
546         } else {
547             return getXrefLocation();
548         }
549     }
550 
551     private boolean isTestSource(final String absoluteFilename) {
552         for (File testSourceDirectory : testSourceDirectories) {
553             if (absoluteFilename.startsWith(testSourceDirectory.getAbsolutePath())) {
554                 return true;
555             }
556         }
557 
558         return false;
559     }
560 
561     public String getXrefLocation() {
562         return xrefLocation;
563     }
564 
565     public void setXrefLocation(String xrefLocation) {
566         this.xrefLocation = xrefLocation;
567     }
568 
569     public String getXrefTestLocation() {
570         return xrefTestLocation;
571     }
572 
573     public void setXrefTestLocation(String xrefTestLocation) {
574         this.xrefTestLocation = xrefTestLocation;
575     }
576 
577     public void setTestSourceDirectories(List<File> testSourceDirectories) {
578         this.testSourceDirectories = testSourceDirectories;
579     }
580 
581     public void setTreeWalkerNames(List<String> treeWalkerNames) {
582         this.treeWalkerNames = treeWalkerNames;
583     }
584 
585     /**
586      * Render an icon of given level with associated text.
587      * @param level one of <code>INFO</code>, <code>WARNING</code> or <code>ERROR</code> constants
588      * @param textType one of <code>NO_TEXT</code>, <code>TEXT_SIMPLE</code>, <code>TEXT_TITLE</code> or
589      * <code>TEXT_ABBREV</code> constants
590      */
591     private void iconSeverity(String level, int textType) {
592         sink.figureGraphics("images/icon_" + level + "_sml.gif");
593 
594         if (textType > NO_TEXT) {
595             sink.nonBreakingSpace();
596             String suffix;
597             switch (textType) {
598                 case TEXT_TITLE:
599                     suffix = "s";
600                     break;
601                 case TEXT_ABBREV:
602                     suffix = "s.abbrev";
603                     break;
604                 default:
605                     suffix = "";
606             }
607             sink.text(getI18nString(level + suffix));
608         }
609     }
610 
611     /**
612      * Get the effective Checkstyle version at runtime.
613      * @return the MANIFEST implementation version of Checkstyle API package (can be <code>null</code>)
614      */
615     private String getCheckstyleVersion() {
616         Package checkstyleApiPackage = Configuration.class.getPackage();
617 
618         return (checkstyleApiPackage == null) ? null : checkstyleApiPackage.getImplementationVersion();
619     }
620 
621     public List<ConfReference> sortConfiguration(CheckstyleResults results) {
622         List<ConfReference> result = new ArrayList<>();
623 
624         sortConfiguration(result, checkstyleConfig, null, results);
625 
626         Collections.sort(result);
627 
628         return result;
629     }
630 
631     private void sortConfiguration(
632             List<ConfReference> result,
633             Configuration config,
634             ChainedItem<Configuration> parent,
635             CheckstyleResults results) {
636         for (Configuration childConfig : config.getChildren()) {
637             String ruleName = childConfig.getName();
638 
639             if (treeWalkerNames.contains(ruleName)) {
640                 // special sub-case: TreeWalker is the parent of multiple rules, not an effective rule
641                 sortConfiguration(result, childConfig, new ChainedItem<>(config, parent), results);
642             } else {
643                 String fixedmessage = getConfigAttribute(childConfig, null, "message", null);
644                 // Grab the severity from the rule configuration. Do not set default value here as
645                 // it breaks our rule aggregate section entirely.  The counts are off but this is
646                 // not appropriate fix location per MCHECKSTYLE-365.
647                 String configSeverity = getConfigAttribute(childConfig, null, "severity", null);
648 
649                 // count rule violations
650                 long violations = 0;
651                 AuditEvent lastMatchedEvent = null;
652                 for (List<AuditEvent> errors : results.getFiles().values()) {
653                     for (AuditEvent event : errors) {
654                         if (matchRule(event, ruleName, fixedmessage, configSeverity)) {
655                             lastMatchedEvent = event;
656                             violations++;
657                         }
658                     }
659                 }
660 
661                 if (violations > 0) // forget rules without violations
662                 {
663                     String category = RuleUtil.getCategory(lastMatchedEvent);
664 
665                     result.add(new ConfReference(category, childConfig, parent, violations, result.size()));
666                 }
667             }
668         }
669     }
670 
671     private static class ConfReference implements Comparable<ConfReference> {
672         private final String category;
673         private final Configuration configuration;
674         private final ChainedItem<Configuration> parentConfiguration;
675         private final long violations;
676         private final int count;
677 
678         ConfReference(
679                 String category,
680                 Configuration configuration,
681                 ChainedItem<Configuration> parentConfiguration,
682                 long violations,
683                 int count) {
684             this.category = category;
685             this.configuration = configuration;
686             this.parentConfiguration = parentConfiguration;
687             this.violations = violations;
688             this.count = count;
689         }
690 
691         public int compareTo(ConfReference o) {
692             int compare = category.compareTo(o.category);
693             if (compare == 0) {
694                 compare = configuration.getName().compareTo(o.configuration.getName());
695             }
696             return (compare == 0) ? (o.count - count) : compare;
697         }
698     }
699 
700     private static class ChainedItem<T> {
701         private final ChainedItem<T> parent;
702 
703         private final T value;
704 
705         ChainedItem(T value, ChainedItem<T> parent) {
706             this.parent = parent;
707             this.value = value;
708         }
709     }
710 }