/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import net.sourceforge.pmd.lang.dfa.report.ReportTree; import net.sourceforge.pmd.renderers.AbstractAccumulatingRenderer; import net.sourceforge.pmd.stat.Metric; import net.sourceforge.pmd.util.DateTimeUtil; import net.sourceforge.pmd.util.EmptyIterator; import net.sourceforge.pmd.util.NumericConstants; import net.sourceforge.pmd.util.StringUtil; /** * A {@link Report} collects all informations during a PMD execution. This * includes violations, suppressed violations, metrics, error during processing * and configuration errors. */ public class Report implements Iterable<RuleViolation> { /* * The idea is to store the violations in a tree instead of a list, to do * better and faster sort and filter mechanism and to visualize the result * as tree. (ide plugins). */ private final ReportTree violationTree = new ReportTree(); // Note that this and the above data structure are both being maintained for // a bit private final List<RuleViolation> violations = new ArrayList<>(); private final Set<Metric> metrics = new HashSet<>(); private final List<ThreadSafeReportListener> listeners = new ArrayList<>(); private List<ProcessingError> errors; private List<RuleConfigurationError> configErrors; private Map<Integer, String> linesToSuppress = new HashMap<>(); private long start; private long end; private List<SuppressedViolation> suppressedRuleViolations = new ArrayList<>(); /** * Creates a new, initialized, empty report for the given file name. * * @param ctx * The context to use to connect to the report * @param fileName * the filename used to report any violations * @return the new report */ public static Report createReport(RuleContext ctx, String fileName) { Report report = new Report(); // overtake the listener report.addListeners(ctx.getReport().getListeners()); ctx.setReport(report); ctx.setSourceCodeFilename(fileName); ctx.setSourceCodeFile(new File(fileName)); return report; } /** * Represents a duration. Useful for reporting processing time. */ public static class ReadableDuration { private final long duration; /** * Creates a new duration. * * @param duration * the duration in milliseconds. */ public ReadableDuration(long duration) { this.duration = duration; } /** * Gets a human readable representation of the duration, such as "1h 3m * 5s". * * @return human readable representation of the duration */ public String getTime() { return DateTimeUtil.asHoursMinutesSeconds(duration); } } /** * Represents a configuration error. */ public static class RuleConfigurationError { private final Rule rule; private final String issue; /** * Creates a new configuration error. * * @param theRule * the rule which is configured wrongly * @param theIssue * the reason, why the configuration is wrong */ public RuleConfigurationError(Rule theRule, String theIssue) { rule = theRule; issue = theIssue; } /** * Gets the wrongly configured rule * * @return the wrongly configured rule */ public Rule rule() { return rule; } /** * Gets the reason for the configuration error. * * @return the issue */ public String issue() { return issue; } } /** * Represents a processing error, such as a parse error. */ public static class ProcessingError { private final String msg; private final String file; /** * Creates a new processing error * * @param msg * the error message * @param file * the file during which the error occurred */ public ProcessingError(String msg, String file) { this.msg = msg; this.file = file; } public String getMsg() { return msg; } public String getFile() { return file; } } /** * Represents a violation, that has been suppressed. */ public static class SuppressedViolation { private final RuleViolation rv; private final boolean isNOPMD; private final String userMessage; /** * Creates a suppressed violation. * * @param rv * the actual violation, that has been suppressed * @param isNOPMD * the suppression mode: <code>true</code> if it is * suppressed via a NOPMD comment, <code>false</code> if * suppressed via annotations. * @param userMessage * contains the suppressed code line or <code>null</code> */ public SuppressedViolation(RuleViolation rv, boolean isNOPMD, String userMessage) { this.isNOPMD = isNOPMD; this.rv = rv; this.userMessage = userMessage; } /** * Returns <code>true</code> if the violation has been suppressed via a * NOPMD comment. * * @return <code>true</code> if the violation has been suppressed via a * NOPMD comment. */ public boolean suppressedByNOPMD() { return this.isNOPMD; } /** * Returns <code>true</code> if the violation has been suppressed via a * annotation. * * @return <code>true</code> if the violation has been suppressed via a * annotation. */ public boolean suppressedByAnnotation() { return !this.isNOPMD; } public RuleViolation getRuleViolation() { return this.rv; } public String getUserMessage() { return userMessage; } } /** * Configure the lines, that are suppressed via a NOPMD comment. * * @param lines * the suppressed lines */ public void suppress(Map<Integer, String> lines) { linesToSuppress = lines; } private static String keyFor(RuleViolation rv) { return StringUtil.isNotEmpty(rv.getPackageName()) ? rv.getPackageName() + '.' + rv.getClassName() : ""; } /** * Calculate a summary of violation counts per fully classified class name. * * @return violations per class name */ public Map<String, Integer> getCountSummary() { Map<String, Integer> summary = new HashMap<>(); for (RuleViolation rv : violationTree) { String key = keyFor(rv); Integer o = summary.get(key); summary.put(key, o == null ? NumericConstants.ONE : o + 1); } return summary; } public ReportTree getViolationTree() { return this.violationTree; } /** * Calculate a summary of violations per rule. * * @return a Map summarizing the Report: String (rule name) -> Integer (count * of violations) */ public Map<String, Integer> getSummary() { Map<String, Integer> summary = new HashMap<>(); for (RuleViolation rv : violations) { String name = rv.getRule().getName(); if (!summary.containsKey(name)) { summary.put(name, NumericConstants.ZERO); } Integer count = summary.get(name); summary.put(name, count + 1); } return summary; } /** * Registers a report listener * * @param listener * the listener * @deprecated Use {@link #addListener(ThreadSafeReportListener)} */ @Deprecated public void addListener(ReportListener listener) { listeners.add(new SynchronizedReportListener(listener)); } /** * Registers a report listener * * @param listener * the listener */ public void addListener(ThreadSafeReportListener listener) { listeners.add(listener); } public List<SuppressedViolation> getSuppressedRuleViolations() { return suppressedRuleViolations; } /** * Adds a new rule violation to the report and notify the listeners. * * @param violation * the violation to add */ public void addRuleViolation(RuleViolation violation) { // NOPMD suppress int line = violation.getBeginLine(); if (linesToSuppress.containsKey(line)) { suppressedRuleViolations.add(new SuppressedViolation(violation, true, linesToSuppress.get(line))); return; } if (violation.isSuppressed()) { suppressedRuleViolations.add(new SuppressedViolation(violation, false, null)); return; } int index = Collections.binarySearch(violations, violation, RuleViolationComparator.INSTANCE); violations.add(index < 0 ? -index - 1 : index, violation); violationTree.addRuleViolation(violation); for (ThreadSafeReportListener listener : listeners) { listener.ruleViolationAdded(violation); } } /** * Adds a new metric to the report and notify the listeners * * @param metric * the metric to add */ public void addMetric(Metric metric) { metrics.add(metric); for (ThreadSafeReportListener listener : listeners) { listener.metricAdded(metric); } } /** * Adds a new configuration error to the report. * * @param error * the error to add */ public void addConfigError(RuleConfigurationError error) { if (configErrors == null) { configErrors = new ArrayList<>(); } configErrors.add(error); } /** * Adds a new processing error to the report. * * @param error * the error to add */ public void addError(ProcessingError error) { if (errors == null) { errors = new ArrayList<>(); } errors.add(error); } /** * Merges the given report into this report. This might be necessary, if a * summary over all violations is needed as PMD creates one report per file * by default. * * @param r * the report to be merged into this. * @see AbstractAccumulatingRenderer */ public void merge(Report r) { Iterator<ProcessingError> i = r.errors(); while (i.hasNext()) { addError(i.next()); } Iterator<Metric> m = r.metrics(); while (m.hasNext()) { addMetric(m.next()); } Iterator<RuleViolation> v = r.iterator(); while (v.hasNext()) { RuleViolation violation = v.next(); int index = Collections.binarySearch(violations, violation, RuleViolationComparator.INSTANCE); violations.add(index < 0 ? -index - 1 : index, violation); violationTree.addRuleViolation(violation); } Iterator<SuppressedViolation> s = r.getSuppressedRuleViolations().iterator(); while (s.hasNext()) { suppressedRuleViolations.add(s.next()); } } /** * Check whether any metrics have been reported * * @return <code>true</code> if there are metrics, <code>false</code> * otherwise */ public boolean hasMetrics() { return !metrics.isEmpty(); } /** * Iterate over the metrics. * * @return an iterator over the metrics */ public Iterator<Metric> metrics() { return metrics.iterator(); } public boolean isEmpty() { return !violations.iterator().hasNext() && !hasErrors(); } /** * Checks whether any processing errors have been reported. * * @return <code>true</code> if there were any processing errors, * <code>false</code> otherwise */ public boolean hasErrors() { return errors != null && !errors.isEmpty(); } /** * Checks whether any configuration errors have been reported. * * @return <code>true</code> if there were any configuration errors, * <code>false</code> otherwise */ public boolean hasConfigErrors() { return configErrors != null && !configErrors.isEmpty(); } /** * Checks whether no violations have been reported. * * @return <code>true</code> if no violations have been reported, * <code>false</code> otherwise */ public boolean treeIsEmpty() { return !violationTree.iterator().hasNext(); } /** * Returns an iteration over the reported violations. * * @return an iterator */ public Iterator<RuleViolation> treeIterator() { return violationTree.iterator(); } @Override public Iterator<RuleViolation> iterator() { return violations.iterator(); } /** * Returns an iterator of the reported processing errors. * * @return the iterator */ public Iterator<ProcessingError> errors() { return errors == null ? EmptyIterator.<ProcessingError>instance() : errors.iterator(); } /** * Returns an iterator of the reported configuration errors. * * @return the iterator */ public Iterator<RuleConfigurationError> configErrors() { return configErrors == null ? EmptyIterator.<RuleConfigurationError>instance() : configErrors.iterator(); } /** * The number of violations. * * @return number of violations. */ public int treeSize() { return violationTree.size(); } /** * The number of violations. * * @return number of violations. */ public int size() { return violations.size(); } /** * Mark the start time of the report. This is used to get the elapsed time * in the end. * * @see #getElapsedTimeInMillis() */ public void start() { start = System.currentTimeMillis(); } /** * Mark the end time of the report. This is ued to get the elapsed time. * * @see #getElapsedTimeInMillis() */ public void end() { end = System.currentTimeMillis(); } public long getElapsedTimeInMillis() { return end - start; } public List<ThreadSafeReportListener> getListeners() { return listeners; } /** * Adds all given listeners to this report * * @param allListeners * the report listeners */ public void addListeners(List<ThreadSafeReportListener> allListeners) { listeners.addAll(allListeners); } }