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