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
53  	 * @param theRules
54  	 * @return
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 duplicates.
77  	 *
78  	 * @param rule the rule to be added
79  	 */
80  	public void addRule(Rule rule) {
81  		if (rule == null) {
82  			throw new IllegalArgumentException("Missing rule");
83  		}
84  		rules.add(rule);
85  	}
86  
87  	/**
88  	 * Adds a rule. If a rule with the same name and language already existed before in the ruleset,
89  	 * then the new rule will replace it. This makes sure that the rule configured is overridden.
90  	 * @param rule
91  	 * @return <code>true</code> if the new rule replaced an existing one, otherwise <code>false</code>.
92  	 */
93  	public boolean addRuleReplaceIfExists(Rule rule) {
94          if (rule == null) {
95              throw new IllegalArgumentException("Missing rule");
96          }
97  
98          boolean replaced = false;
99          for (Iterator<Rule> it = rules.iterator(); it.hasNext(); ) {
100             Rule r = it.next();
101             if (r.getName().equals(rule.getName()) && r.getLanguage() == rule.getLanguage()) {
102                 it.remove();
103                 replaced = true;
104             }
105         }
106         addRule(rule);
107         return replaced;
108 	}
109 
110 	/**
111 	 * Only adds a rule to the ruleset if no rule with the same name for the same language was added
112 	 * before, so that the existent rule configuration won't be overridden.
113 	 * @param rule
114 	 * @return <code>true</code> if the rule was added, <code>false</code> otherwise
115 	 */
116 	public boolean addRuleIfNotExists(Rule rule) {
117         if (rule == null) {
118             throw new IllegalArgumentException("Missing rule");
119         }
120 
121         boolean exists = false;
122         for (Rule r : rules) {
123             if (r.getName().equals(rule.getName()) && r.getLanguage() == rule.getLanguage()) {
124                 exists = true;
125                 break;
126             }
127         }
128         if (!exists) {
129             addRule(rule);
130         }
131         return !exists;
132 	}
133 
134 	/**
135 	 * Add a new rule by reference to this ruleset.
136 	 *
137 	 * @param ruleSetFileName the ruleset which contains the rule
138 	 * @param rule the rule to be added
139 	 */
140 	public void addRuleByReference(String ruleSetFileName, Rule rule) {
141 		if (StringUtil.isEmpty(ruleSetFileName)) {
142 			throw new RuntimeException("Adding a rule by reference is not allowed with an empty rule set file name.");
143 		}
144 		if (rule == null) {
145 			throw new IllegalArgumentException("Cannot add a null rule reference to a RuleSet");
146 		}
147 		if (!(rule instanceof RuleReference)) {
148 			RuleSetReference ruleSetReference = new RuleSetReference();
149 			ruleSetReference.setRuleSetFileName(ruleSetFileName);
150 			RuleReference ruleReference = new RuleReference();
151 			ruleReference.setRule(rule);
152 			ruleReference.setRuleSetReference(ruleSetReference);
153 			rule = ruleReference;
154 		}
155 		rules.add(rule);
156 	}
157 
158 	/**
159 	 * Returns the actual Collection of rules in this ruleset
160 	 *
161 	 * @return a Collection with the rules. All objects are of type {@link Rule}
162 	 */
163 	public Collection<Rule> getRules() {
164 		return rules;
165 	}
166 
167 	/**
168 	 * Does any Rule for the given Language use the DFA layer?
169 	 * @param language The Language.
170 	 * @return <code>true</code> if a Rule for the Language uses the DFA layer,
171 	 * <code>false</code> otherwise.
172 	 */
173 	public boolean usesDFA(Language language) {
174 		for (Rule r : rules) {
175 			if (r.getLanguage().equals(language)) {
176 				if (r.usesDFA()) {
177 					return true;
178 				}
179 			}
180 		}
181 		return false;
182 	}
183 
184 	/**
185 	 * Returns the first Rule found with the given name (case-sensitive).
186 	 * 
187 	 * Note: Since we support multiple languages, rule names 
188 	 * are not expected to be unique within any specific
189 	 * ruleset.
190 	 *
191 	 * @param ruleName the exact name of the rule to find
192 	 * @return the rule or null if not found
193 	 */
194 	public Rule getRuleByName(String ruleName) {
195 		
196 		for (Rule r : rules) {
197 			if (r.getName().equals(ruleName)) {
198 				return r;
199 			}
200 		}
201 		return null;
202 	}
203 
204 	/**
205 	 * Add a whole RuleSet to this RuleSet
206 	 *
207 	 * @param ruleSet the RuleSet to add
208 	 */
209 	public void addRuleSet(RuleSet ruleSet) {
210 		rules.addAll(rules.size(), ruleSet.getRules());
211 	}
212 
213    /**
214      * Add all rules by reference from one RuleSet to this RuleSet.  The rules
215      * can be added as individual references, or collectively as an all rule
216      * reference.
217      *
218      * @param ruleSet the RuleSet to add
219      * @param allRules 
220      */
221     public void addRuleSetByReference(RuleSet ruleSet, boolean allRules) {
222         addRuleSetByReference(ruleSet, allRules, (String[])null);
223     }
224 
225 	/**
226 	 * Add all rules by reference from one RuleSet to this RuleSet.  The rules
227 	 * can be added as individual references, or collectively as an all rule
228 	 * reference.
229 	 *
230 	 * @param ruleSet the RuleSet to add
231 	 * @param allRules 
232 	 * @param excludes names of the rules that should be excluded.
233 	 */
234 	public void addRuleSetByReference(RuleSet ruleSet, boolean allRules, String ... excludes) {
235 		if (StringUtil.isEmpty(ruleSet.getFileName())) {
236 			throw new RuntimeException("Adding a rule by reference is not allowed with an empty rule set file name.");
237 		}
238 		RuleSetReference ruleSetReference = new RuleSetReference(ruleSet.getFileName());
239 		ruleSetReference.setAllRules(allRules);
240 		if (excludes != null) {
241 		    ruleSetReference.setExcludes(new HashSet<String>(Arrays.asList(excludes)));
242 		}
243 		for (Rule rule : ruleSet.getRules()) {
244 			RuleReference ruleReference = new RuleReference(rule, ruleSetReference);
245 			rules.add(ruleReference);
246 		}
247 	}
248 
249 	/**
250 	 * Check if a given source file should be checked by rules in this RuleSet.  A file
251 	 * should not be checked if there is an <code>exclude</code> pattern which matches
252 	 * the file, unless there is an <code>include</code> pattern which also matches
253 	 * the file.  In other words, <code>include</code> patterns override <code>exclude</code>
254 	 * patterns.
255 	 *
256 	 * @param file the source file to check
257 	 * @return <code>true</code> if the file should be checked, <code>false</code> otherwise
258 	 */
259 	public boolean applies(File file) {
260 		// Initialize filter based on patterns
261 		if (filter == null) {
262 			Filter<String> regexFilter = Filters.buildRegexFilterIncludeOverExclude(includePatterns, excludePatterns);
263 			filter = Filters.toNormalizedFileFilter(regexFilter);
264 		}
265 
266 		return file != null ? filter.filter(file) : true;
267 	}
268 
269 	public void start(RuleContext ctx) {
270 		for (Rule rule : rules) {
271 			rule.start(ctx);
272 		}
273 	}
274 
275 	public void apply(List<? extends Node> acuList, RuleContext ctx) {
276 		long start = System.nanoTime();
277 		for (Rule rule : rules) {
278             try {
279                 if (!rule.usesRuleChain() && applies(rule, ctx.getLanguageVersion())) {
280                     rule.apply(acuList, ctx);
281                     long end = System.nanoTime();
282                     Benchmarker.mark(Benchmark.Rule, rule.getName(), end - start, 1);
283                     start = end;
284                 }
285             } catch (ThreadDeath td) {
286                 throw td;
287             } catch (Throwable t) {
288                 LOG.log(Level.WARNING, "Exception applying rule " + rule.getName() + ", continuing with next rule", t);
289             }
290 		}
291 	}
292 
293 	/**
294 	 * Does the given Rule apply to the given LanguageVersion?  If so, the
295 	 * Language must be the same and be between the minimum and maximums
296 	 * versions on the Rule.
297 	 * 
298 	 * @param rule The rule.
299 	 * @param languageVersion The language version.
300 	 */
301 	public static boolean applies(Rule rule, LanguageVersion languageVersion) {
302 		final LanguageVersion min = rule.getMinimumLanguageVersion();
303 		final LanguageVersion max = rule.getMinimumLanguageVersion();
304 		return rule.getLanguage().equals(languageVersion.getLanguage())
305 		&& (min == null || min.compareTo(languageVersion) <= 0)
306 		&& (max == null || max.compareTo(languageVersion) >= 0);
307 	}
308 
309 	public void end(RuleContext ctx) {
310 		for (Rule rule : rules) {
311 			rule.end(ctx);
312 		}
313 	}
314 
315 	/**
316 	 * @see java.lang.Object#equals(java.lang.Object)
317 	 */
318 	@Override
319 	public boolean equals(Object o) {
320 		if (!(o instanceof RuleSet)) {
321 			return false; // Trivial
322 		}
323 
324 		if (this == o) {
325 			return true; // Basic equality
326 		}
327 
328 		RuleSet ruleSet = (RuleSet) o;
329 		return getName().equals(ruleSet.getName()) && getRules().equals(ruleSet.getRules());
330 	}
331 
332 	/**
333 	 * @see java.lang.Object#hashCode()
334 	 */
335 	@Override
336 	public int hashCode() {
337 		return getName().hashCode() + 13 * getRules().hashCode();
338 	}
339 
340 	public String getFileName() {
341 		return fileName;
342 	}
343 
344 	public void setFileName(String fileName) {
345 		this.fileName = fileName;
346 	}
347 
348 	public String getName() {
349 		return name;
350 	}
351 
352 	public void setName(String name) {
353 		this.name = name;
354 	}
355 
356 	public String getDescription() {
357 		return description;
358 	}
359 
360 	public void setDescription(String description) {
361 		this.description = description;
362 	}
363 
364 	public List<String> getExcludePatterns() {
365 		return excludePatterns;
366 	}
367 
368 	public void addExcludePattern(String aPattern) {
369 
370 		if (excludePatterns.contains(aPattern)) return;
371 		
372 		excludePatterns.add(aPattern);
373 		patternsChanged();
374 	}
375 	
376 	public void addExcludePatterns(Collection<String> someExcludePatterns) {
377 		
378 		int added = CollectionUtil.addWithoutDuplicates(someExcludePatterns, excludePatterns);
379 		if (added > 0) patternsChanged();
380 	}
381 
382 	public void setExcludePatterns(Collection<String> theExcludePatterns) {
383 		
384 		if (excludePatterns.equals(theExcludePatterns)) return;
385 		
386 		excludePatterns.clear();
387 		CollectionUtil.addWithoutDuplicates(theExcludePatterns, excludePatterns);
388 		patternsChanged();
389 	}
390 
391 	public List<String> getIncludePatterns() {
392 		return includePatterns;
393 	}
394 
395 	public void addIncludePattern(String aPattern) {
396 		
397 		if (includePatterns.contains(aPattern)) return;
398 		
399 		includePatterns.add(aPattern);
400 		patternsChanged();
401 	}
402 
403 	public void addIncludePatterns(Collection<String> someIncludePatterns) {
404 
405 		int added = CollectionUtil.addWithoutDuplicates(someIncludePatterns, includePatterns);
406 		if (added > 0) patternsChanged();
407 	}
408 
409 	public void setIncludePatterns(Collection<String> theIncludePatterns) {
410 
411 		if (includePatterns.equals(theIncludePatterns)) return;
412 		
413 		includePatterns.clear();
414 		CollectionUtil.addWithoutDuplicates(theIncludePatterns, includePatterns);
415 		patternsChanged();
416 	}
417 
418 	private void patternsChanged() {
419 		filter = null;	// ensure we start with one that reflects the current patterns
420 	}
421 	
422 	/**
423 	 * Does any Rule for the given Language use Type Resolution?
424 	 * @param language The Language.
425 	 * @return <code>true</code> if a Rule for the Language uses Type Resolution,
426 	 * <code>false</code> otherwise.
427 	 */
428 	public boolean usesTypeResolution(Language language) {
429 		for (Rule r : rules) {
430 			if (r.getLanguage().equals(language)) {
431 				if (r.usesTypeResolution()) {
432 					return true;
433 				}
434 			}
435 		}
436 		return false;
437 	}
438 
439 	/**
440 	 * Remove and collect any misconfigured rules.
441 	 * 
442 	 * @param collector
443 	 */
444 	public void removeDysfunctionalRules(Collection<Rule> collector) {
445 		
446 		Iterator<Rule> iter = rules.iterator();
447 		
448 		while (iter.hasNext()) {
449 			Rule rule = iter.next();
450 			if (rule.dysfunctionReason() != null) {
451 				iter.remove();
452 				collector.add(rule);
453 			}
454 		}
455 	}
456 	
457 }