Skip to content

Commit

Permalink
Merge branch 'issue-1360-ruleset-compatibility' into pmd/5.4.x
Browse files Browse the repository at this point in the history
  • Loading branch information
adangel committed May 13, 2016
2 parents 07be0d6 + a8a18e6 commit fadd6a9
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 1 deletion.
27 changes: 26 additions & 1 deletion pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
* RuleSetFactory is responsible for creating RuleSet instances from XML
* content. By default Rules will be loaded using the ClassLoader for this
* class, using the {@link RulePriority#LOW} priority, with Rule deprecation
* warnings off.
* By default, the ruleset compatibility filter is active, too. See {@link RuleSetFactoryCompatibility}.
*/
public class RuleSetFactory {

Expand All @@ -49,6 +51,7 @@ public class RuleSetFactory {
private ClassLoader classLoader = RuleSetFactory.class.getClassLoader();
private RulePriority minimumPriority = RulePriority.LOW;
private boolean warnDeprecated = false;
private RuleSetFactoryCompatibility compatibilityFilter = new RuleSetFactoryCompatibility();

/**
* Set the ClassLoader to use when loading Rules.
Expand Down Expand Up @@ -79,6 +82,22 @@ public void setWarnDeprecated(boolean warnDeprecated) {
this.warnDeprecated = warnDeprecated;
}

/**
* Disable the ruleset compatibility filter. Disabling this filter will cause
* exception when loading a ruleset, which uses references to old/not existing rules.
*/
public void disableCompatibilityFilter() {
compatibilityFilter = null;
}

/**
* Gets the compatibility filter in order to adjust it, e.g. add additional filters.
* @return the {@link RuleSetFactoryCompatibility}
*/
public RuleSetFactoryCompatibility getCompatibilityFilter() {
return compatibilityFilter;
}

/**
* Returns an Iterator of RuleSet objects loaded from descriptions from the
* "rulesets.properties" resource for each Language with Rule support.
Expand Down Expand Up @@ -220,7 +239,13 @@ private RuleSet parseRuleSetNode(RuleSetReferenceId ruleSetReferenceId, InputStr
}
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = builder.parse(inputStream);
InputSource inputSource;
if (compatibilityFilter != null) {
inputSource = new InputSource(compatibilityFilter.filterRuleSetFile(inputStream));
} else {
inputSource = new InputSource(inputStream);
}
Document document = builder.parse(inputSource);
Element ruleSetElement = document.getDocumentElement();

RuleSet ruleSet = new RuleSet();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;

/**
* Provides a simple filter mechanism to avoid failing to parse an old ruleset, which references rules, that
* have either been removed from PMD already or renamed or moved to another ruleset.
*
* @see <a href="https://sourceforge.net/p/pmd/bugs/1360/">issue 1360</a>
*/
public class RuleSetFactoryCompatibility {
private static final Logger LOG = Logger.getLogger(RuleSetFactoryCompatibility.class.getName());

private List<RuleSetFilter> filters = new LinkedList<RuleSetFilter>();

/**
* Creates a new instance of the compatibility filter with the built-in filters for the
* modified PMD rules.
*/
public RuleSetFactoryCompatibility() {
// PMD 5.3.0
addFilterRuleRenamed("java", "design", "UncommentedEmptyMethod", "UncommentedEmptyMethodBody");
addFilterRuleRemoved("java", "controversial", "BooleanInversion");

// PMD 5.3.1
addFilterRuleRenamed("java", "design", "UseSingleton", "UseUtilityClass");

// PMD 5.4.0
addFilterRuleMoved("java", "basic", "empty", "EmptyCatchBlock");
addFilterRuleMoved("java", "basic", "empty", "EmptyIfStatement");
addFilterRuleMoved("java", "basic", "empty", "EmptyWhileStmt");
addFilterRuleMoved("java", "basic", "empty", "EmptyTryBlock");
addFilterRuleMoved("java", "basic", "empty", "EmptyFinallyBlock");
addFilterRuleMoved("java", "basic", "empty", "EmptySwitchStatements");
addFilterRuleMoved("java", "basic", "empty", "EmptySynchronizedBlock");
addFilterRuleMoved("java", "basic", "empty", "EmptyStatementNotInLoop");
addFilterRuleMoved("java", "basic", "empty", "EmptyInitializer");
addFilterRuleMoved("java", "basic", "empty", "EmptyStatementBlock");
addFilterRuleMoved("java", "basic", "empty", "EmptyStaticInitializer");
addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryConversionTemporary");
addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryReturn");
addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryFinalModifier");
addFilterRuleMoved("java", "basic", "unnecessary", "UselessOverridingMethod");
addFilterRuleMoved("java", "basic", "unnecessary", "UselessOperationOnImmutable");
addFilterRuleMoved("java", "basic", "unnecessary", "UnusedNullCheckInEquals");
addFilterRuleMoved("java", "basic", "unnecessary", "UselessParentheses");
}

void addFilterRuleRenamed(String language, String ruleset, String oldName, String newName) {
filters.add(RuleSetFilter.ruleRenamed(language, ruleset, oldName, newName));
}
void addFilterRuleMoved(String language, String oldRuleset, String newRuleset, String ruleName) {
filters.add(RuleSetFilter.ruleMoved(language, oldRuleset, newRuleset, ruleName));
}
void addFilterRuleRemoved(String language, String ruleset, String name) {
filters.add(RuleSetFilter.ruleRemoved(language, ruleset, name));
}

/**
* Applies all configured filters against the given input stream.
* The resulting reader will contain the original ruleset modified by
* the filters.
*
* @param stream
* @return
* @throws IOException
*/
public Reader filterRuleSetFile(InputStream stream) throws IOException {
byte[] bytes = IOUtils.toByteArray(stream);
String encoding = determineEncoding(bytes);
String ruleset = new String(bytes, encoding);

ruleset = applyAllFilters(ruleset);

return new StringReader(ruleset);
}

private String applyAllFilters(String in) {
String result = in;
for (RuleSetFilter filter : filters) {
result = filter.apply(result);
}
return result;
}

private static final Pattern ENCODING_PATTERN = Pattern.compile("encoding=\"([^\"]+)\"");
/**
* Determines the encoding of the given bytes, assuming this is a XML document, which specifies
* the encoding in the first 1024 bytes.
*
* @param bytes the input bytes, might be more or less than 1024 bytes
* @return the determined encoding, falls back to the default UTF-8 encoding
*/
String determineEncoding(byte[] bytes) {
String firstBytes = new String(bytes, 0, bytes.length > 1024 ? 1024 : bytes.length, Charset.forName("ISO-8859-1"));
Matcher matcher = ENCODING_PATTERN.matcher(firstBytes);
String encoding = Charset.forName("UTF-8").name();
if (matcher.find()) {
encoding = matcher.group(1);
}
return encoding;
}

private static class RuleSetFilter {
private final Pattern refPattern;
private final String replacement;
private Pattern exclusionPattern;
private String exclusionReplacement;
private final String logMessage;
private RuleSetFilter(String refPattern, String replacement, String logMessage) {
this.logMessage = logMessage;
if (replacement != null) {
this.refPattern = Pattern.compile("ref=\"" + Pattern.quote(refPattern) + "\"");
this.replacement = "ref=\"" + replacement + "\"";
} else {
this.refPattern = Pattern.compile("<rule\\s+ref=\"" + Pattern.quote(refPattern) + "\"\\s*/>");
this.replacement = "";
}
}

private void setExclusionPattern(String oldName, String newName) {
exclusionPattern = Pattern.compile("<exclude\\s+name=[\"']" + Pattern.quote(oldName) + "[\"']\\s*/>");
if (newName != null) {
exclusionReplacement = "<exclude name=\"" + newName + "\" />";
} else {
exclusionReplacement = "";
}
}

public static RuleSetFilter ruleRenamed(String language, String ruleset, String oldName, String newName) {
String base = "rulesets/" + language + "/" + ruleset + ".xml/";
RuleSetFilter filter = new RuleSetFilter(base + oldName, base + newName,
"The rule \"" + oldName + "\" has been renamed to \"" + newName + "\". Please change your ruleset!");
filter.setExclusionPattern(oldName, newName);
return filter;
}
public static RuleSetFilter ruleMoved(String language, String oldRuleset, String newRuleset, String ruleName) {
String base = "rulesets/" + language + "/";
return new RuleSetFilter(base + oldRuleset + ".xml/" + ruleName, base + newRuleset + ".xml/" + ruleName,
"The rule \"" + ruleName + "\" has been moved from ruleset \"" + oldRuleset + "\" to \"" + newRuleset + "\". Please change your ruleset!");
}
public static RuleSetFilter ruleRemoved(String language, String ruleset, String name) {
RuleSetFilter filter = new RuleSetFilter("rulesets/" + language + "/" + ruleset + ".xml/" + name, null,
"The rule \"" + name + "\" in ruleset \"" + ruleset + "\" has been removed from PMD and no longer exists. Please change your ruleset!");
filter.setExclusionPattern(name, null);
return filter;
}

String apply(String in) {
String result = in;
Matcher matcher = refPattern.matcher(in);

if (matcher.find()) {
result = matcher.replaceAll(replacement);

if (LOG.isLoggable(Level.WARNING)) {
LOG.warning("Applying rule set filter: " + logMessage);
}
}

if (exclusionPattern == null) return result;

Matcher exclusions = exclusionPattern.matcher(result);
if (exclusions.find()) {
result = exclusions.replaceAll(exclusionReplacement);

if (LOG.isLoggable(Level.WARNING)) {
LOG.warning("Applying rule set filter for exclusions: " + logMessage);
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.Charset;

import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;

public class RuleSetFactoryCompatibilityTest {
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
private static final Charset UTF_8 = Charset.forName("UTF-8");

@Test
public void testCorrectOldReference() throws Exception {
final String ruleset = "<?xml version=\"1.0\"?>\n" +
"\n" +
"<ruleset name=\"Test\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd\">\n" +
" <description>Test</description>\n" +
"\n" +
" <rule ref=\"rulesets/dummy/notexisting.xml/DummyBasicMockRule\" />\n" +
"</ruleset>\n";

RuleSetFactory factory = new RuleSetFactory();
factory.getCompatibilityFilter().addFilterRuleMoved("dummy", "notexisting", "basic", "DummyBasicMockRule");

RuleSet createdRuleSet = createRulesetFromString(ruleset, factory);
Assert.assertNotNull(createdRuleSet.getRuleByName("DummyBasicMockRule"));
}

@Test
public void testExclusion() throws Exception {
final String ruleset = "<?xml version=\"1.0\"?>\n" +
"\n" +
"<ruleset name=\"Test\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd\">\n" +
" <description>Test</description>\n" +
"\n" +
" <rule ref=\"rulesets/dummy/basic.xml\">\n" +
" <exclude name=\"OldNameOfSampleXPathRule\"/>\n" +
" </rule>\n" +
"</ruleset>\n";

RuleSetFactory factory = new RuleSetFactory();
factory.getCompatibilityFilter().addFilterRuleRenamed("dummy", "basic", "OldNameOfSampleXPathRule", "SampleXPathRule");

RuleSet createdRuleSet = createRulesetFromString(ruleset, factory);
Assert.assertNotNull(createdRuleSet.getRuleByName("DummyBasicMockRule"));
Assert.assertNull(createdRuleSet.getRuleByName("SampleXPathRule"));
}

@Test
public void testFilter() throws Exception {
RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility();
rsfc.addFilterRuleMoved("dummy", "notexisting", "basic", "DummyBasicMockRule");
rsfc.addFilterRuleRemoved("dummy", "basic", "DeletedRule");
rsfc.addFilterRuleRenamed("dummy", "basic", "OldNameOfBasicMockRule", "NewNameOfBasicMockRule");

String in = "<?xml version=\"1.0\"?>\n" +
"\n" +
"<ruleset name=\"Test\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd\">\n" +
" <description>Test</description>\n" +
"\n" +
" <rule ref=\"rulesets/dummy/notexisting.xml/DummyBasicMockRule\" />\n" +
" <rule ref=\"rulesets/dummy/basic.xml/DeletedRule\" />\n" +
" <rule ref=\"rulesets/dummy/basic.xml/OldNameOfBasicMockRule\" />\n" +
"</ruleset>\n";
InputStream stream = new ByteArrayInputStream(in.getBytes(ISO_8859_1));
Reader filtered = rsfc.filterRuleSetFile(stream);
String out = IOUtils.toString(filtered);

Assert.assertFalse(out.contains("notexisting.xml"));
Assert.assertTrue(out.contains("<rule ref=\"rulesets/dummy/basic.xml/DummyBasicMockRule\" />"));

Assert.assertFalse(out.contains("DeletedRule"));

Assert.assertFalse(out.contains("OldNameOfBasicMockRule"));
Assert.assertTrue(out.contains("<rule ref=\"rulesets/dummy/basic.xml/NewNameOfBasicMockRule\" />"));
}

@Test
public void testExclusionFilter() throws Exception {
RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility();
rsfc.addFilterRuleRenamed("dummy", "basic", "AnotherOldNameOfBasicMockRule", "NewNameOfBasicMockRule");

String in = "<?xml version=\"1.0\"?>\n" +
"\n" +
"<ruleset name=\"Test\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd\">\n" +
" <description>Test</description>\n" +
"\n" +
" <rule ref=\"rulesets/dummy/basic.xml\">\n" +
" <exclude name=\"AnotherOldNameOfBasicMockRule\"/>\n" +
" </rule>\n" +
"</ruleset>\n";
InputStream stream = new ByteArrayInputStream(in.getBytes(ISO_8859_1));
Reader filtered = rsfc.filterRuleSetFile(stream);
String out = IOUtils.toString(filtered);

Assert.assertFalse(out.contains("OldNameOfBasicMockRule"));
Assert.assertTrue(out.contains("<exclude name=\"NewNameOfBasicMockRule\" />"));
}

@Test
public void testEncoding() {
RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility();
String testString;

testString = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><x></x>";
Assert.assertEquals("ISO-8859-1", rsfc.determineEncoding(testString.getBytes(ISO_8859_1)));

testString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><x></x>";
Assert.assertEquals("UTF-8", rsfc.determineEncoding(testString.getBytes(ISO_8859_1)));
}

private RuleSet createRulesetFromString(final String ruleset, RuleSetFactory factory)
throws RuleSetNotFoundException {
return factory.createRuleSet(new RuleSetReferenceId(null) {
@Override
public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
return new ByteArrayInputStream(ruleset.getBytes(UTF_8));
}
});
}
}
Loading

0 comments on commit fadd6a9

Please sign in to comment.