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.Arrays;
9   import java.util.Collection;
10  import java.util.HashSet;
11  import java.util.Iterator;
12  import java.util.List;
13  import java.util.logging.Level;
14  import java.util.logging.Logger;
15  
16  import net.sourceforge.pmd.benchmark.Benchmark;
17  import net.sourceforge.pmd.benchmark.Benchmarker;
18  import net.sourceforge.pmd.lang.Language;
19  import net.sourceforge.pmd.lang.LanguageVersion;
20  import net.sourceforge.pmd.lang.ast.Node;
21  import net.sourceforge.pmd.lang.rule.RuleReference;
22  import net.sourceforge.pmd.util.CollectionUtil;
23  import net.sourceforge.pmd.util.StringUtil;
24  import net.sourceforge.pmd.util.filter.Filter;
25  import net.sourceforge.pmd.util.filter.Filters;
26  
27  /**
28   * This class represents a collection of rules along with some optional filter
29   * patterns that can preclude their application on specific files.
30   *
31   * @see Rule
32   */
33  // FUTURE Implement Cloneable and clone()
34  public class RuleSet {
35  
36      private static final Logger LOG = Logger.getLogger(RuleSet.class.getName());
37  
38      private List<Rule> rules = new ArrayList<Rule>();
39      private String fileName;
40      private String name = "";
41      private String description = "";
42  
43      // TODO should these not be Sets or is their order important?
44      private List<String> excludePatterns = new ArrayList<String>(0);
45      private List<String> includePatterns = new ArrayList<String>(0);
46  
47      private Filter<File> filter;
48  
49      /**
50       * A convenience constructor
51       *
52       * @param name the rule set name
53       * @param theRules the rules to add to the rule set
54       * @return a new rule set with the given rules added
55       */
56      public static RuleSet createFor(String name, Rule... theRules) {
57  
58          RuleSet rs = new RuleSet();
59          rs.setName(name);
60          for (Rule rule : theRules) {
61              rs.addRule(rule);
62          }
63          return rs;
64      }
65  
66      /**
67       * Returns the number of rules in this ruleset
68       *
69       * @return an int representing the number of rules
70       */
71      public int size() {
72          return rules.size();
73      }
74  
75      /**
76       * Add a new rule to this ruleset. Note that this method does not check for
77       * duplicates.
78       *
79       * @param rule the rule to be added
80       */
81      public void addRule(Rule rule) {
82          if (rule == null) {
83              throw new IllegalArgumentException("Missing rule");
84          }
85          rules.add(rule);
86      }
87  
88      /**
89       * Adds a rule. If a rule with the same name and language already existed
90       * before in the ruleset, then the new rule will replace it. This makes sure
91       * that the rule configured is overridden.
92       *
93       * @param rule the new rule to add
94       * @return <code>true</code> if the new rule replaced an existing one,
95       *         otherwise <code>false</code>.
96       */
97      public boolean addRuleReplaceIfExists(Rule rule) {
98          if (rule == null) {
99              throw new IllegalArgumentException("Missing rule");
100         }
101 
102         boolean replaced = false;
103         for (Iterator<Rule> it = rules.iterator(); it.hasNext();) {
104             Rule r = it.next();
105             if (r.getName().equals(rule.getName()) && r.getLanguage() == rule.getLanguage()) {
106                 it.remove();
107                 replaced = true;
108             }
109         }
110         addRule(rule);
111         return replaced;
112     }
113 
114     /**
115      * Only adds a rule to the ruleset if no rule with the same name for the
116      * same language was added before, so that the existent rule configuration
117      * won't be overridden.
118      *
119      * @param rule the new rule to add
120      * @return <code>true</code> if the rule was added, <code>false</code>
121      *         otherwise
122      */
123     public boolean addRuleIfNotExists(Rule rule) {
124         if (rule == null) {
125             throw new IllegalArgumentException("Missing rule");
126         }
127 
128         boolean exists = false;
129         for (Rule r : rules) {
130             if (r.getName().equals(rule.getName()) && r.getLanguage() == rule.getLanguage()) {
131                 exists = true;
132                 break;
133             }
134         }
135         if (!exists) {
136             addRule(rule);
137         }
138         return !exists;
139     }
140 
141     /**
142      * Add a new rule by reference to this ruleset.
143      *
144      * @param ruleSetFileName the ruleset which contains the rule
145      * @param rule the rule to be added
146      */
147     public void addRuleByReference(String ruleSetFileName, Rule rule) {
148         if (StringUtil.isEmpty(ruleSetFileName)) {
149             throw new RuntimeException("Adding a rule by reference is not allowed with an empty rule set file name.");
150         }
151         if (rule == null) {
152             throw new IllegalArgumentException("Cannot add a null rule reference to a RuleSet");
153         }
154         RuleReference ruleReference;
155         if (rule instanceof RuleReference) {
156             ruleReference = (RuleReference) rule;
157         } else {
158             RuleSetReference ruleSetReference = new RuleSetReference();
159             ruleSetReference.setRuleSetFileName(ruleSetFileName);
160             ruleReference = new RuleReference();
161             ruleReference.setRule(rule);
162             ruleReference.setRuleSetReference(ruleSetReference);
163         }
164         rules.add(ruleReference);
165     }
166 
167     /**
168      * Returns the actual Collection of rules in this ruleset
169      *
170      * @return a Collection with the rules. All objects are of type {@link Rule}
171      */
172     public Collection<Rule> getRules() {
173         return rules;
174     }
175 
176     /**
177      * Does any Rule for the given Language use the DFA layer?
178      *
179      * @param language The Language.
180      * @return <code>true</code> if a Rule for the Language uses the DFA layer,
181      *         <code>false</code> otherwise.
182      */
183     public boolean usesDFA(Language language) {
184         for (Rule r : rules) {
185             if (r.getLanguage().equals(language)) {
186                 if (r.usesDFA()) {
187                     return true;
188                 }
189             }
190         }
191         return false;
192     }
193 
194     /**
195      * Returns the first Rule found with the given name (case-sensitive).
196      *
197      * Note: Since we support multiple languages, rule names are not expected to
198      * be unique within any specific ruleset.
199      *
200      * @param ruleName the exact name of the rule to find
201      * @return the rule or null if not found
202      */
203     public Rule getRuleByName(String ruleName) {
204 
205         for (Rule r : rules) {
206             if (r.getName().equals(ruleName)) {
207                 return r;
208             }
209         }
210         return null;
211     }
212 
213     /**
214      * Add a whole RuleSet to this RuleSet
215      *
216      * @param ruleSet the RuleSet to add
217      */
218     public void addRuleSet(RuleSet ruleSet) {
219         rules.addAll(rules.size(), ruleSet.getRules());
220     }
221 
222     /**
223      * Add all rules by reference from one RuleSet to this RuleSet. The rules
224      * can be added as individual references, or collectively as an all rule
225      * reference.
226      *
227      * @param ruleSet the RuleSet to add
228      * @param allRules <code>true</code> if the ruleset should be added
229      *            collectively or <code>false</code> to add individual
230      *            references for each rule.
231      */
232     public void addRuleSetByReference(RuleSet ruleSet, boolean allRules) {
233         addRuleSetByReference(ruleSet, allRules, (String[]) null);
234     }
235 
236     /**
237      * Add all rules by reference from one RuleSet to this RuleSet. The rules
238      * can be added as individual references, or collectively as an all rule
239      * reference.
240      *
241      * @param ruleSet the RuleSet to add
242      * @param allRules <code>true</code> if the ruleset should be added
243      *            collectively or <code>false</code> to add individual
244      *            references for each rule.
245      * @param excludes names of the rules that should be excluded.
246      */
247     public void addRuleSetByReference(RuleSet ruleSet, boolean allRules, String... excludes) {
248         if (StringUtil.isEmpty(ruleSet.getFileName())) {
249             throw new RuntimeException("Adding a rule by reference is not allowed with an empty rule set file name.");
250         }
251         RuleSetReference ruleSetReference = new RuleSetReference(ruleSet.getFileName());
252         ruleSetReference.setAllRules(allRules);
253         if (excludes != null) {
254             ruleSetReference.setExcludes(new HashSet<String>(Arrays.asList(excludes)));
255         }
256         for (Rule rule : ruleSet.getRules()) {
257             RuleReference ruleReference = new RuleReference(rule, ruleSetReference);
258             rules.add(ruleReference);
259         }
260     }
261 
262     /**
263      * Check if a given source file should be checked by rules in this RuleSet.
264      * A file should not be checked if there is an <code>exclude</code> pattern
265      * which matches the file, unless there is an <code>include</code> pattern
266      * which also matches the file. In other words, <code>include</code>
267      * patterns override <code>exclude</code> patterns.
268      *
269      * @param file the source file to check
270      * @return <code>true</code> if the file should be checked,
271      *         <code>false</code> otherwise
272      */
273     public boolean applies(File file) {
274         // Initialize filter based on patterns
275         if (filter == null) {
276             Filter<String> regexFilter = Filters.buildRegexFilterIncludeOverExclude(includePatterns, excludePatterns);
277             filter = Filters.toNormalizedFileFilter(regexFilter);
278         }
279 
280         return file != null ? filter.filter(file) : true;
281     }
282 
283     /**
284      * Triggers that start lifecycle event on each rule in this ruleset. Some
285      * rules perform initialization tasks on start.
286      *
287      * @param ctx the current context
288      */
289     public void start(RuleContext ctx) {
290         for (Rule rule : rules) {
291             rule.start(ctx);
292         }
293     }
294 
295     /**
296      * Executes the rules in this ruleset against each of the given nodes.
297      *
298      * @param acuList the node list, usually the root nodes like compilation
299      *            units
300      * @param ctx the current context
301      */
302     public void apply(List<? extends Node> acuList, RuleContext ctx) {
303         long start = System.nanoTime();
304         for (Rule rule : rules) {
305             try {
306                 if (!rule.usesRuleChain() && applies(rule, ctx.getLanguageVersion())) {
307                     rule.apply(acuList, ctx);
308                     long end = System.nanoTime();
309                     Benchmarker.mark(Benchmark.Rule, rule.getName(), end - start, 1);
310                     start = end;
311                 }
312             } catch (ThreadDeath td) {
313                 throw td;
314             } catch (Throwable t) {
315                 if (ctx.isIgnoreExceptions()) {
316                     LOG.log(Level.WARNING, "Exception applying rule " + rule.getName()
317                             + " on file " + ctx.getSourceCodeFilename() + ", continuing with next rule",
318                             t);
319                 } else {
320                     throw new RuntimeException(t);
321                 }
322             }
323         }
324     }
325 
326     /**
327      * Does the given Rule apply to the given LanguageVersion? If so, the
328      * Language must be the same and be between the minimum and maximums
329      * versions on the Rule.
330      *
331      * @param rule The rule.
332      * @param languageVersion The language version.
333      *
334      * @return <code>true</code> if the given rule matches the given language,
335      *         which means, that the rule would be executed.
336      */
337     public static boolean applies(Rule rule, LanguageVersion languageVersion) {
338         final LanguageVersion min = rule.getMinimumLanguageVersion();
339         final LanguageVersion max = rule.getMinimumLanguageVersion();
340         return rule.getLanguage().equals(languageVersion.getLanguage())
341                 && (min == null || min.compareTo(languageVersion) <= 0)
342                 && (max == null || max.compareTo(languageVersion) >= 0);
343     }
344 
345     /**
346      * Triggers the end lifecycle event on each rule in the ruleset. Some rules
347      * perform a final summary calculation or cleanup in the end.
348      *
349      * @param ctx the current context
350      */
351     public void end(RuleContext ctx) {
352         for (Rule rule : rules) {
353             rule.end(ctx);
354         }
355     }
356 
357     /**
358      * Two rulesets are equals, if they have the same name and contain the same
359      * rules.
360      *
361      * @param o the other ruleset to compare with
362      * @return <code>true</code> if o is a ruleset with the same name and rules,
363      *         <code>false</code> otherwise
364      */
365     @Override
366     public boolean equals(Object o) {
367         if (!(o instanceof RuleSet)) {
368             return false; // Trivial
369         }
370 
371         if (this == o) {
372             return true; // Basic equality
373         }
374 
375         RuleSet ruleSet = (RuleSet) o;
376         return getName().equals(ruleSet.getName()) && getRules().equals(ruleSet.getRules());
377     }
378 
379     @Override
380     public int hashCode() {
381         return getName().hashCode() + 13 * getRules().hashCode();
382     }
383 
384     public String getFileName() {
385         return fileName;
386     }
387 
388     public void setFileName(String fileName) {
389         this.fileName = fileName;
390     }
391 
392     public String getName() {
393         return name;
394     }
395 
396     public void setName(String name) {
397         this.name = name;
398     }
399 
400     public String getDescription() {
401         return description;
402     }
403 
404     public void setDescription(String description) {
405         this.description = description;
406     }
407 
408     public List<String> getExcludePatterns() {
409         return excludePatterns;
410     }
411 
412     /**
413      * Adds a new file exclusion pattern.
414      *
415      * @param aPattern the pattern
416      */
417     public void addExcludePattern(String aPattern) {
418         if (excludePatterns.contains(aPattern))
419             return;
420 
421         excludePatterns.add(aPattern);
422         patternsChanged();
423     }
424 
425     /**
426      * Adds new file exclusion patterns.
427      *
428      * @param someExcludePatterns the patterns
429      */
430     public void addExcludePatterns(Collection<String> someExcludePatterns) {
431         int added = CollectionUtil.addWithoutDuplicates(someExcludePatterns, excludePatterns);
432         if (added > 0)
433             patternsChanged();
434     }
435 
436     /**
437      * Replaces the existing exclusion patterns with the given patterns.
438      *
439      * @param theExcludePatterns the new patterns
440      */
441     public void setExcludePatterns(Collection<String> theExcludePatterns) {
442         if (excludePatterns.equals(theExcludePatterns))
443             return;
444 
445         excludePatterns.clear();
446         CollectionUtil.addWithoutDuplicates(theExcludePatterns, excludePatterns);
447         patternsChanged();
448     }
449 
450     public List<String> getIncludePatterns() {
451         return includePatterns;
452     }
453 
454     /**
455      * Adds a new inclusion pattern.
456      *
457      * @param aPattern the pattern
458      */
459     public void addIncludePattern(String aPattern) {
460 
461         if (includePatterns.contains(aPattern))
462             return;
463 
464         includePatterns.add(aPattern);
465         patternsChanged();
466     }
467 
468     /**
469      * Adds new inclusion patterns.
470      *
471      * @param someIncludePatterns the patterns
472      */
473     public void addIncludePatterns(Collection<String> someIncludePatterns) {
474 
475         int added = CollectionUtil.addWithoutDuplicates(someIncludePatterns, includePatterns);
476         if (added > 0)
477             patternsChanged();
478     }
479 
480     /**
481      * Replaces the existing inclusion patterns with the given patterns.
482      *
483      * @param theIncludePatterns the new patterns
484      */
485     public void setIncludePatterns(Collection<String> theIncludePatterns) {
486 
487         if (includePatterns.equals(theIncludePatterns))
488             return;
489 
490         includePatterns.clear();
491         CollectionUtil.addWithoutDuplicates(theIncludePatterns, includePatterns);
492         patternsChanged();
493     }
494 
495     private void patternsChanged() {
496         filter = null; // ensure we start with one that reflects the current
497                        // patterns
498     }
499 
500     /**
501      * Does any Rule for the given Language use Type Resolution?
502      *
503      * @param language The Language.
504      * @return <code>true</code> if a Rule for the Language uses Type
505      *         Resolution, <code>false</code> otherwise.
506      */
507     public boolean usesTypeResolution(Language language) {
508         for (Rule r : rules) {
509             if (r.getLanguage().equals(language)) {
510                 if (r.usesTypeResolution()) {
511                     return true;
512                 }
513             }
514         }
515         return false;
516     }
517 
518     /**
519      * Remove and collect any misconfigured rules.
520      *
521      * @param collector the removed rules will be added to this collection
522      */
523     public void removeDysfunctionalRules(Collection<Rule> collector) {
524 
525         Iterator<Rule> iter = rules.iterator();
526 
527         while (iter.hasNext()) {
528             Rule rule = iter.next();
529             if (rule.dysfunctionReason() != null) {
530                 iter.remove();
531                 collector.add(rule);
532             }
533         }
534     }
535 }