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                 if (ctx.isIgnoreExceptions()) {
289                     LOG.log(Level.WARNING, "Exception applying rule " + rule.getName() + ", continuing with next rule", t);
290                 } else {
291                     throw new RuntimeException(t);
292                 }
293             }
294 		}
295 	}
296 
297 	/**
298 	 * Does the given Rule apply to the given LanguageVersion?  If so, the
299 	 * Language must be the same and be between the minimum and maximums
300 	 * versions on the Rule.
301 	 * 
302 	 * @param rule The rule.
303 	 * @param languageVersion The language version.
304 	 */
305 	public static boolean applies(Rule rule, LanguageVersion languageVersion) {
306 		final LanguageVersion min = rule.getMinimumLanguageVersion();
307 		final LanguageVersion max = rule.getMinimumLanguageVersion();
308 		return rule.getLanguage().equals(languageVersion.getLanguage())
309 		&& (min == null || min.compareTo(languageVersion) <= 0)
310 		&& (max == null || max.compareTo(languageVersion) >= 0);
311 	}
312 
313 	public void end(RuleContext ctx) {
314 		for (Rule rule : rules) {
315 			rule.end(ctx);
316 		}
317 	}
318 
319 	/**
320 	 * @see java.lang.Object#equals(java.lang.Object)
321 	 */
322 	@Override
323 	public boolean equals(Object o) {
324 		if (!(o instanceof RuleSet)) {
325 			return false; // Trivial
326 		}
327 
328 		if (this == o) {
329 			return true; // Basic equality
330 		}
331 
332 		RuleSet ruleSet = (RuleSet) o;
333 		return getName().equals(ruleSet.getName()) && getRules().equals(ruleSet.getRules());
334 	}
335 
336 	/**
337 	 * @see java.lang.Object#hashCode()
338 	 */
339 	@Override
340 	public int hashCode() {
341 		return getName().hashCode() + 13 * getRules().hashCode();
342 	}
343 
344 	public String getFileName() {
345 		return fileName;
346 	}
347 
348 	public void setFileName(String fileName) {
349 		this.fileName = fileName;
350 	}
351 
352 	public String getName() {
353 		return name;
354 	}
355 
356 	public void setName(String name) {
357 		this.name = name;
358 	}
359 
360 	public String getDescription() {
361 		return description;
362 	}
363 
364 	public void setDescription(String description) {
365 		this.description = description;
366 	}
367 
368 	public List<String> getExcludePatterns() {
369 		return excludePatterns;
370 	}
371 
372 	public void addExcludePattern(String aPattern) {
373 
374 		if (excludePatterns.contains(aPattern)) return;
375 		
376 		excludePatterns.add(aPattern);
377 		patternsChanged();
378 	}
379 	
380 	public void addExcludePatterns(Collection<String> someExcludePatterns) {
381 		
382 		int added = CollectionUtil.addWithoutDuplicates(someExcludePatterns, excludePatterns);
383 		if (added > 0) patternsChanged();
384 	}
385 
386 	public void setExcludePatterns(Collection<String> theExcludePatterns) {
387 		
388 		if (excludePatterns.equals(theExcludePatterns)) return;
389 		
390 		excludePatterns.clear();
391 		CollectionUtil.addWithoutDuplicates(theExcludePatterns, excludePatterns);
392 		patternsChanged();
393 	}
394 
395 	public List<String> getIncludePatterns() {
396 		return includePatterns;
397 	}
398 
399 	public void addIncludePattern(String aPattern) {
400 		
401 		if (includePatterns.contains(aPattern)) return;
402 		
403 		includePatterns.add(aPattern);
404 		patternsChanged();
405 	}
406 
407 	public void addIncludePatterns(Collection<String> someIncludePatterns) {
408 
409 		int added = CollectionUtil.addWithoutDuplicates(someIncludePatterns, includePatterns);
410 		if (added > 0) patternsChanged();
411 	}
412 
413 	public void setIncludePatterns(Collection<String> theIncludePatterns) {
414 
415 		if (includePatterns.equals(theIncludePatterns)) return;
416 		
417 		includePatterns.clear();
418 		CollectionUtil.addWithoutDuplicates(theIncludePatterns, includePatterns);
419 		patternsChanged();
420 	}
421 
422 	private void patternsChanged() {
423 		filter = null;	// ensure we start with one that reflects the current patterns
424 	}
425 	
426 	/**
427 	 * Does any Rule for the given Language use Type Resolution?
428 	 * @param language The Language.
429 	 * @return <code>true</code> if a Rule for the Language uses Type Resolution,
430 	 * <code>false</code> otherwise.
431 	 */
432 	public boolean usesTypeResolution(Language language) {
433 		for (Rule r : rules) {
434 			if (r.getLanguage().equals(language)) {
435 				if (r.usesTypeResolution()) {
436 					return true;
437 				}
438 			}
439 		}
440 		return false;
441 	}
442 
443 	/**
444 	 * Remove and collect any misconfigured rules.
445 	 * 
446 	 * @param collector
447 	 */
448 	public void removeDysfunctionalRules(Collection<Rule> collector) {
449 		
450 		Iterator<Rule> iter = rules.iterator();
451 		
452 		while (iter.hasNext()) {
453 			Rule rule = iter.next();
454 			if (rule.dysfunctionReason() != null) {
455 				iter.remove();
456 				collector.add(rule);
457 			}
458 		}
459 	}
460 	
461 }