blob: 3e985fb3bed30956683456a11401674b63c23647 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugins.checkstyle;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.tools.SiteTool;
import org.apache.maven.doxia.util.DoxiaUtils;
import org.apache.maven.plugins.checkstyle.exec.CheckstyleResults;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReportRenderer;
import org.codehaus.plexus.i18n.I18N;
/**
* Generate a report based on CheckstyleResults.
*
*
*/
public class CheckstyleReportRenderer extends AbstractMavenReportRenderer {
private static final int NO_TEXT = 0;
private static final int TEXT_SIMPLE = 1;
private static final int TEXT_TITLE = 2;
private static final int TEXT_ABBREV = 3;
private final I18N i18n;
private final Locale locale;
private final MavenProject project;
private final Configuration checkstyleConfig;
private final boolean enableRulesSummary;
private final boolean enableSeveritySummary;
private final boolean enableFilesSummary;
private final SiteTool siteTool;
private String xrefLocation;
private String xrefTestLocation;
private List<File> testSourceDirectories;
private List<String> treeWalkerNames = Collections.singletonList("TreeWalker");
private final String ruleset;
private final CheckstyleResults results;
public CheckstyleReportRenderer(
Sink sink,
I18N i18n,
Locale locale,
MavenProject project,
SiteTool siteTool,
String ruleset,
String xrefLocation,
String xrefTestLocation,
List<File> testSourceDirectories,
boolean enableRulesSummary,
boolean enableSeveritySummary,
boolean enableFilesSummary,
CheckstyleResults results) {
super(sink);
this.i18n = i18n;
this.locale = locale;
this.project = project;
this.siteTool = siteTool;
this.ruleset = ruleset;
this.xrefLocation = xrefLocation;
this.xrefTestLocation = xrefTestLocation;
this.testSourceDirectories = testSourceDirectories;
this.enableRulesSummary = enableRulesSummary;
this.enableSeveritySummary = enableSeveritySummary;
this.enableFilesSummary = enableFilesSummary;
this.results = results;
this.checkstyleConfig = results.getConfiguration();
}
@Override
public String getTitle() {
return getI18nString("title");
}
/**
* @param key The key.
* @return The translated string.
*/
private String getI18nString(String key) {
return i18n.getString("checkstyle-report", locale, "report.checkstyle." + key);
}
protected void renderBody() {
startSection(getTitle());
sink.paragraph();
sink.text(getI18nString("checkstylelink") + " ");
sink.link("https://checkstyle.org/");
sink.text("Checkstyle");
sink.link_();
String version = getCheckstyleVersion();
if (version != null) {
sink.text(" ");
sink.text(version);
}
sink.text(" ");
sink.text(String.format(getI18nString("ruleset"), ruleset));
sink.text(".");
sink.paragraph_();
renderSeveritySummarySection();
renderFilesSummarySection();
renderRulesSummarySection();
renderDetailsSection();
endSection();
}
/**
* Get the value of the specified attribute from the Checkstyle configuration.
* If parentConfigurations is non-null and non-empty, the parent
* configurations are searched if the attribute cannot be found in the
* current configuration. If the attribute is still not found, the
* specified default value will be returned.
*
* @param config The current Checkstyle configuration
* @param parentConfiguration The configuration of the parent of the current configuration
* @param attributeName The name of the attribute
* @param defaultValue The default value to use if the attribute cannot be found in any configuration
* @return The value of the specified attribute
*/
private String getConfigAttribute(
Configuration config,
ChainedItem<Configuration> parentConfiguration,
String attributeName,
String defaultValue) {
String ret;
try {
ret = config.getAttribute(attributeName);
} catch (CheckstyleException e) {
// Try to find the attribute in a parent, if there are any
if (parentConfiguration != null) {
ret = getConfigAttribute(
parentConfiguration.value, parentConfiguration.parent, attributeName, defaultValue);
} else {
ret = defaultValue;
}
}
return ret;
}
/**
* Create the rules summary section of the report.
*
* @param results The results to summarize
*/
private void renderRulesSummarySection() {
if (!enableRulesSummary) {
return;
}
if (checkstyleConfig == null) {
return;
}
startSection(getI18nString("rules"));
startTable();
tableHeader(new String[] {
getI18nString("rule.category"),
getI18nString("rule"),
getI18nString("violations"),
getI18nString("column.severity")
});
// Top level should be the checker.
if ("checker".equalsIgnoreCase(checkstyleConfig.getName())) {
String category = null;
for (ConfReference ref : sortConfiguration(results)) {
renderRuleRow(ref, results, category);
category = ref.category;
}
} else {
tableRow(new String[] {getI18nString("norule")});
}
endTable();
endSection();
}
/**
* Create a summary for one Checkstyle rule.
*
* @param ref The configuration reference for the row
* @param results The results to summarize
* @param previousCategory The previous row's category
*/
private void renderRuleRow(ConfReference ref, CheckstyleResults results, String previousCategory) {
Configuration checkerConfig = ref.configuration;
ChainedItem<Configuration> parentConfiguration = ref.parentConfiguration;
String ruleName = checkerConfig.getName();
sink.tableRow();
// column 1: rule category
sink.tableCell();
String category = ref.category;
if (!category.equals(previousCategory)) {
sink.text(category);
}
sink.tableCell_();
// column 2: Rule name + configured attributes
sink.tableCell();
if (!"extension".equals(category)) {
sink.link("https://checkstyle.org/config_" + category + ".html#" + ruleName);
sink.text(ruleName);
sink.link_();
} else {
sink.text(ruleName);
}
List<String> attribnames = new ArrayList<>(Arrays.asList(checkerConfig.getAttributeNames()));
attribnames.remove("severity"); // special value (deserves unique column)
if (!attribnames.isEmpty()) {
sink.list();
for (String name : attribnames) {
sink.listItem();
sink.text(name);
String value = getConfigAttribute(checkerConfig, null, name, "");
// special case, Header.header and RegexpHeader.header
if ("header".equals(name) && ("Header".equals(ruleName) || "RegexpHeader".equals(ruleName))) {
String[] lines = StringUtils.split(value, "\\n");
int linenum = 1;
for (String line : lines) {
sink.lineBreak();
sink.rawText("<span style=\"color: gray\">");
sink.text(linenum + ":");
sink.rawText("</span>");
sink.nonBreakingSpace();
sink.monospaced();
sink.text(line);
sink.monospaced_();
linenum++;
}
} else if ("headerFile".equals(name) && "RegexpHeader".equals(ruleName)) {
sink.text(": ");
sink.monospaced();
sink.text("\"");
// Make the headerFile value relative to ${basedir}
String path =
siteTool.getRelativePath(value, project.getBasedir().getAbsolutePath());
sink.text(path.replace('\\', '/'));
sink.text(value);
sink.text("\"");
sink.monospaced_();
} else {
sink.text(": ");
sink.monospaced();
sink.text("\"");
sink.text(value);
sink.text("\"");
sink.monospaced_();
}
sink.listItem_();
}
sink.list_();
}
sink.tableCell_();
// column 3: rule violation count
sink.tableCell();
sink.text(String.valueOf(ref.violations));
sink.tableCell_();
// column 4: severity
sink.tableCell();
// Grab the severity from the rule configuration, this time use error as default value
// Also pass along all parent configurations, so that we can try to find the severity there
String severity = getConfigAttribute(checkerConfig, parentConfiguration, "severity", "error");
iconSeverity(severity, TEXT_SIMPLE);
sink.tableCell_();
sink.tableRow_();
}
/**
* Check if a violation matches a rule.
*
* @param event the violation to check
* @param ruleName The name of the rule
* @param expectedMessage A message that, if it's not null, will be matched to the message from the violation
* @param expectedSeverity A severity that, if it's not null, will be matched to the severity from the violation
* @return The number of rule violations
*/
public boolean matchRule(AuditEvent event, String ruleName, String expectedMessage, String expectedSeverity) {
if (!ruleName.equals(RuleUtil.getName(event))) {
return false;
}
// check message too, for those that have a specific one.
// like GenericIllegalRegexp and Regexp
if (expectedMessage != null) {
// event.getMessage() uses java.text.MessageFormat in its implementation.
// Read MessageFormat Javadoc about single quote:
// http://java.sun.com/j2se/1.4.2/docs/api/java/text/MessageFormat.html
String msgWithoutSingleQuote = StringUtils.replace(expectedMessage, "'", "");
if (!(expectedMessage.equals(event.getMessage()) || msgWithoutSingleQuote.equals(event.getMessage()))) {
return false;
}
}
// Check the severity. This helps to distinguish between
// different configurations for the same rule, where each
// configuration has a different severity, like JavadocMethod.
// See also https://issues.apache.org/jira/browse/MCHECKSTYLE-41
if (expectedSeverity != null) {
if (!expectedSeverity.equals(event.getSeverityLevel().getName())) {
return false;
}
}
return true;
}
private void renderSeveritySummarySection() {
if (!enableSeveritySummary) {
return;
}
startSection(getI18nString("summary"));
startTable();
sink.tableRow();
sink.tableHeaderCell();
sink.text(getI18nString("files"));
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconSeverity("info", TEXT_TITLE);
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconSeverity("warning", TEXT_TITLE);
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconSeverity("error", TEXT_TITLE);
sink.tableHeaderCell_();
sink.tableRow_();
tableRow(new String[] {
String.valueOf(results.getFileCount()),
String.valueOf(results.getSeverityCount(SeverityLevel.INFO)),
String.valueOf(results.getSeverityCount(SeverityLevel.WARNING)),
String.valueOf(results.getSeverityCount(SeverityLevel.ERROR))
});
endTable();
endSection();
}
private void renderFilesSummarySection() {
if (!enableFilesSummary) {
return;
}
startSection(getI18nString("files"));
startTable();
sink.tableRow();
sink.tableHeaderCell();
sink.text(getI18nString("file"));
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconSeverity("info", TEXT_ABBREV);
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconSeverity("warning", TEXT_ABBREV);
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconSeverity("error", TEXT_ABBREV);
sink.tableHeaderCell_();
sink.tableRow_();
// Sort the files before writing them to the report
List<String> fileList = new ArrayList<>(results.getFiles().keySet());
Collections.sort(fileList);
for (String filename : fileList) {
List<AuditEvent> violations = results.getFileViolations(filename);
if (violations.isEmpty()) {
// skip files without violations
continue;
}
sink.tableRow();
sink.tableCell();
sink.link("#" + DoxiaUtils.encodeId(filename));
sink.text(filename);
sink.link_();
sink.tableCell_();
sink.tableCell();
sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.INFO)));
sink.tableCell_();
sink.tableCell();
sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.WARNING)));
sink.tableCell_();
sink.tableCell();
sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.ERROR)));
sink.tableCell_();
sink.tableRow_();
}
endTable();
endSection();
}
private void renderDetailsSection() {
startSection(getI18nString("details"));
// Sort the files before writing their details to the report
List<String> fileList = new ArrayList<>(results.getFiles().keySet());
Collections.sort(fileList);
for (String file : fileList) {
List<AuditEvent> violations = results.getFileViolations(file);
if (violations.isEmpty()) {
// skip files without violations
continue;
}
startSection(file);
startTable();
tableHeader(new String[] {
getI18nString("column.severity"),
getI18nString("rule.category"),
getI18nString("rule"),
getI18nString("column.message"),
getI18nString("column.line")
});
renderFileEvents(violations, file);
endTable();
endSection();
}
endSection();
}
private void renderFileEvents(List<AuditEvent> eventList, String filename) {
for (AuditEvent event : eventList) {
SeverityLevel level = event.getSeverityLevel();
sink.tableRow();
sink.tableCell();
iconSeverity(level.getName(), TEXT_SIMPLE);
sink.tableCell_();
sink.tableCell();
String category = RuleUtil.getCategory(event);
if (category != null) {
sink.text(category);
}
sink.tableCell_();
sink.tableCell();
String ruleName = RuleUtil.getName(event);
if (ruleName != null) {
sink.text(ruleName);
}
sink.tableCell_();
sink.tableCell();
sink.text(event.getMessage());
sink.tableCell_();
sink.tableCell();
int line = event.getLine();
String effectiveXrefLocation = getEffectiveXrefLocation(eventList);
if (effectiveXrefLocation != null && line != 0) {
sink.link(effectiveXrefLocation + "/" + filename.replaceAll("\\.java$", ".html") + "#L" + line);
sink.text(String.valueOf(line));
sink.link_();
} else if (line != 0) {
sink.text(String.valueOf(line));
}
sink.tableCell_();
sink.tableRow_();
}
}
private String getEffectiveXrefLocation(List<AuditEvent> eventList) {
String absoluteFilename = eventList.get(0).getFileName();
if (isTestSource(absoluteFilename)) {
return xrefTestLocation;
} else {
return xrefLocation;
}
}
private boolean isTestSource(final String absoluteFilename) {
for (File testSourceDirectory : testSourceDirectories) {
if (absoluteFilename.startsWith(testSourceDirectory.getAbsolutePath())) {
return true;
}
}
return false;
}
public void setTreeWalkerNames(List<String> treeWalkerNames) {
this.treeWalkerNames = treeWalkerNames;
}
/**
* Render an icon of given level with associated text.
* @param level one of <code>INFO</code>, <code>WARNING</code> or <code>ERROR</code> constants
* @param textType one of <code>NO_TEXT</code>, <code>TEXT_SIMPLE</code>, <code>TEXT_TITLE</code> or
* <code>TEXT_ABBREV</code> constants
*/
private void iconSeverity(String level, int textType) {
sink.figureGraphics("images/icon_" + level + "_sml.gif");
if (textType > NO_TEXT) {
sink.nonBreakingSpace();
String suffix;
switch (textType) {
case TEXT_TITLE:
suffix = "s";
break;
case TEXT_ABBREV:
suffix = "s.abbrev";
break;
default:
suffix = "";
}
sink.text(getI18nString(level + suffix));
}
}
/**
* Get the effective Checkstyle version at runtime.
* @return the MANIFEST implementation version of Checkstyle API package (can be <code>null</code>)
*/
private String getCheckstyleVersion() {
Package checkstyleApiPackage = Configuration.class.getPackage();
return (checkstyleApiPackage == null) ? null : checkstyleApiPackage.getImplementationVersion();
}
public List<ConfReference> sortConfiguration(CheckstyleResults results) {
List<ConfReference> result = new ArrayList<>();
sortConfiguration(result, checkstyleConfig, null, results);
Collections.sort(result);
return result;
}
private void sortConfiguration(
List<ConfReference> result,
Configuration config,
ChainedItem<Configuration> parent,
CheckstyleResults results) {
for (Configuration childConfig : config.getChildren()) {
String ruleName = childConfig.getName();
if (treeWalkerNames.contains(ruleName)) {
// special sub-case: TreeWalker is the parent of multiple rules, not an effective rule
sortConfiguration(result, childConfig, new ChainedItem<>(config, parent), results);
} else {
String fixedmessage = getConfigAttribute(childConfig, null, "message", null);
// Grab the severity from the rule configuration. Do not set default value here as
// it breaks our rule aggregate section entirely. The counts are off but this is
// not appropriate fix location per MCHECKSTYLE-365.
String configSeverity = getConfigAttribute(childConfig, null, "severity", null);
// count rule violations
long violations = 0;
AuditEvent lastMatchedEvent = null;
for (List<AuditEvent> errors : results.getFiles().values()) {
for (AuditEvent event : errors) {
if (matchRule(event, ruleName, fixedmessage, configSeverity)) {
lastMatchedEvent = event;
violations++;
}
}
}
if (violations > 0) // forget rules without violations
{
String category = RuleUtil.getCategory(lastMatchedEvent);
result.add(new ConfReference(category, childConfig, parent, violations, result.size()));
}
}
}
}
private static class ConfReference implements Comparable<ConfReference> {
private final String category;
private final Configuration configuration;
private final ChainedItem<Configuration> parentConfiguration;
private final long violations;
private final int count;
ConfReference(
String category,
Configuration configuration,
ChainedItem<Configuration> parentConfiguration,
long violations,
int count) {
this.category = category;
this.configuration = configuration;
this.parentConfiguration = parentConfiguration;
this.violations = violations;
this.count = count;
}
public int compareTo(ConfReference o) {
int compare = category.compareTo(o.category);
if (compare == 0) {
compare = configuration.getName().compareTo(o.configuration.getName());
}
return (compare == 0) ? (o.count - count) : compare;
}
}
private static class ChainedItem<T> {
private final ChainedItem<T> parent;
private final T value;
ChainedItem(T value, ChainedItem<T> parent) {
this.parent = parent;
this.value = value;
}
}
}