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<>();
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<>(0);
45      private List<String> includePatterns = new ArrayList<>(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<>(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 (RuntimeException e) {
313                 if (ctx.isIgnoreExceptions()) {
314                     if (LOG.isLoggable(Level.WARNING)) {
315                         LOG.log(Level.WARNING, "Exception applying rule " + rule.getName()
316                             + " on file " + ctx.getSourceCodeFilename() + ", continuing with next rule",
317                             e);
318                     }
319                 } else {
320                     throw e;
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.getMaximumLanguageVersion();
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 
422         excludePatterns.add(aPattern);
423         patternsChanged();
424     }
425 
426     /**
427      * Adds new file exclusion patterns.
428      *
429      * @param someExcludePatterns the patterns
430      */
431     public void addExcludePatterns(Collection<String> someExcludePatterns) {
432         int added = CollectionUtil.addWithoutDuplicates(someExcludePatterns, excludePatterns);
433         if (added > 0) {
434             patternsChanged();
435         }
436     }
437 
438     /**
439      * Replaces the existing exclusion patterns with the given patterns.
440      *
441      * @param theExcludePatterns the new patterns
442      */
443     public void setExcludePatterns(Collection<String> theExcludePatterns) {
444         if (excludePatterns.equals(theExcludePatterns)) {
445             return;
446         }
447 
448         excludePatterns.clear();
449         CollectionUtil.addWithoutDuplicates(theExcludePatterns, excludePatterns);
450         patternsChanged();
451     }
452 
453     public List<String> getIncludePatterns() {
454         return includePatterns;
455     }
456 
457     /**
458      * Adds a new inclusion pattern.
459      *
460      * @param aPattern the pattern
461      */
462     public void addIncludePattern(String aPattern) {
463         if (includePatterns.contains(aPattern)) {
464             return;
465         }
466 
467         includePatterns.add(aPattern);
468         patternsChanged();
469     }
470 
471     /**
472      * Adds new inclusion patterns.
473      *
474      * @param someIncludePatterns the patterns
475      */
476     public void addIncludePatterns(Collection<String> someIncludePatterns) {
477         int added = CollectionUtil.addWithoutDuplicates(someIncludePatterns, includePatterns);
478         if (added > 0) {
479             patternsChanged();
480         }
481     }
482 
483     /**
484      * Replaces the existing inclusion patterns with the given patterns.
485      *
486      * @param theIncludePatterns the new patterns
487      */
488     public void setIncludePatterns(Collection<String> theIncludePatterns) {
489         if (includePatterns.equals(theIncludePatterns)) {
490             return;
491         }
492 
493         includePatterns.clear();
494         CollectionUtil.addWithoutDuplicates(theIncludePatterns, includePatterns);
495         patternsChanged();
496     }
497 
498     private void patternsChanged() {
499         filter = null; // ensure we start with one that reflects the current
500                        // patterns
501     }
502 
503     /**
504      * Does any Rule for the given Language use Type Resolution?
505      *
506      * @param language The Language.
507      * @return <code>true</code> if a Rule for the Language uses Type
508      *         Resolution, <code>false</code> otherwise.
509      */
510     public boolean usesTypeResolution(Language language) {
511         for (Rule r : rules) {
512             if (r.getLanguage().equals(language)) {
513                 if (r.usesTypeResolution()) {
514                     return true;
515                 }
516             }
517         }
518         return false;
519     }
520 
521     /**
522      * Remove and collect any misconfigured rules.
523      *
524      * @param collector the removed rules will be added to this collection
525      */
526     public void removeDysfunctionalRules(Collection<Rule> collector) {
527         Iterator<Rule> iter = rules.iterator();
528 
529         while (iter.hasNext()) {
530             Rule rule = iter.next();
531             if (rule.dysfunctionReason() != null) {
532                 iter.remove();
533                 collector.add(rule);
534             }
535         }
536     }
537 }