/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.junit.BeforeClass; import org.junit.Test; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.rule.RuleReference; import net.sourceforge.pmd.lang.rule.XPathRule; import net.sourceforge.pmd.util.ResourceLoader; /** * Base test class to verify the language's rulesets. This class should be * subclassed for each language. */ public abstract class AbstractRuleSetFactoryTest { private static SAXParserFactory saxParserFactory; private static ValidateDefaultHandler validateDefaultHandlerXsd; private static ValidateDefaultHandler validateDefaultHandlerDtd; private static SAXParser saxParser; /** * Setups the XML parser with validation. * * @throws Exception * any error */ @BeforeClass public static void init() throws Exception { saxParserFactory = SAXParserFactory.newInstance(); saxParserFactory.setValidating(true); saxParserFactory.setNamespaceAware(true); // Hope we're using Xerces, or this may not work! // Note: Features are listed here // http://xerces.apache.org/xerces2-j/features.html saxParserFactory.setFeature("http://xml.org/sax/features/validation", true); saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema", true); saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true); validateDefaultHandlerXsd = new ValidateDefaultHandler("ruleset_2_0_0.xsd"); validateDefaultHandlerDtd = new ValidateDefaultHandler("ruleset_2_0_0.dtd"); saxParser = saxParserFactory.newSAXParser(); } /** * Checks all rulesets of all languages on the classpath and verifies that * all required attributes for all rules are specified. * * @throws Exception * any error */ @Test public void testAllPMDBuiltInRulesMeetConventions() throws Exception { int invalidSinceAttributes = 0; int invalidExternalInfoURL = 0; int invalidClassName = 0; int invalidRegexSuppress = 0; int invalidXPathSuppress = 0; String messages = ""; List<String> ruleSetFileNames = getRuleSetFileNames(); for (String fileName : ruleSetFileNames) { RuleSet ruleSet = loadRuleSetByFileName(fileName); for (Rule rule : ruleSet.getRules()) { // Skip references if (rule instanceof RuleReference) { continue; } Language language = rule.getLanguage(); String group = fileName.substring(fileName.lastIndexOf('/') + 1); group = group.substring(0, group.indexOf(".xml")); if (group.indexOf('-') >= 0) { group = group.substring(0, group.indexOf('-')); } // Is since missing ? if (rule.getSince() == null) { invalidSinceAttributes++; messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'since' attribute" + PMD.EOL; } // Is URL valid ? if (rule.getExternalInfoUrl() == null || "".equalsIgnoreCase(rule.getExternalInfoUrl())) { invalidExternalInfoURL++; messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'externalInfoURL' attribute" + PMD.EOL; } else { String expectedExternalInfoURL = "https?://pmd.(sourceforge.net|github.io)/.+/rules/" + fileName.replaceAll("rulesets/", "").replaceAll(".xml", "") + ".html#" + rule.getName(); if (rule.getExternalInfoUrl() == null || !rule.getExternalInfoUrl().matches(expectedExternalInfoURL)) { invalidExternalInfoURL++; messages += "Rule " + fileName + "/" + rule.getName() + " seems to have an invalid 'externalInfoURL' value (" + rule.getExternalInfoUrl() + "), it should be:" + expectedExternalInfoURL + PMD.EOL; } } // Proper class name/packaging? String expectedClassName = "net.sourceforge.pmd.lang." + language.getTerseName() + ".rule." + group + "." + rule.getName() + "Rule"; if (!rule.getRuleClass().equals(expectedClassName) && !rule.getRuleClass().equals(XPathRule.class.getName())) { invalidClassName++; messages += "Rule " + fileName + "/" + rule.getName() + " seems to have an invalid 'class' value (" + rule.getRuleClass() + "), it should be:" + expectedClassName + PMD.EOL; } // Should not have violation suppress regex property if (rule.getProperty(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR) != null) { invalidRegexSuppress++; messages += "Rule " + fileName + "/" + rule.getName() + " should not have '" + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name() + "', this is intended for end user customization only." + PMD.EOL; } // Should not have violation suppress xpath property if (rule.getProperty(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR) != null) { invalidXPathSuppress++; messages += "Rule " + fileName + "/" + rule.getName() + " should not have '" + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name() + "', this is intended for end user customization only." + PMD.EOL; } } } // We do this at the end to ensure we test ALL the rules before failing // the test if (invalidSinceAttributes > 0 || invalidExternalInfoURL > 0 || invalidClassName > 0 || invalidRegexSuppress > 0 || invalidXPathSuppress > 0) { fail("All built-in PMD rules need 'since' attribute (" + invalidSinceAttributes + " are missing), a proper ExternalURLInfo (" + invalidExternalInfoURL + " are invalid), a class name meeting conventions (" + invalidClassName + " are invalid), no '" + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name() + "' property (" + invalidRegexSuppress + " are invalid), and no '" + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name() + "' property (" + invalidXPathSuppress + " are invalid)" + PMD.EOL + messages); } } /** * Verifies that all rulesets are valid XML according to the xsd schema. * * @throws Exception * any error */ @Test public void testXmlSchema() throws Exception { boolean allValid = true; List<String> ruleSetFileNames = getRuleSetFileNames(); for (String fileName : ruleSetFileNames) { boolean valid = validateAgainstSchema(fileName); allValid = allValid && valid; } assertTrue("All XML must parse without producing validation messages.", allValid); } /** * Verifies that all rulesets are valid XML according to the DTD. * * @throws Exception * any error */ @Test public void testDtd() throws Exception { boolean allValid = true; List<String> ruleSetFileNames = getRuleSetFileNames(); for (String fileName : ruleSetFileNames) { boolean valid = validateAgainstDtd(fileName); allValid = allValid && valid; } assertTrue("All XML must parse without producing validation messages.", allValid); } /** * Reads and writes the rulesets to make sure, that no data is lost if the * rulests are processed. * * @throws Exception * any error */ @Test public void testReadWriteRoundTrip() throws Exception { List<String> ruleSetFileNames = getRuleSetFileNames(); for (String fileName : ruleSetFileNames) { testRuleSet(fileName); } } // Gets all test PMD Ruleset XML files private List<String> getRuleSetFileNames() throws IOException, RuleSetNotFoundException { List<String> result = new ArrayList<>(); for (Language language : LanguageRegistry.getLanguages()) { result.addAll(getRuleSetFileNames(language.getTerseName())); } return result; } private List<String> getRuleSetFileNames(String language) throws IOException, RuleSetNotFoundException { List<String> ruleSetFileNames = new ArrayList<>(); try { Properties properties = new Properties(); try (InputStream is = ResourceLoader.loadResourceAsStream("rulesets/" + language + "/rulesets.properties")) { properties.load(is); } String fileNames = properties.getProperty("rulesets.filenames"); StringTokenizer st = new StringTokenizer(fileNames, ","); while (st.hasMoreTokens()) { ruleSetFileNames.add(st.nextToken()); } } catch (RuleSetNotFoundException e) { // this might happen if a language is only support by CPD, but not // by PMD System.err.println("No ruleset found for language " + language); } return ruleSetFileNames; } private RuleSet loadRuleSetByFileName(String ruleSetFileName) throws RuleSetNotFoundException { RuleSetFactory rsf = new RuleSetFactory(); return rsf.createRuleSet(ruleSetFileName); } private boolean validateAgainstSchema(String fileName) throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { InputStream inputStream = loadResourceAsStream(fileName); boolean valid = validateAgainstSchema(inputStream); if (!valid) { System.err.println("Validation against XML Schema failed for: " + fileName); } return valid; } private boolean validateAgainstSchema(InputStream inputStream) throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { saxParser.parse(inputStream, validateDefaultHandlerXsd.resetValid()); inputStream.close(); return validateDefaultHandlerXsd.isValid(); } private boolean validateAgainstDtd(String fileName) throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { InputStream inputStream = loadResourceAsStream(fileName); boolean valid = validateAgainstDtd(inputStream); if (!valid) { System.err.println("Validation against DTD failed for: " + fileName); } return valid; } private boolean validateAgainstDtd(InputStream inputStream) throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { // Read file into memory String file = readFullyToString(inputStream); inputStream.close(); // Remove XML Schema stuff, replace with DTD file = file.replaceAll("<\\?xml [ a-zA-Z0-9=\".-]*\\?>", ""); file = file.replaceAll("xmlns=\"" + RuleSetWriter.RULESET_NS_URI + "\"", ""); file = file.replaceAll("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", ""); file = file.replaceAll("xsi:schemaLocation=\"" + RuleSetWriter.RULESET_NS_URI + " http://pmd.sourceforge.net/ruleset_2_0_0.xsd\"", ""); file = "<?xml version=\"1.0\"?>" + PMD.EOL + "<!DOCTYPE ruleset SYSTEM \"file://" + "/path/does/not/matter/will/be/replaced/ruleset_2_0_0.dtd\">" + PMD.EOL + file; InputStream modifiedStream = new ByteArrayInputStream(file.getBytes()); saxParser.parse(modifiedStream, validateDefaultHandlerDtd.resetValid()); modifiedStream.close(); return validateDefaultHandlerDtd.isValid(); } private String readFullyToString(InputStream inputStream) throws IOException { StringBuilder buf = new StringBuilder(64 * 1024); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { buf.append(line); buf.append(PMD.EOL); } reader.close(); return buf.toString(); } private static InputStream loadResourceAsStream(String resource) throws RuleSetNotFoundException { InputStream inputStream = ResourceLoader.loadResourceAsStream(resource, AbstractRuleSetFactoryTest.class.getClassLoader()); if (inputStream == null) { throw new RuleSetNotFoundException("Can't find resource " + resource + " Make sure the resource is a valid file or URL or is on the CLASSPATH. Here's the current classpath: " + System.getProperty("java.class.path")); } return inputStream; } private void testRuleSet(String fileName) throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { // Load original XML // String xml1 = // readFullyToString(ResourceLoader.loadResourceAsStream(fileName)); // System.out.println("xml1: " + xml1); // Load the original RuleSet RuleSet ruleSet1 = loadRuleSetByFileName(fileName); // Write to XML, first time ByteArrayOutputStream outputStream1 = new ByteArrayOutputStream(); RuleSetWriter writer1 = new RuleSetWriter(outputStream1); writer1.write(ruleSet1); writer1.close(); String xml2 = new String(outputStream1.toByteArray()); // System.out.println("xml2: " + xml2); // Read RuleSet from XML, first time RuleSetFactory ruleSetFactory = new RuleSetFactory(); RuleSet ruleSet2 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml2)); // Do write/read a 2nd time, just to be sure // Write to XML, second time ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream(); RuleSetWriter writer2 = new RuleSetWriter(outputStream2); writer2.write(ruleSet2); writer2.close(); String xml3 = new String(outputStream2.toByteArray()); // System.out.println("xml3: " + xml3); // Read RuleSet from XML, second time RuleSet ruleSet3 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml3)); // The 2 written XMLs should all be valid w.r.t Schema/DTD assertTrue("1st roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")", validateAgainstSchema(new ByteArrayInputStream(xml2.getBytes()))); assertTrue("2nd roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")", validateAgainstSchema(new ByteArrayInputStream(xml3.getBytes()))); assertTrue("1st roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")", validateAgainstDtd(new ByteArrayInputStream(xml2.getBytes()))); assertTrue("2nd roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")", validateAgainstDtd(new ByteArrayInputStream(xml3.getBytes()))); // All 3 versions of the RuleSet should be the same assertEqualsRuleSet("Original RuleSet and 1st roundtrip Ruleset not the same (filename: " + fileName + ")", ruleSet1, ruleSet2); assertEqualsRuleSet("1st roundtrip Ruleset and 2nd roundtrip RuleSet not the same (filename: " + fileName + ")", ruleSet2, ruleSet3); // It's hard to compare the XML DOMs. At least the roundtrip ones should // textually be the same. assertEquals("1st roundtrip RuleSet XML and 2nd roundtrip RuleSet XML (filename: " + fileName + ")", xml2, xml3); } private void assertEqualsRuleSet(String message, RuleSet ruleSet1, RuleSet ruleSet2) { assertEquals(message + ", RuleSet name", ruleSet1.getName(), ruleSet2.getName()); assertEquals(message + ", RuleSet description", ruleSet1.getDescription(), ruleSet2.getDescription()); assertEquals(message + ", RuleSet exclude patterns", ruleSet1.getExcludePatterns(), ruleSet2.getExcludePatterns()); assertEquals(message + ", RuleSet include patterns", ruleSet1.getIncludePatterns(), ruleSet2.getIncludePatterns()); assertEquals(message + ", RuleSet rule count", ruleSet1.getRules().size(), ruleSet2.getRules().size()); for (int i = 0; i < ruleSet1.getRules().size(); i++) { Rule rule1 = ((List<Rule>) ruleSet1.getRules()).get(i); Rule rule2 = ((List<Rule>) ruleSet2.getRules()).get(i); assertFalse(message + ", Different RuleReference", rule1 instanceof RuleReference && !(rule2 instanceof RuleReference) || !(rule1 instanceof RuleReference) && rule2 instanceof RuleReference); if (rule1 instanceof RuleReference) { RuleReference ruleReference1 = (RuleReference) rule1; RuleReference ruleReference2 = (RuleReference) rule2; assertEquals(message + ", RuleReference overridden language", ruleReference1.getOverriddenLanguage(), ruleReference2.getOverriddenLanguage()); assertEquals(message + ", RuleReference overridden minimum language version", ruleReference1.getOverriddenMinimumLanguageVersion(), ruleReference2.getOverriddenMinimumLanguageVersion()); assertEquals(message + ", RuleReference overridden maximum language version", ruleReference1.getOverriddenMaximumLanguageVersion(), ruleReference2.getOverriddenMaximumLanguageVersion()); assertEquals(message + ", RuleReference overridden deprecated", ruleReference1.isOverriddenDeprecated(), ruleReference2.isOverriddenDeprecated()); assertEquals(message + ", RuleReference overridden name", ruleReference1.getOverriddenName(), ruleReference2.getOverriddenName()); assertEquals(message + ", RuleReference overridden description", ruleReference1.getOverriddenDescription(), ruleReference2.getOverriddenDescription()); assertEquals(message + ", RuleReference overridden message", ruleReference1.getOverriddenMessage(), ruleReference2.getOverriddenMessage()); assertEquals(message + ", RuleReference overridden external info url", ruleReference1.getOverriddenExternalInfoUrl(), ruleReference2.getOverriddenExternalInfoUrl()); assertEquals(message + ", RuleReference overridden priority", ruleReference1.getOverriddenPriority(), ruleReference2.getOverriddenPriority()); assertEquals(message + ", RuleReference overridden examples", ruleReference1.getOverriddenExamples(), ruleReference2.getOverriddenExamples()); } assertEquals(message + ", Rule name", rule1.getName(), rule2.getName()); assertEquals(message + ", Rule class", rule1.getRuleClass(), rule2.getRuleClass()); assertEquals(message + ", Rule description " + rule1.getName(), rule1.getDescription(), rule2.getDescription()); assertEquals(message + ", Rule message", rule1.getMessage(), rule2.getMessage()); assertEquals(message + ", Rule external info url", rule1.getExternalInfoUrl(), rule2.getExternalInfoUrl()); assertEquals(message + ", Rule priority", rule1.getPriority(), rule2.getPriority()); assertEquals(message + ", Rule examples", rule1.getExamples(), rule2.getExamples()); List<PropertyDescriptor<?>> propertyDescriptors1 = rule1.getPropertyDescriptors(); List<PropertyDescriptor<?>> propertyDescriptors2 = rule2.getPropertyDescriptors(); assertEquals(message + ", Rule property descriptor ", propertyDescriptors1, propertyDescriptors2); for (int j = 0; j < propertyDescriptors1.size(); j++) { assertEquals(message + ", Rule property value " + j, rule1.getProperty(propertyDescriptors1.get(j)), rule2.getProperty(propertyDescriptors2.get(j))); } assertEquals(message + ", Rule property descriptor count", propertyDescriptors1.size(), propertyDescriptors2.size()); } } /** * Create a {@link RuleSetReferenceId} by the given XML string. * * @param ruleSetXml * the ruleset file content as string * @return the {@link RuleSetReferenceId} */ protected static RuleSetReferenceId createRuleSetReferenceId(final String ruleSetXml) { return new RuleSetReferenceId(null) { @Override public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException { try { return new ByteArrayInputStream(ruleSetXml.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { return null; } } }; } /** * Validator for the SAX parser */ private static class ValidateDefaultHandler extends DefaultHandler { private final String validateDocument; private boolean valid = true; ValidateDefaultHandler(String validateDocument) { this.validateDocument = validateDocument; } public ValidateDefaultHandler resetValid() { valid = true; return this; } public boolean isValid() { return valid; } @Override public void error(SAXParseException e) throws SAXException { log("Error", e); } @Override public void fatalError(SAXParseException e) throws SAXException { log("FatalError", e); } @Override public void warning(SAXParseException e) throws SAXException { log("Warning", e); } private void log(String prefix, SAXParseException e) { String message = prefix + " at (" + e.getLineNumber() + ", " + e.getColumnNumber() + "): " + e.getMessage(); System.err.println(message); valid = false; } @Override public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException { if ("http://pmd.sourceforge.net/ruleset_2_0_0.xsd".equals(systemId) || systemId.endsWith("ruleset_2_0_0.dtd")) { try { InputStream inputStream = loadResourceAsStream(validateDocument); return new InputSource(inputStream); } catch (RuleSetNotFoundException e) { System.err.println(e.getMessage()); throw new IOException(e.getMessage()); } } throw new IllegalArgumentException( "No clue how to handle: publicId=" + publicId + ", systemId=" + systemId); } } }