All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.maven.plugins.pmd.PmdReportRenderer Maven / Gradle / Ivy

/*
 * 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 files;

    // TODO Should not share state
    private String currentFilename;

    private final Collection violations;

    private boolean renderRuleViolationPriority;

    private final boolean renderViolationsByPriority;

    private final boolean aggregate;

    private Collection suppressedViolations = new ArrayList<>();

    private Collection processingErrors = new ArrayList<>();

    public PmdReportRenderer(
            Log log,
            Sink sink,
            I18N i18n,
            Locale locale,
            Map files,
            Collection 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 suppressedViolations) {
        this.suppressedViolations = suppressedViolations;
    }

    public void setProcessingErrors(Collection 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("–"); // \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> violationsByPriority = new HashMap<>();
        for (Violation violation : violations) {
            RulePriority priority = RulePriority.valueOf(violation.getPriority());
            List violationSegment = violationsByPriority.get(priority);
            if (violationSegment == null) {
                violationSegment = new ArrayList<>();
                violationsByPriority.put(priority, violationSegment);
            }
            violationSegment.add(violation);
        }

        for (RulePriority priority : RulePriority.values()) {
            List 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 violationSegment) {
        List violationSegmentCopy = new ArrayList<>(violationSegment);
        Collections.sort(violationSegmentCopy, new Comparator() {
            /** {@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 suppressedViolationsCopy = new ArrayList<>(suppressedViolations);
        Collections.sort(suppressedViolationsCopy, new Comparator() {
            @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 processingErrorsCopy = new ArrayList<>(processingErrors);
        Collections.sort(processingErrorsCopy, new Comparator() {
            @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);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy