blob: d961eebb02c05131d11ad38eada5205be9b6b5c4 [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.pmd;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import net.sourceforge.pmd.lang.rule.RulePriority;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.pmd.model.ProcessingError;
import org.apache.maven.plugins.pmd.model.SuppressedViolation;
import org.apache.maven.plugins.pmd.model.Violation;
import org.apache.maven.reporting.AbstractMavenReportRenderer;
import org.codehaus.plexus.i18n.I18N;
import org.codehaus.plexus.util.StringUtils;
/**
* Render the PMD violations into Doxia events.
*
* @author Brett Porter
* @version $Id$
*/
public class PmdReportRenderer extends AbstractMavenReportRenderer {
private final Log log;
private final I18N i18n;
private final Locale locale;
private final Map<File, PmdFileInfo> files;
// TODO Should not share state
private String currentFilename;
private final Collection<Violation> violations;
private boolean renderRuleViolationPriority;
private final boolean renderViolationsByPriority;
private final boolean aggregate;
private Collection<SuppressedViolation> suppressedViolations = new ArrayList<>();
private Collection<ProcessingError> processingErrors = new ArrayList<>();
public PmdReportRenderer(
Log log,
Sink sink,
I18N i18n,
Locale locale,
Map<File, PmdFileInfo> files,
Collection<Violation> violations,
boolean renderRuleViolationPriority,
boolean renderViolationsByPriority,
boolean aggregate) {
super(sink);
this.log = log;
this.i18n = i18n;
this.locale = locale;
this.files = files;
this.violations = violations;
this.renderRuleViolationPriority = renderRuleViolationPriority;
this.renderViolationsByPriority = renderViolationsByPriority;
this.aggregate = aggregate;
}
public void setSuppressedViolations(Collection<SuppressedViolation> suppressedViolations) {
this.suppressedViolations = suppressedViolations;
}
public void setProcessingErrors(Collection<ProcessingError> processingErrors) {
this.processingErrors = processingErrors;
}
@Override
public String getTitle() {
return getI18nString("title");
}
/**
* @param key The key.
* @return The translated string.
*/
private String getI18nString(String key) {
return i18n.getString("pmd-report", locale, "report.pmd." + key);
}
protected void renderBody() {
startSection(getTitle());
sink.paragraph();
sink.text(getI18nString("pmdlink") + " ");
link("https://pmd.github.io", "PMD");
sink.text(" " + AbstractPmdReport.getPmdVersion() + ".");
sink.paragraph_();
if (!violations.isEmpty()) {
renderViolationsByPriority();
renderViolations();
} else {
paragraph(getI18nString("noProblems"));
}
renderSuppressedViolations();
renderProcessingErrors();
endSection();
}
private void startFileSection(String currentFilename, PmdFileInfo fileInfo) {
// prepare the filename
this.currentFilename = shortenFilename(currentFilename, fileInfo);
startSection(makeFileSectionName(this.currentFilename, fileInfo));
startTable();
sink.tableRow();
tableHeaderCell(getI18nString("column.rule"));
tableHeaderCell(getI18nString("column.violation"));
if (this.renderRuleViolationPriority) {
tableHeaderCell(getI18nString("column.priority"));
}
tableHeaderCell(getI18nString("column.line"));
sink.tableRow_();
}
private void endFileSection() {
endTable();
endSection();
}
private void addRuleName(Violation ruleViolation) {
boolean hasUrl = StringUtils.isNotBlank(ruleViolation.getExternalInfoUrl());
if (hasUrl) {
sink.link(ruleViolation.getExternalInfoUrl());
}
sink.text(ruleViolation.getRule());
if (hasUrl) {
sink.link_();
}
}
private void renderSingleRuleViolation(Violation ruleViolation, PmdFileInfo fileInfo) {
sink.tableRow();
sink.tableCell();
addRuleName(ruleViolation);
sink.tableCell_();
// May contain content not legit for #tableCell()
sink.tableCell();
sink.text(ruleViolation.getText());
sink.tableCell_();
if (this.renderRuleViolationPriority) {
tableCell(String.valueOf(
RulePriority.valueOf(ruleViolation.getPriority()).getPriority()));
}
sink.tableCell();
int beginLine = ruleViolation.getBeginline();
outputLineLink(beginLine, fileInfo);
int endLine = ruleViolation.getEndline();
if (endLine != beginLine) {
sink.text("&#x2013;"); // \u2013 is a medium long dash character
outputLineLink(endLine, fileInfo);
}
sink.tableCell_();
sink.tableRow_();
}
// PMD might run the analysis multi-threaded, so the violations might be reported
// out of order. We sort them here by filename and line number before writing them to
// the report.
private void renderViolations() {
startSection(getI18nString("files"));
// TODO files summary
renderViolationsTable(violations);
endSection();
}
private void renderViolationsByPriority() {
if (!renderViolationsByPriority) {
return;
}
boolean oldPriorityColumn = this.renderRuleViolationPriority;
this.renderRuleViolationPriority = false;
startSection(getI18nString("violationsByPriority"));
Map<RulePriority, List<Violation>> violationsByPriority = new HashMap<>();
for (Violation violation : violations) {
RulePriority priority = RulePriority.valueOf(violation.getPriority());
List<Violation> violationSegment = violationsByPriority.get(priority);
if (violationSegment == null) {
violationSegment = new ArrayList<>();
violationsByPriority.put(priority, violationSegment);
}
violationSegment.add(violation);
}
for (RulePriority priority : RulePriority.values()) {
List<Violation> violationsWithPriority = violationsByPriority.get(priority);
if (violationsWithPriority == null || violationsWithPriority.isEmpty()) {
continue;
}
startSection(getI18nString("priority") + " " + priority.getPriority());
renderViolationsTable(violationsWithPriority);
endSection();
}
if (violations.isEmpty()) {
paragraph(getI18nString("noProblems"));
}
endSection();
this.renderRuleViolationPriority = oldPriorityColumn;
}
private void renderViolationsTable(Collection<Violation> violationSegment) {
List<Violation> violationSegmentCopy = new ArrayList<>(violationSegment);
Collections.sort(violationSegmentCopy, new Comparator<Violation>() {
/** {@inheritDoc} */
public int compare(Violation o1, Violation o2) {
int filenames = o1.getFileName().compareTo(o2.getFileName());
if (filenames == 0) {
return o1.getBeginline() - o2.getBeginline();
} else {
return filenames;
}
}
});
boolean fileSectionStarted = false;
String previousFilename = null;
for (Violation ruleViolation : violationSegmentCopy) {
String currentFn = ruleViolation.getFileName();
PmdFileInfo fileInfo = determineFileInfo(currentFn);
if (!currentFn.equalsIgnoreCase(previousFilename) && fileSectionStarted) {
endFileSection();
fileSectionStarted = false;
}
if (!fileSectionStarted) {
startFileSection(currentFn, fileInfo);
fileSectionStarted = true;
}
renderSingleRuleViolation(ruleViolation, fileInfo);
previousFilename = currentFn;
}
if (fileSectionStarted) {
endFileSection();
}
}
private void outputLineLink(int line, PmdFileInfo fileInfo) {
String xrefLocation = null;
if (fileInfo != null) {
xrefLocation = fileInfo.getXrefLocation();
}
if (xrefLocation != null) {
sink.link(xrefLocation + "/" + currentFilename.replaceAll("\\.java$", ".html") + "#L" + line);
}
sink.text(String.valueOf(line));
if (xrefLocation != null) {
sink.link_();
}
}
// PMD might run the analysis multi-threaded, so the suppressed violations might be reported
// out of order. We sort them here by filename before writing them to
// the report.
private void renderSuppressedViolations() {
if (suppressedViolations.isEmpty()) {
return;
}
startSection(getI18nString("suppressedViolations.title"));
List<SuppressedViolation> suppressedViolationsCopy = new ArrayList<>(suppressedViolations);
Collections.sort(suppressedViolationsCopy, new Comparator<SuppressedViolation>() {
@Override
public int compare(SuppressedViolation o1, SuppressedViolation o2) {
return o1.getFilename().compareTo(o2.getFilename());
}
});
startTable();
tableHeader(new String[] {
getI18nString("suppressedViolations.column.filename"),
getI18nString("suppressedViolations.column.ruleMessage"),
getI18nString("suppressedViolations.column.suppressionType"),
getI18nString("suppressedViolations.column.userMessage")
});
for (SuppressedViolation suppressedViolation : suppressedViolationsCopy) {
String filename = suppressedViolation.getFilename();
PmdFileInfo fileInfo = determineFileInfo(filename);
filename = shortenFilename(filename, fileInfo);
// May contain content not legit for #tableCell()
sink.tableRow();
tableCell(filename);
sink.tableCell();
sink.text(suppressedViolation.getRuleMessage());
sink.tableCell_();
tableCell(suppressedViolation.getSuppressionType());
sink.tableCell();
sink.text(suppressedViolation.getUserMessage());
sink.tableCell_();
sink.tableRow_();
}
endTable();
endSection();
}
private void renderProcessingErrors() {
if (processingErrors.isEmpty()) {
return;
}
// sort the problem by filename first, since PMD is executed multi-threaded
// and might reports the results unsorted
List<ProcessingError> processingErrorsCopy = new ArrayList<>(processingErrors);
Collections.sort(processingErrorsCopy, new Comparator<ProcessingError>() {
@Override
public int compare(ProcessingError e1, ProcessingError e2) {
return e1.getFilename().compareTo(e2.getFilename());
}
});
startSection(getI18nString("processingErrors.title"));
startTable();
tableHeader(new String[] {
getI18nString("processingErrors.column.filename"), getI18nString("processingErrors.column.problem")
});
for (ProcessingError error : processingErrorsCopy) {
renderSingleProcessingError(error);
}
endTable();
endSection();
}
private void renderSingleProcessingError(ProcessingError error) {
String filename = error.getFilename();
PmdFileInfo fileInfo = determineFileInfo(filename);
filename = makeFileSectionName(shortenFilename(filename, fileInfo), fileInfo);
sink.tableRow();
tableCell(filename);
sink.tableCell();
sink.text(error.getMsg());
sink.verbatim(null);
sink.rawText(error.getDetail());
sink.verbatim_();
sink.tableCell_();
sink.tableRow_();
}
private String shortenFilename(String filename, PmdFileInfo fileInfo) {
String result = filename;
if (fileInfo != null && fileInfo.getSourceDirectory() != null) {
result = StringUtils.substring(
result, fileInfo.getSourceDirectory().getAbsolutePath().length() + 1);
}
return StringUtils.replace(result, "\\", "/");
}
private String makeFileSectionName(String filename, PmdFileInfo fileInfo) {
if (aggregate && fileInfo != null && fileInfo.getProject() != null) {
return fileInfo.getProject().getName() + " - " + filename;
}
return filename;
}
private PmdFileInfo determineFileInfo(String filename) {
try {
File canonicalFilename = new File(filename).getCanonicalFile();
PmdFileInfo fileInfo = files.get(canonicalFilename);
if (fileInfo == null) {
log.warn("Couldn't determine PmdFileInfo for file " + filename + " (canonical: " + canonicalFilename
+ "). XRef links won't be available.");
}
return fileInfo;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}