View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd;
5   import static org.junit.Assert.assertEquals;
6   import static org.junit.Assert.assertFalse;
7   import static org.junit.Assert.assertTrue;
8   import static org.junit.Assert.fail;
9   
10  import java.io.BufferedReader;
11  import java.io.ByteArrayInputStream;
12  import java.io.ByteArrayOutputStream;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.InputStreamReader;
16  import java.io.UnsupportedEncodingException;
17  import java.util.ArrayList;
18  import java.util.List;
19  import java.util.Properties;
20  import java.util.StringTokenizer;
21  
22  import javax.xml.parsers.ParserConfigurationException;
23  import javax.xml.parsers.SAXParser;
24  import javax.xml.parsers.SAXParserFactory;
25  
26  import net.sourceforge.pmd.lang.Language;
27  import net.sourceforge.pmd.lang.LanguageRegistry;
28  import net.sourceforge.pmd.lang.rule.RuleReference;
29  import net.sourceforge.pmd.lang.rule.XPathRule;
30  import net.sourceforge.pmd.util.ResourceLoader;
31  
32  import org.junit.BeforeClass;
33  import org.junit.Test;
34  import org.xml.sax.InputSource;
35  import org.xml.sax.SAXException;
36  import org.xml.sax.SAXParseException;
37  import org.xml.sax.helpers.DefaultHandler;
38  
39  /**
40   * Base test class to verify the language's rulesets.
41   * This class should be subclassed for each language.
42   */
43  public abstract class AbstractRuleSetFactoryTest {
44      private static SAXParserFactory saxParserFactory;
45      private static ValidateDefaultHandler validateDefaultHandlerXsd;
46      private static ValidateDefaultHandler validateDefaultHandlerDtd;
47      private static SAXParser saxParser;
48  
49      /**
50       * Setups the XML parser with validation.
51       * @throws Exception any error
52       */
53      @BeforeClass
54      public static void init() throws Exception {
55          saxParserFactory = SAXParserFactory.newInstance();
56          saxParserFactory.setValidating(true);
57          saxParserFactory.setNamespaceAware(true);
58          
59          // Hope we're using Xerces, or this may not work!
60          // Note: Features are listed here
61          // http://xerces.apache.org/xerces2-j/features.html
62          saxParserFactory.setFeature("http://xml.org/sax/features/validation",
63                  true);
64          saxParserFactory.setFeature(
65                  "http://apache.org/xml/features/validation/schema", true);
66          saxParserFactory
67                  .setFeature(
68                          "http://apache.org/xml/features/validation/schema-full-checking",
69                          true);
70          
71          validateDefaultHandlerXsd = new ValidateDefaultHandler("ruleset_2_0_0.xsd");
72          validateDefaultHandlerDtd = new ValidateDefaultHandler("ruleset_2_0_0.dtd");
73          
74          saxParser = saxParserFactory.newSAXParser();
75      }
76  
77      /**
78       * Checks all rulesets of all languages on the classpath and verifies that all required attributes
79       * for all rules are specified.
80       * @throws Exception any error
81       */
82      @Test
83      public void testAllPMDBuiltInRulesMeetConventions() throws Exception {
84          int invalidSinceAttributes = 0;
85          int invalidExternalInfoURL = 0;
86          int invalidClassName = 0;
87          int invalidRegexSuppress = 0;
88          int invalidXPathSuppress = 0;
89          String messages = "";
90          List<String> ruleSetFileNames = getRuleSetFileNames();
91          for (String fileName : ruleSetFileNames) {
92              RuleSet ruleSet = loadRuleSetByFileName(fileName);
93              for (Rule rule : ruleSet.getRules()) {
94  
95                  // Skip references
96                  if (rule instanceof RuleReference) {
97                      continue;
98                  }
99  
100                 Language language = rule.getLanguage();
101                 String group = fileName.substring(fileName.lastIndexOf('/') + 1);
102                 group = group.substring(0, group.indexOf(".xml"));
103                 if (group.indexOf('-') >= 0) {
104                     group = group.substring(0, group.indexOf('-'));
105                 }
106 
107                 // Is since missing ?
108                 if (rule.getSince() == null) {
109                     invalidSinceAttributes++;
110                     messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'since' attribute" + PMD.EOL;
111                 }
112                 // Is URL valid ?
113                 if (rule.getExternalInfoUrl() == null || "".equalsIgnoreCase(rule.getExternalInfoUrl())) {
114                     invalidExternalInfoURL++;
115                     messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'externalInfoURL' attribute"
116                             + PMD.EOL;
117                 } else {
118                     String expectedExternalInfoURL = "https?://pmd.(sourceforge.net|github.io)/.+/rules/"
119                             + fileName.replaceAll("rulesets/", "").replaceAll(".xml", "") + ".html#" + rule.getName();
120                     if (rule.getExternalInfoUrl() == null
121                             || !rule.getExternalInfoUrl().matches(expectedExternalInfoURL)) {
122                         invalidExternalInfoURL++;
123                         messages += "Rule " + fileName + "/" + rule.getName()
124                                 + " seems to have an invalid 'externalInfoURL' value (" + rule.getExternalInfoUrl()
125                                 + "), it should be:" + expectedExternalInfoURL + PMD.EOL;
126                     }
127                 }
128                 // Proper class name/packaging?
129                 String expectedClassName = "net.sourceforge.pmd.lang." + language.getTerseName() + ".rule." + group
130                         + "." + rule.getName() + "Rule";
131                 if (!rule.getRuleClass().equals(expectedClassName)
132                         && !rule.getRuleClass().equals(XPathRule.class.getName())) {
133                     invalidClassName++;
134                     messages += "Rule " + fileName + "/" + rule.getName() + " seems to have an invalid 'class' value ("
135                             + rule.getRuleClass() + "), it should be:" + expectedClassName + PMD.EOL;
136                 }
137                 // Should not have violation suppress regex property
138                 if (rule.getProperty(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR) != null) {
139                     invalidRegexSuppress++;
140                     messages += "Rule " + fileName + "/" + rule.getName() + " should not have '"
141                             + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name()
142                             + "', this is intended for end user customization only." + PMD.EOL;
143                 }
144                 // Should not have violation suppress xpath property
145                 if (rule.getProperty(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR) != null) {
146                     invalidXPathSuppress++;
147                     messages += "Rule " + fileName + "/" + rule.getName() + " should not have '"
148                             + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name()
149                             + "', this is intended for end user customization only." + PMD.EOL;
150                 }
151             }
152         }
153         // We do this at the end to ensure we test ALL the rules before failing
154         // the test
155         if (invalidSinceAttributes > 0 || invalidExternalInfoURL > 0 || invalidClassName > 0
156                 || invalidRegexSuppress > 0 || invalidXPathSuppress > 0) {
157             fail("All built-in PMD rules need 'since' attribute (" + invalidSinceAttributes
158                     + " are missing), a proper ExternalURLInfo (" + invalidExternalInfoURL
159                     + " are invalid), a class name meeting conventions (" + invalidClassName + " are invalid), no '"
160                     + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name() + "' property (" + invalidRegexSuppress
161                     + " are invalid), and no '" + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name() + "' property ("
162                     + invalidXPathSuppress + " are invalid)" + PMD.EOL + messages);
163         }
164     }
165 
166     /**
167      * Verifies that all rulesets are valid XML according to the xsd schema.
168      * @throws Exception any error
169      */
170     @Test
171     public void testXmlSchema() throws Exception {
172         boolean allValid = true;
173         List<String> ruleSetFileNames = getRuleSetFileNames();
174         for (String fileName : ruleSetFileNames) {
175             boolean valid = validateAgainstSchema(fileName);
176             allValid = allValid && valid;
177         }
178         assertTrue("All XML must parse without producing validation messages.", allValid);
179     }
180 
181     /**
182      * Verifies that all rulesets are valid XML according to the DTD.
183      * @throws Exception any error
184      */
185     @Test
186     public void testDtd() throws Exception {
187         boolean allValid = true;
188         List<String> ruleSetFileNames = getRuleSetFileNames();
189         for (String fileName : ruleSetFileNames) {
190             boolean valid = validateAgainstDtd(fileName);
191             allValid = allValid && valid;
192         }
193         assertTrue("All XML must parse without producing validation messages.", allValid);
194     }
195 
196     /**
197      * Reads and writes the rulesets to make sure, that no data is lost if the rulests are
198      * processed.
199      * @throws Exception any error
200      */
201     @Test
202     public void testReadWriteRoundTrip() throws Exception {
203 
204         List<String> ruleSetFileNames = getRuleSetFileNames();
205         for (String fileName : ruleSetFileNames) {
206             testRuleSet(fileName);
207         }
208     }
209 
210     // Gets all test PMD Ruleset XML files
211     private List<String> getRuleSetFileNames() throws IOException, RuleSetNotFoundException {
212         List<String> result = new ArrayList<>();
213 
214         for (Language language : LanguageRegistry.getLanguages()) {
215             result.addAll(getRuleSetFileNames(language.getTerseName()));
216         }
217 
218         return result;
219     }
220 
221     private List<String> getRuleSetFileNames(String language) throws IOException, RuleSetNotFoundException {
222         List<String> ruleSetFileNames = new ArrayList<>();
223         try {
224             Properties properties = new Properties();
225             properties.load(ResourceLoader.loadResourceAsStream("rulesets/" + language + "/rulesets.properties"));
226             String fileNames = properties.getProperty("rulesets.filenames");
227             StringTokenizer st = new StringTokenizer(fileNames, ",");
228             while (st.hasMoreTokens()) {
229                 ruleSetFileNames.add(st.nextToken());
230             }
231         } catch (RuleSetNotFoundException e) {
232             // this might happen if a language is only support by CPD, but not
233             // by PMD
234             System.err.println("No ruleset found for language " + language);
235         }
236         return ruleSetFileNames;
237     }
238 
239     private RuleSet loadRuleSetByFileName(String ruleSetFileName) throws RuleSetNotFoundException {
240         RuleSetFactory rsf = new RuleSetFactory();
241         return rsf.createRuleSet(ruleSetFileName);
242     }
243 
244     private boolean validateAgainstSchema(String fileName) throws IOException, RuleSetNotFoundException,
245             ParserConfigurationException, SAXException {
246         InputStream inputStream = loadResourceAsStream(fileName);
247         boolean valid = validateAgainstSchema(inputStream);
248         if (!valid) {
249             System.err.println("Validation against XML Schema failed for: " + fileName);
250         }
251         return valid;
252     }
253 
254     private boolean validateAgainstSchema(InputStream inputStream) throws IOException, RuleSetNotFoundException,
255             ParserConfigurationException, SAXException {
256 
257         saxParser.parse(inputStream, validateDefaultHandlerXsd.resetValid());
258         inputStream.close();
259         return validateDefaultHandlerXsd.isValid();
260     }
261 
262     private boolean validateAgainstDtd(String fileName) throws IOException, RuleSetNotFoundException,
263             ParserConfigurationException, SAXException {
264         InputStream inputStream = loadResourceAsStream(fileName);
265         boolean valid = validateAgainstDtd(inputStream);
266         if (!valid) {
267             System.err.println("Validation against DTD failed for: " + fileName);
268         }
269         return valid;
270     }
271 
272     private boolean validateAgainstDtd(InputStream inputStream) throws IOException, RuleSetNotFoundException,
273             ParserConfigurationException, SAXException {
274 
275         // Read file into memory
276         String file = readFullyToString(inputStream);
277         inputStream.close();
278 
279         // Remove XML Schema stuff, replace with DTD
280         file = file.replaceAll("<\\?xml [ a-zA-Z0-9=\".-]*\\?>", "");
281         file = file.replaceAll("xmlns=\"" + RuleSetWriter.RULESET_NS_URI + "\"", "");
282         file = file.replaceAll("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", "");
283         file = file.replaceAll("xsi:schemaLocation=\"" + RuleSetWriter.RULESET_NS_URI
284                 + " http://pmd.sourceforge.net/ruleset_2_0_0.xsd\"", "");
285 
286         file = "<?xml version=\"1.0\"?>" + PMD.EOL + "<!DOCTYPE ruleset SYSTEM \"file://"
287                 + "/path/does/not/matter/will/be/replaced/ruleset_2_0_0.dtd\">" + PMD.EOL + file;
288 
289         InputStream modifiedStream = new ByteArrayInputStream(file.getBytes());
290 
291         saxParser.parse(modifiedStream, validateDefaultHandlerDtd.resetValid());
292         modifiedStream.close();
293         return validateDefaultHandlerDtd.isValid();
294     }
295 
296     private String readFullyToString(InputStream inputStream) throws IOException {
297         StringBuilder buf = new StringBuilder(64 * 1024);
298         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
299         String line;
300         while ((line = reader.readLine()) != null) {
301             buf.append(line);
302             buf.append(PMD.EOL);
303         }
304         reader.close();
305         return buf.toString();
306     }
307 
308     private static InputStream loadResourceAsStream(String resource) throws RuleSetNotFoundException {
309         InputStream inputStream = ResourceLoader.loadResourceAsStream(resource,
310                 AbstractRuleSetFactoryTest.class.getClassLoader());
311         if (inputStream == null) {
312             throw new RuleSetNotFoundException(
313                     "Can't find resource "
314                             + resource
315                             + "  Make sure the resource is a valid file or URL or is on the CLASSPATH.  Here's the current classpath: "
316                             + System.getProperty("java.class.path"));
317         }
318         return inputStream;
319     }
320 
321     private void testRuleSet(String fileName) throws IOException, RuleSetNotFoundException,
322             ParserConfigurationException, SAXException {
323 
324         // Load original XML
325         // String xml1 =
326         // readFullyToString(ResourceLoader.loadResourceAsStream(fileName));
327         // System.out.println("xml1: " + xml1);
328 
329         // Load the original RuleSet
330         RuleSet ruleSet1 = loadRuleSetByFileName(fileName);
331 
332         // Write to XML, first time
333         ByteArrayOutputStream outputStream1 = new ByteArrayOutputStream();
334         RuleSetWriter writer1 = new RuleSetWriter(outputStream1);
335         writer1.write(ruleSet1);
336         writer1.close();
337         String xml2 = new String(outputStream1.toByteArray());
338         // System.out.println("xml2: " + xml2);
339 
340         // Read RuleSet from XML, first time
341         RuleSetFactory ruleSetFactory = new RuleSetFactory();
342         RuleSet ruleSet2 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml2));
343 
344         // Do write/read a 2nd time, just to be sure
345 
346         // Write to XML, second time
347         ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
348         RuleSetWriter writer2 = new RuleSetWriter(outputStream2);
349         writer2.write(ruleSet2);
350         writer2.close();
351         String xml3 = new String(outputStream2.toByteArray());
352         // System.out.println("xml3: " + xml3);
353 
354         // Read RuleSet from XML, second time
355         RuleSet ruleSet3 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml3));
356 
357         // The 2 written XMLs should all be valid w.r.t Schema/DTD
358         assertTrue("1st roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")",
359                 validateAgainstSchema(new ByteArrayInputStream(xml2.getBytes())));
360         assertTrue("2nd roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")",
361                 validateAgainstSchema(new ByteArrayInputStream(xml3.getBytes())));
362         assertTrue("1st roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")",
363                 validateAgainstDtd(new ByteArrayInputStream(xml2.getBytes())));
364         assertTrue("2nd roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")",
365                 validateAgainstDtd(new ByteArrayInputStream(xml3.getBytes())));
366 
367         // All 3 versions of the RuleSet should be the same
368         assertEqualsRuleSet("Original RuleSet and 1st roundtrip Ruleset not the same (filename: " + fileName + ")",
369                 ruleSet1, ruleSet2);
370         assertEqualsRuleSet(
371                 "1st roundtrip Ruleset and 2nd roundtrip RuleSet not the same (filename: " + fileName + ")", ruleSet2,
372                 ruleSet3);
373 
374         // It's hard to compare the XML DOMs. At least the roundtrip ones should
375         // textually be the same.
376         assertEquals("1st roundtrip RuleSet XML and 2nd roundtrip RuleSet XML (filename: " + fileName + ")", xml2, xml3);
377     }
378 
379     private void assertEqualsRuleSet(String message, RuleSet ruleSet1,
380             RuleSet ruleSet2) {
381         assertEquals(message + ", RuleSet name", ruleSet1.getName(), ruleSet2
382                 .getName());
383         assertEquals(message + ", RuleSet description", ruleSet1
384                 .getDescription(), ruleSet2.getDescription());
385         assertEquals(message + ", RuleSet exclude patterns", ruleSet1
386                 .getExcludePatterns(), ruleSet2.getExcludePatterns());
387         assertEquals(message + ", RuleSet include patterns", ruleSet1
388                 .getIncludePatterns(), ruleSet2.getIncludePatterns());
389         assertEquals(message + ", RuleSet rule count", ruleSet1.getRules()
390                 .size(), ruleSet2.getRules().size());
391 
392         for (int i = 0; i < ruleSet1.getRules().size(); i++) {
393             Rule rule1 = ((List<Rule>) ruleSet1.getRules()).get(i);
394             Rule rule2 = ((List<Rule>) ruleSet2.getRules()).get(i);
395 
396             assertFalse(message + ", Different RuleReference",
397                     rule1 instanceof RuleReference
398                             && !(rule2 instanceof RuleReference)
399                             || !(rule1 instanceof RuleReference)
400                             && rule2 instanceof RuleReference);
401 
402             if (rule1 instanceof RuleReference) {
403                 RuleReference ruleReference1 = (RuleReference) rule1;
404                 RuleReference ruleReference2 = (RuleReference) rule2;
405                 assertEquals(message + ", RuleReference overridden language",
406                         ruleReference1.getOverriddenLanguage(), ruleReference2
407                                 .getOverriddenLanguage());
408                 assertEquals(
409                         message
410                                 + ", RuleReference overridden minimum language version",
411                         ruleReference1.getOverriddenMinimumLanguageVersion(),
412                         ruleReference2.getOverriddenMinimumLanguageVersion());
413                 assertEquals(
414                         message
415                                 + ", RuleReference overridden maximum language version",
416                         ruleReference1.getOverriddenMaximumLanguageVersion(),
417                         ruleReference2.getOverriddenMaximumLanguageVersion());
418                 assertEquals(message + ", RuleReference overridden deprecated",
419                         ruleReference1.isOverriddenDeprecated(), ruleReference2
420                                 .isOverriddenDeprecated());
421                 assertEquals(message + ", RuleReference overridden name",
422                         ruleReference1.getOverriddenName(), ruleReference2
423                                 .getOverriddenName());
424                 assertEquals(
425                         message + ", RuleReference overridden description",
426                         ruleReference1.getOverriddenDescription(),
427                         ruleReference2.getOverriddenDescription());
428                 assertEquals(message + ", RuleReference overridden message",
429                         ruleReference1.getOverriddenMessage(), ruleReference2
430                                 .getOverriddenMessage());
431                 assertEquals(message
432                         + ", RuleReference overridden external info url",
433                         ruleReference1.getOverriddenExternalInfoUrl(),
434                         ruleReference2.getOverriddenExternalInfoUrl());
435                 assertEquals(message + ", RuleReference overridden priority",
436                         ruleReference1.getOverriddenPriority(), ruleReference2
437                                 .getOverriddenPriority());
438                 assertEquals(message + ", RuleReference overridden examples",
439                         ruleReference1.getOverriddenExamples(), ruleReference2
440                                 .getOverriddenExamples());
441             }
442 
443             assertEquals(message + ", Rule name", rule1.getName(), rule2
444                     .getName());
445             assertEquals(message + ", Rule class", rule1.getRuleClass(), rule2
446                     .getRuleClass());
447             assertEquals(message + ", Rule description " + rule1.getName(),
448                     rule1.getDescription(), rule2.getDescription());
449             assertEquals(message + ", Rule message", rule1.getMessage(), rule2
450                     .getMessage());
451             assertEquals(message + ", Rule external info url", rule1
452                     .getExternalInfoUrl(), rule2.getExternalInfoUrl());
453             assertEquals(message + ", Rule priority", rule1.getPriority(),
454                     rule2.getPriority());
455             assertEquals(message + ", Rule examples", rule1.getExamples(),
456                     rule2.getExamples());
457 
458             List<PropertyDescriptor<?>> propertyDescriptors1 = rule1
459                     .getPropertyDescriptors();
460             List<PropertyDescriptor<?>> propertyDescriptors2 = rule2
461                     .getPropertyDescriptors();
462             assertEquals(message + ", Rule property descriptor ",
463                     propertyDescriptors1, propertyDescriptors2);
464             for (int j = 0; j < propertyDescriptors1.size(); j++) {
465                 assertEquals(message + ", Rule property value " + j, rule1
466                         .getProperty(propertyDescriptors1.get(j)), rule2
467                         .getProperty(propertyDescriptors2.get(j)));
468             }
469             assertEquals(message + ", Rule property descriptor count",
470                     propertyDescriptors1.size(), propertyDescriptors2.size());
471         }
472     }
473 
474     /**
475      * Create a {@link RuleSetReferenceId} by the given XML string.
476      * @param ruleSetXml the ruleset file content as string
477      * @return the {@link RuleSetReferenceId}
478      */
479     protected static RuleSetReferenceId createRuleSetReferenceId(final String ruleSetXml) {
480         return new RuleSetReferenceId(null) {
481             @Override
482             public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
483                 try {
484                     return new ByteArrayInputStream(ruleSetXml.getBytes("UTF-8"));
485                 } catch (UnsupportedEncodingException e) {
486                     return null;
487                 }
488             }
489         };
490     }
491 
492     /**
493      * Validator for the SAX parser
494      */
495     private static class ValidateDefaultHandler extends DefaultHandler {
496         private final String validateDocument;
497         private boolean valid = true;
498 
499         public ValidateDefaultHandler(String validateDocument) {
500             this.validateDocument = validateDocument;
501         }
502         
503         public ValidateDefaultHandler resetValid() {
504             valid = true;
505             return this;
506         }
507 
508         public boolean isValid() {
509             return valid;
510         }
511 
512         @Override
513         public void error(SAXParseException e) throws SAXException {
514             log("Error", e);
515         }
516 
517         @Override
518         public void fatalError(SAXParseException e) throws SAXException {
519             log("FatalError", e);
520         }
521 
522         @Override
523         public void warning(SAXParseException e) throws SAXException {
524             log("Warning", e);
525         }
526 
527         private void log(String prefix, SAXParseException e) {
528             String message = prefix + " at (" + e.getLineNumber() + ", " + e.getColumnNumber() + "): " + e.getMessage();
529             System.err.println(message);
530             valid = false;
531         }
532 
533         @Override
534         public InputSource resolveEntity(String publicId, String systemId)
535                 throws IOException, SAXException {
536             if ("http://pmd.sourceforge.net/ruleset_2_0_0.xsd".equals(systemId)
537                     || systemId.endsWith("ruleset_2_0_0.dtd")) {
538                 try {
539                     InputStream inputStream = loadResourceAsStream(validateDocument);
540                     return new InputSource(inputStream);
541                 } catch (RuleSetNotFoundException e) {
542                     System.err.println(e.getMessage());
543                     throw new IOException(e.getMessage());
544                 }
545             }
546             throw new IllegalArgumentException(
547                     "No clue how to handle: publicId=" + publicId
548                             + ", systemId=" + systemId);
549         }
550     }
551 
552 }