View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd;
5   
6   import java.io.File;
7   import java.util.ArrayList;
8   import java.util.Collections;
9   import java.util.HashMap;
10  import java.util.HashSet;
11  import java.util.Iterator;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.Set;
15  
16  import net.sourceforge.pmd.lang.dfa.report.ReportTree;
17  import net.sourceforge.pmd.renderers.AbstractAccumulatingRenderer;
18  import net.sourceforge.pmd.stat.Metric;
19  import net.sourceforge.pmd.util.DateTimeUtil;
20  import net.sourceforge.pmd.util.EmptyIterator;
21  import net.sourceforge.pmd.util.NumericConstants;
22  import net.sourceforge.pmd.util.StringUtil;
23  
24  /**
25   * A {@link Report} collects all informations during a PMD execution. This
26   * includes violations, suppressed violations, metrics, error during processing
27   * and configuration errors.
28   */
29  public class Report implements Iterable<RuleViolation> {
30  
31      /**
32       * Creates a new, initialized, empty report for the given file name.
33       *
34       * @param ctx The context to use to connect to the report
35       * @param fileName the filename used to report any violations
36       * @return the new report
37       */
38      public static Report createReport(RuleContext ctx, String fileName) {
39          Report report = new Report();
40  
41          // overtake the listener
42          report.addSynchronizedListeners(ctx.getReport().getSynchronizedListeners());
43  
44          ctx.setReport(report);
45          ctx.setSourceCodeFilename(fileName);
46          ctx.setSourceCodeFile(new File(fileName));
47          return report;
48      }
49  
50      /**
51       * Represents a duration. Useful for reporting processing time.
52       */
53      public static class ReadableDuration {
54          private final long duration;
55  
56          /**
57           * Creates a new duration.
58           *
59           * @param duration the duration in milliseconds.
60           */
61          public ReadableDuration(long duration) {
62              this.duration = duration;
63          }
64  
65          /**
66           * Gets a human readable representation of the duration, such as
67           * "1h 3m 5s".
68           *
69           * @return human readable representation of the duration
70           */
71          public String getTime() {
72              return DateTimeUtil.asHoursMinutesSeconds(duration);
73          }
74      }
75  
76      /**
77       * Represents a configuration error.
78       */
79      public static class RuleConfigurationError {
80          private final Rule rule;
81          private final String issue;
82  
83          /**
84           * Creates a new configuration error.
85           *
86           * @param theRule the rule which is configured wrongly
87           * @param theIssue the reason, why the configuration is wrong
88           */
89          public RuleConfigurationError(Rule theRule, String theIssue) {
90              rule = theRule;
91              issue = theIssue;
92          }
93  
94          /**
95           * Gets the wrongly configured rule
96           *
97           * @return the wrongly configured rule
98           */
99          public Rule rule() {
100             return rule;
101         }
102 
103         /**
104          * Gets the reason for the configuration error.
105          *
106          * @return the issue
107          */
108         public String issue() {
109             return issue;
110         }
111     }
112 
113     /**
114      * Represents a processing error, such as a parse error.
115      */
116     public static class ProcessingError {
117         private final String msg;
118         private final String file;
119 
120         /**
121          * Creates a new processing error
122          *
123          * @param msg the error message
124          * @param file the file during which the error occurred
125          */
126         public ProcessingError(String msg, String file) {
127             this.msg = msg;
128             this.file = file;
129         }
130 
131         public String getMsg() {
132             return msg;
133         }
134 
135         public String getFile() {
136             return file;
137         }
138     }
139 
140     /**
141      * Represents a violation, that has been suppressed.
142      */
143     public static class SuppressedViolation {
144         private final RuleViolation rv;
145         private final boolean isNOPMD;
146         private final String userMessage;
147 
148         /**
149          * Creates a suppressed violation.
150          *
151          * @param rv the actual violation, that has been suppressed
152          * @param isNOPMD the suppression mode: <code>true</code> if it is
153          *            suppressed via a NOPMD comment, <code>false</code> if
154          *            suppressed via annotations.
155          * @param userMessage contains the suppressed code line or
156          *            <code>null</code>
157          */
158         public SuppressedViolation(RuleViolation rv, boolean isNOPMD, String userMessage) {
159             this.isNOPMD = isNOPMD;
160             this.rv = rv;
161             this.userMessage = userMessage;
162         }
163 
164         /**
165          * Returns <code>true</code> if the violation has been suppressed via a
166          * NOPMD comment.
167          *
168          * @return <code>true</code> if the violation has been suppressed via a
169          *         NOPMD comment.
170          */
171         public boolean suppressedByNOPMD() {
172             return this.isNOPMD;
173         }
174 
175         /**
176          * Returns <code>true</code> if the violation has been suppressed via a
177          * annotation.
178          *
179          * @return <code>true</code> if the violation has been suppressed via a
180          *         annotation.
181          */
182         public boolean suppressedByAnnotation() {
183             return !this.isNOPMD;
184         }
185 
186         public RuleViolation getRuleViolation() {
187             return this.rv;
188         }
189 
190         public String getUserMessage() {
191             return userMessage;
192         }
193     }
194 
195     /*
196      * The idea is to store the violations in a tree instead of a list, to do
197      * better and faster sort and filter mechanism and to visualize the result
198      * as tree. (ide plugins).
199      */
200     private final ReportTree violationTree = new ReportTree();
201 
202     // Note that this and the above data structure are both being maintained for
203     // a bit
204     private final List<RuleViolation> violations = new ArrayList<>();
205     private final Set<Metric> metrics = new HashSet<>();
206     private final List<SynchronizedReportListener> listeners = new ArrayList<>();
207     private List<ProcessingError> errors;
208     private List<RuleConfigurationError> configErrors;
209     private Map<Integer, String> linesToSuppress = new HashMap<>();
210     private long start;
211     private long end;
212 
213     private List<SuppressedViolation> suppressedRuleViolations = new ArrayList<>();
214 
215     /**
216      * Configure the lines, that are suppressed via a NOPMD comment.
217      *
218      * @param lines the suppressed lines
219      */
220     public void suppress(Map<Integer, String> lines) {
221         linesToSuppress = lines;
222     }
223 
224     private static String keyFor(RuleViolation rv) {
225 
226         return StringUtil.isNotEmpty(rv.getPackageName()) ? rv.getPackageName() + '.' + rv.getClassName() : "";
227     }
228 
229     /**
230      * Calculate a summary of violation counts per fully classified class name.
231      *
232      * @return violations per class name
233      */
234     public Map<String, Integer> getCountSummary() {
235         Map<String, Integer> summary = new HashMap<>();
236         for (RuleViolation rv : violationTree) {
237             String key = keyFor(rv);
238             Integer o = summary.get(key);
239             summary.put(key, o == null ? NumericConstants.ONE : o + 1);
240         }
241         return summary;
242     }
243 
244     public ReportTree getViolationTree() {
245         return this.violationTree;
246     }
247 
248     /**
249      * Calculate a summary of violations per rule.
250      *
251      * @return a Map summarizing the Report: String (rule name) ->Integer (count
252      *         of violations)
253      */
254     public Map<String, Integer> getSummary() {
255         Map<String, Integer> summary = new HashMap<>();
256         for (RuleViolation rv : violations) {
257             String name = rv.getRule().getName();
258             if (!summary.containsKey(name)) {
259                 summary.put(name, NumericConstants.ZERO);
260             }
261             Integer count = summary.get(name);
262             summary.put(name, count + 1);
263         }
264         return summary;
265     }
266 
267     /**
268      * Registers a report listener
269      *
270      * @param listener the listener
271      */
272     public void addListener(ReportListener listener) {
273         listeners.add(new SynchronizedReportListener(listener));
274     }
275 
276     public List<SuppressedViolation> getSuppressedRuleViolations() {
277         return suppressedRuleViolations;
278     }
279 
280     /**
281      * Adds a new rule violation to the report and notify the listeners.
282      *
283      * @param violation the violation to add
284      */
285     public void addRuleViolation(RuleViolation violation) {
286 
287         // NOPMD suppress
288         int line = violation.getBeginLine();
289         if (linesToSuppress.containsKey(line)) {
290             suppressedRuleViolations.add(new SuppressedViolation(violation, true, linesToSuppress.get(line)));
291             return;
292         }
293 
294         if (violation.isSuppressed()) {
295             suppressedRuleViolations.add(new SuppressedViolation(violation, false, null));
296             return;
297         }
298 
299         int index = Collections.binarySearch(violations, violation, RuleViolationComparator.INSTANCE);
300         violations.add(index < 0 ? -index - 1 : index, violation);
301         violationTree.addRuleViolation(violation);
302         for (ReportListener listener : listeners) {
303             listener.ruleViolationAdded(violation);
304         }
305     }
306 
307     /**
308      * Adds a new metric to the report and notify the listeners
309      *
310      * @param metric the metric to add
311      */
312     public void addMetric(Metric metric) {
313         metrics.add(metric);
314         for (ReportListener listener : listeners) {
315             listener.metricAdded(metric);
316         }
317     }
318 
319     /**
320      * Adds a new configuration error to the report.
321      *
322      * @param error the error to add
323      */
324     public void addConfigError(RuleConfigurationError error) {
325         if (configErrors == null) {
326             configErrors = new ArrayList<>();
327         }
328         configErrors.add(error);
329     }
330 
331     /**
332      * Adds a new processing error to the report.
333      *
334      * @param error the error to add
335      */
336     public void addError(ProcessingError error) {
337         if (errors == null) {
338             errors = new ArrayList<>();
339         }
340         errors.add(error);
341     }
342 
343     /**
344      * Merges the given report into this report. This might be necessary, if a
345      * summary over all violations is needed as PMD creates one report per file
346      * by default.
347      *
348      * @param r the report to be merged into this.
349      * @see AbstractAccumulatingRenderer
350      */
351     public void merge(Report r) {
352         Iterator<ProcessingError> i = r.errors();
353         while (i.hasNext()) {
354             addError(i.next());
355         }
356         Iterator<Metric> m = r.metrics();
357         while (m.hasNext()) {
358             addMetric(m.next());
359         }
360         Iterator<RuleViolation> v = r.iterator();
361         while (v.hasNext()) {
362             RuleViolation violation = v.next();
363             int index = Collections.binarySearch(violations, violation, RuleViolationComparator.INSTANCE);
364             violations.add(index < 0 ? -index - 1 : index, violation);
365             violationTree.addRuleViolation(violation);
366         }
367         Iterator<SuppressedViolation> s = r.getSuppressedRuleViolations().iterator();
368         while (s.hasNext()) {
369             suppressedRuleViolations.add(s.next());
370         }
371     }
372 
373     /**
374      * Check whether any metrics have been reported
375      *
376      * @return <code>true</code> if there are metrics, <code>false</code>
377      *         otherwise
378      */
379     public boolean hasMetrics() {
380         return !metrics.isEmpty();
381     }
382 
383     /**
384      * Iterate over the metrics.
385      *
386      * @return an iterator over the metrics
387      */
388     public Iterator<Metric> metrics() {
389         return metrics.iterator();
390     }
391 
392     public boolean isEmpty() {
393         return !violations.iterator().hasNext() && !hasErrors();
394     }
395 
396     /**
397      * Checks whether any processing errors have been reported.
398      *
399      * @return <code>true</code> if there were any processing errors,
400      *         <code>false</code> otherwise
401      */
402     public boolean hasErrors() {
403         return errors != null && !errors.isEmpty();
404     }
405 
406     /**
407      * Checks whether any configuration errors have been reported.
408      *
409      * @return <code>true</code> if there were any configuration errors,
410      *         <code>false</code> otherwise
411      */
412     public boolean hasConfigErrors() {
413         return configErrors != null && !configErrors.isEmpty();
414     }
415 
416     /**
417      * Checks whether no violations have been reported.
418      *
419      * @return <code>true</code> if no violations have been reported,
420      *         <code>false</code> otherwise
421      */
422     public boolean treeIsEmpty() {
423         return !violationTree.iterator().hasNext();
424     }
425 
426     /**
427      * Returns an iteration over the reported violations.
428      *
429      * @return an iterator
430      */
431     public Iterator<RuleViolation> treeIterator() {
432         return violationTree.iterator();
433     }
434 
435     @Override
436     public Iterator<RuleViolation> iterator() {
437         return violations.iterator();
438     }
439 
440     /**
441      * Returns an iterator of the reported processing errors.
442      *
443      * @return the iterator
444      */
445     public Iterator<ProcessingError> errors() {
446         return errors == null ? EmptyIterator.<ProcessingError> instance() : errors.iterator();
447     }
448 
449     /**
450      * Returns an iterator of the reported configuration errors.
451      *
452      * @return the iterator
453      */
454     public Iterator<RuleConfigurationError> configErrors() {
455         return configErrors == null ? EmptyIterator.<RuleConfigurationError> instance() : configErrors.iterator();
456     }
457 
458     /**
459      * The number of violations.
460      *
461      * @return number of violations.
462      */
463     public int treeSize() {
464         return violationTree.size();
465     }
466 
467     /**
468      * The number of violations.
469      *
470      * @return number of violations.
471      */
472     public int size() {
473         return violations.size();
474     }
475 
476     /**
477      * Mark the start time of the report. This is used to get the elapsed time
478      * in the end.
479      *
480      * @see #getElapsedTimeInMillis()
481      */
482     public void start() {
483         start = System.currentTimeMillis();
484     }
485 
486     /**
487      * Mark the end time of the report. This is ued to get the elapsed time.
488      *
489      * @see #getElapsedTimeInMillis()
490      */
491     public void end() {
492         end = System.currentTimeMillis();
493     }
494 
495     public long getElapsedTimeInMillis() {
496         return end - start;
497     }
498 
499     public List<SynchronizedReportListener> getSynchronizedListeners() {
500         return listeners;
501     }
502 
503     /**
504      * Adds all given listeners to this report
505      *
506      * @param synchronizedListeners the report listeners
507      */
508     public void addSynchronizedListeners(List<SynchronizedReportListener> synchronizedListeners) {
509         listeners.addAll(synchronizedListeners);
510     }
511 }