/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sourceforge.pmd.benchmark.Benchmark;
import net.sourceforge.pmd.benchmark.Benchmarker;
import net.sourceforge.pmd.cache.ChecksumAware;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.rule.RuleReference;
import net.sourceforge.pmd.util.CollectionUtil;
import net.sourceforge.pmd.util.StringUtil;
import net.sourceforge.pmd.util.filter.Filter;
import net.sourceforge.pmd.util.filter.Filters;
/**
* This class represents a collection of rules along with some optional filter
* patterns that can preclude their application on specific files.
*
* @see Rule
*/
// FUTURE Implement Cloneable and clone()
public class RuleSet implements ChecksumAware {
private static final Logger LOG = Logger.getLogger(RuleSet.class.getName());
private static final String MISSING_RULE = "Missing rule";
private final long checksum;
private final List<Rule> rules;
private final String fileName;
private final String name;
private final String description;
// TODO should these not be Sets or is their order important?
private final List<String> excludePatterns;
private final List<String> includePatterns;
private final Filter<File> filter;
/**
* Creates a new RuleSet with the given checksum.
*
* @param checksum
* A checksum of the ruleset, should change only if the ruleset
* was configured differently
* @param rules
* The rules to be applied as part of this ruleset
*/
private RuleSet(final RuleSetBuilder builder) {
checksum = builder.checksum;
fileName = builder.fileName;
name = builder.name;
description = builder.description;
// TODO: ideally, the rules would be unmodifiable, too. But removeDysfunctionalRules might change the rules.
rules = builder.rules;
excludePatterns = Collections.unmodifiableList(builder.excludePatterns);
includePatterns = Collections.unmodifiableList(builder.includePatterns);
final Filter<String> regexFilter = Filters.buildRegexFilterIncludeOverExclude(includePatterns, excludePatterns);
filter = Filters.toNormalizedFileFilter(regexFilter);
}
/* package */ static class RuleSetBuilder {
public String description = "";
public String name = "";
public String fileName;
private final List<Rule> rules = new ArrayList<>();
private final List<String> excludePatterns = new ArrayList<>(0);
private final List<String> includePatterns = new ArrayList<>(0);
private final long checksum;
/* package */ RuleSetBuilder(final long checksum) {
this.checksum = checksum;
}
/** Copy constructor. Takes the same checksum as the original ruleset. */
/* package */ RuleSetBuilder(final RuleSet original) {
checksum = original.getChecksum();
this.withName(original.getName())
.withDescription(original.getDescription())
.withFileName(original.getFileName())
.setExcludePatterns(original.getExcludePatterns())
.setIncludePatterns(original.getIncludePatterns());
addRuleSet(original);
}
/**
* Add a new rule to this ruleset. Note that this method does not check
* for duplicates.
*
* @param rule
* the rule to be added
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addRule(final Rule rule) {
if (rule == null) {
throw new IllegalArgumentException(MISSING_RULE);
}
rules.add(rule);
return this;
}
/**
* Adds a rule. If a rule with the same name and language already
* existed before in the ruleset, then the new rule will replace it.
* This makes sure that the rule configured is overridden.
*
* @param rule
* the new rule to add
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addRuleReplaceIfExists(final Rule rule) {
if (rule == null) {
throw new IllegalArgumentException(MISSING_RULE);
}
for (final Iterator<Rule> it = rules.iterator(); it.hasNext();) {
final Rule r = it.next();
if (r.getName().equals(rule.getName()) && r.getLanguage() == rule.getLanguage()) {
it.remove();
}
}
addRule(rule);
return this;
}
/**
* Only adds a rule to the ruleset if no rule with the same name for the
* same language was added before, so that the existent rule
* configuration won't be overridden.
*
* @param rule
* the new rule to add
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addRuleIfNotExists(final Rule rule) {
if (rule == null) {
throw new IllegalArgumentException(MISSING_RULE);
}
boolean exists = false;
for (final Rule r : rules) {
if (r.getName().equals(rule.getName()) && r.getLanguage() == rule.getLanguage()) {
exists = true;
break;
}
}
if (!exists) {
addRule(rule);
}
return this;
}
/**
* Add a new rule by reference to this ruleset.
*
* @param ruleSetFileName
* the ruleset which contains the rule
* @param rule
* the rule to be added
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addRuleByReference(final String ruleSetFileName, final Rule rule) {
if (StringUtil.isEmpty(ruleSetFileName)) {
throw new RuntimeException(
"Adding a rule by reference is not allowed with an empty rule set file name.");
}
if (rule == null) {
throw new IllegalArgumentException("Cannot add a null rule reference to a RuleSet");
}
final RuleReference ruleReference;
if (rule instanceof RuleReference) {
ruleReference = (RuleReference) rule;
} else {
final RuleSetReference ruleSetReference = new RuleSetReference();
ruleSetReference.setRuleSetFileName(ruleSetFileName);
ruleReference = new RuleReference();
ruleReference.setRule(rule);
ruleReference.setRuleSetReference(ruleSetReference);
}
rules.add(ruleReference);
return this;
}
/**
* Add all rules of a whole RuleSet to this RuleSet
*
* @param ruleSet
* the RuleSet to add
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addRuleSet(final RuleSet ruleSet) {
rules.addAll(rules.size(), ruleSet.getRules());
return this;
}
/**
* Add all rules by reference from one RuleSet to this RuleSet. The
* rules can be added as individual references, or collectively as an
* all rule reference.
*
* @param ruleSet
* the RuleSet to add
* @param allRules
* <code>true</code> if the ruleset should be added
* collectively or <code>false</code> to add individual
* references for each rule.
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addRuleSetByReference(final RuleSet ruleSet, final boolean allRules) {
return addRuleSetByReference(ruleSet, allRules, (String[]) null);
}
/**
* Add all rules by reference from one RuleSet to this RuleSet. The
* rules can be added as individual references, or collectively as an
* all rule reference.
*
* @param ruleSet
* the RuleSet to add
* @param allRules
* <code>true</code> if the ruleset should be added
* collectively or <code>false</code> to add individual
* references for each rule.
* @param excludes
* names of the rules that should be excluded.
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addRuleSetByReference(final RuleSet ruleSet, final boolean allRules,
final String... excludes) {
if (StringUtil.isEmpty(ruleSet.getFileName())) {
throw new RuntimeException(
"Adding a rule by reference is not allowed with an empty rule set file name.");
}
final RuleSetReference ruleSetReference = new RuleSetReference(ruleSet.getFileName());
ruleSetReference.setAllRules(allRules);
if (excludes != null) {
ruleSetReference.setExcludes(new HashSet<>(Arrays.asList(excludes)));
}
for (final Rule rule : ruleSet.getRules()) {
final RuleReference ruleReference = new RuleReference(rule, ruleSetReference);
rules.add(ruleReference);
}
return this;
}
/**
* Adds a new file exclusion pattern.
*
* @param aPattern
* the pattern
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addExcludePattern(final String aPattern) {
if (!excludePatterns.contains(aPattern)) {
excludePatterns.add(aPattern);
}
return this;
}
/**
* Adds new file exclusion patterns.
*
* @param someExcludePatterns
* the patterns
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addExcludePatterns(final Collection<String> someExcludePatterns) {
CollectionUtil.addWithoutDuplicates(someExcludePatterns, excludePatterns);
return this;
}
/**
* Replaces the existing exclusion patterns with the given patterns.
*
* @param theExcludePatterns
* the new patterns
*/
public RuleSetBuilder setExcludePatterns(final Collection<String> theExcludePatterns) {
if (!excludePatterns.equals(theExcludePatterns)) {
excludePatterns.clear();
CollectionUtil.addWithoutDuplicates(theExcludePatterns, excludePatterns);
}
return this;
}
/**
* Adds new inclusion patterns.
*
* @param someIncludePatterns
* the patterns
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addIncludePatterns(final Collection<String> someIncludePatterns) {
CollectionUtil.addWithoutDuplicates(someIncludePatterns, includePatterns);
return this;
}
/**
* Replaces the existing inclusion patterns with the given patterns.
*
* @param theIncludePatterns
* the new patterns
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder setIncludePatterns(final Collection<String> theIncludePatterns) {
if (!includePatterns.equals(theIncludePatterns)) {
includePatterns.clear();
CollectionUtil.addWithoutDuplicates(theIncludePatterns, includePatterns);
}
return this;
}
/**
* Adds a new inclusion pattern.
*
* @param aPattern
* the pattern
* @return The same builder, for a fluid programming interface
*/
public RuleSetBuilder addIncludePattern(final String aPattern) {
if (!includePatterns.contains(aPattern)) {
includePatterns.add(aPattern);
}
return this;
}
public RuleSetBuilder withFileName(final String fileName) {
this.fileName = fileName;
return this;
}
public RuleSetBuilder withName(final String name) {
this.name = name;
return this;
}
public RuleSetBuilder withDescription(final String description) {
this.description = description;
return this;
}
public String getName() {
return name;
}
public RuleSet build() {
return new RuleSet(this);
}
}
/**
* Returns the number of rules in this ruleset
*
* @return an int representing the number of rules
*/
public int size() {
return rules.size();
}
/**
* Returns the actual Collection of rules in this ruleset
*
* @return a Collection with the rules. All objects are of type {@link Rule}
*/
public Collection<Rule> getRules() {
return rules;
}
/**
* Does any Rule for the given Language use the DFA layer?
*
* @param language
* The Language.
* @return <code>true</code> if a Rule for the Language uses the DFA layer,
* <code>false</code> otherwise.
*/
public boolean usesDFA(Language language) {
for (Rule r : rules) {
if (r.getLanguage().equals(language) && r.usesDFA()) {
return true;
}
}
return false;
}
/**
* Returns the first Rule found with the given name (case-sensitive).
*
* Note: Since we support multiple languages, rule names are not expected to
* be unique within any specific ruleset.
*
* @param ruleName
* the exact name of the rule to find
* @return the rule or null if not found
*/
public Rule getRuleByName(String ruleName) {
for (Rule r : rules) {
if (r.getName().equals(ruleName)) {
return r;
}
}
return null;
}
/**
* Check if a given source file should be checked by rules in this RuleSet.
* A file should not be checked if there is an <code>exclude</code> pattern
* which matches the file, unless there is an <code>include</code> pattern
* which also matches the file. In other words, <code>include</code>
* patterns override <code>exclude</code> patterns.
*
* @param file
* the source file to check
* @return <code>true</code> if the file should be checked,
* <code>false</code> otherwise
*/
public boolean applies(File file) {
return file != null ? filter.filter(file) : true;
}
/**
* Triggers that start lifecycle event on each rule in this ruleset. Some
* rules perform initialization tasks on start.
*
* @param ctx
* the current context
*/
public void start(RuleContext ctx) {
for (Rule rule : rules) {
rule.start(ctx);
}
}
/**
* Executes the rules in this ruleset against each of the given nodes.
*
* @param acuList
* the node list, usually the root nodes like compilation units
* @param ctx
* the current context
*/
public void apply(List<? extends Node> acuList, RuleContext ctx) {
long start = System.nanoTime();
for (Rule rule : rules) {
try {
if (!rule.usesRuleChain() && applies(rule, ctx.getLanguageVersion())) {
rule.apply(acuList, ctx);
long end = System.nanoTime();
Benchmarker.mark(Benchmark.Rule, rule.getName(), end - start, 1);
start = end;
}
} catch (RuntimeException e) {
if (ctx.isIgnoreExceptions()) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Exception applying rule " + rule.getName() + " on file "
+ ctx.getSourceCodeFilename() + ", continuing with next rule", e);
}
} else {
throw e;
}
}
}
}
/**
* Does the given Rule apply to the given LanguageVersion? If so, the
* Language must be the same and be between the minimum and maximums
* versions on the Rule.
*
* @param rule
* The rule.
* @param languageVersion
* The language version.
*
* @return <code>true</code> if the given rule matches the given language,
* which means, that the rule would be executed.
*/
public static boolean applies(Rule rule, LanguageVersion languageVersion) {
final LanguageVersion min = rule.getMinimumLanguageVersion();
final LanguageVersion max = rule.getMaximumLanguageVersion();
return rule.getLanguage().equals(languageVersion.getLanguage())
&& (min == null || min.compareTo(languageVersion) <= 0)
&& (max == null || max.compareTo(languageVersion) >= 0);
}
/**
* Triggers the end lifecycle event on each rule in the ruleset. Some rules
* perform a final summary calculation or cleanup in the end.
*
* @param ctx
* the current context
*/
public void end(RuleContext ctx) {
for (Rule rule : rules) {
rule.end(ctx);
}
}
/**
* Two rulesets are equals, if they have the same name and contain the same
* rules.
*
* @param o
* the other ruleset to compare with
* @return <code>true</code> if o is a ruleset with the same name and rules,
* <code>false</code> otherwise
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof RuleSet)) {
return false; // Trivial
}
if (this == o) {
return true; // Basic equality
}
RuleSet ruleSet = (RuleSet) o;
return getName().equals(ruleSet.getName()) && getRules().equals(ruleSet.getRules());
}
@Override
public int hashCode() {
return getName().hashCode() + 13 * getRules().hashCode();
}
public String getFileName() {
return fileName;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public List<String> getExcludePatterns() {
return excludePatterns;
}
public List<String> getIncludePatterns() {
return includePatterns;
}
/**
* Does any Rule for the given Language use Type Resolution?
*
* @param language
* The Language.
* @return <code>true</code> if a Rule for the Language uses Type
* Resolution, <code>false</code> otherwise.
*/
public boolean usesTypeResolution(Language language) {
for (Rule r : rules) {
if (r.getLanguage().equals(language) && r.usesTypeResolution()) {
return true;
}
}
return false;
}
/**
* Remove and collect any misconfigured rules.
*
* @param collector
* the removed rules will be added to this collection
*/
public void removeDysfunctionalRules(Collection<Rule> collector) {
Iterator<Rule> iter = rules.iterator();
while (iter.hasNext()) {
Rule rule = iter.next();
if (rule.dysfunctionReason() != null) {
iter.remove();
collector.add(rule);
}
}
}
@Override
public long getChecksum() {
return checksum;
}
}