• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 213

24 Oct 2025 08:38AM UTC coverage: 78.661% (-0.01%) from 78.671%
213

push

github

web-flow
[java] Fix #6038: Make AvoidCatchingGenericException configurable (#6051)

18232 of 24027 branches covered (75.88%)

Branch coverage included in aggregate %.

0 of 14 new or added lines in 1 file covered. (0.0%)

39748 of 49682 relevant lines covered (80.0%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/pmd-test/src/main/java/net/sourceforge/pmd/test/lang/rule/AbstractRuleSetFactoryTest.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.test.lang.rule;
6

7
import static org.hamcrest.MatcherAssert.assertThat;
8
import static org.hamcrest.Matchers.containsString;
9
import static org.hamcrest.Matchers.emptyString;
10
import static org.junit.jupiter.api.Assertions.assertEquals;
11
import static org.junit.jupiter.api.Assertions.assertFalse;
12
import static org.junit.jupiter.api.Assertions.assertTrue;
13
import static org.junit.jupiter.api.Assertions.fail;
14
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
15

16
import java.io.BufferedReader;
17
import java.io.ByteArrayInputStream;
18
import java.io.ByteArrayOutputStream;
19
import java.io.FileNotFoundException;
20
import java.io.IOException;
21
import java.io.InputStream;
22
import java.io.InputStreamReader;
23
import java.nio.charset.StandardCharsets;
24
import java.util.ArrayList;
25
import java.util.Arrays;
26
import java.util.Collections;
27
import java.util.HashMap;
28
import java.util.HashSet;
29
import java.util.List;
30
import java.util.Locale;
31
import java.util.Map;
32
import java.util.Properties;
33
import java.util.Set;
34
import java.util.StringTokenizer;
35
import java.util.regex.Pattern;
36
import javax.xml.parsers.SAXParser;
37
import javax.xml.parsers.SAXParserFactory;
38

39
import org.junit.jupiter.api.BeforeAll;
40
import org.junit.jupiter.api.Test;
41
import org.junit.jupiter.api.TestInstance;
42
import org.junit.jupiter.params.ParameterizedTest;
43
import org.junit.jupiter.params.provider.MethodSource;
44
import org.slf4j.event.Level;
45
import org.xml.sax.InputSource;
46
import org.xml.sax.SAXException;
47
import org.xml.sax.SAXParseException;
48
import org.xml.sax.helpers.DefaultHandler;
49

50
import net.sourceforge.pmd.internal.util.IOUtil;
51
import net.sourceforge.pmd.lang.Language;
52
import net.sourceforge.pmd.lang.LanguageRegistry;
53
import net.sourceforge.pmd.lang.rule.InternalApiBridge;
54
import net.sourceforge.pmd.lang.rule.Rule;
55
import net.sourceforge.pmd.lang.rule.RuleReference;
56
import net.sourceforge.pmd.lang.rule.RuleSet;
57
import net.sourceforge.pmd.lang.rule.RuleSetLoader;
58
import net.sourceforge.pmd.lang.rule.RuleSetWriter;
59
import net.sourceforge.pmd.lang.rule.impl.UnnecessaryPmdSuppressionRule;
60
import net.sourceforge.pmd.lang.rule.xpath.XPathRule;
61
import net.sourceforge.pmd.properties.PropertyDescriptor;
62
import net.sourceforge.pmd.util.log.internal.MessageReporterBase;
63

64
/**
65
 * Base test class to verify the language's rulesets. This class should be
66
 * subclassed for each language.
67
 */
68
@TestInstance(PER_CLASS)
69
public abstract class AbstractRuleSetFactoryTest {
70

71
    private static ValidateDefaultHandler validateDefaultHandler;
72
    private static SAXParser saxParser;
73

74
    // todo rename this field to validCoreRules or something. Make private.
75
    protected Set<String> validXPathClassNames = new HashSet<>();
×
76
    private final Set<String> languagesToSkip = new HashSet<>();
×
NEW
77
    private final Map<String, Set<String>> expectedMessagesPerRuleset = new HashMap<>();
×
78

79
    public AbstractRuleSetFactoryTest() {
80
        this(new String[0]);
×
81
    }
×
82

83
    public AbstractRuleSetFactoryTest(Map<String, Set<String>> expectedMessagesPerRuleset) {
NEW
84
        this();
×
NEW
85
        this.expectedMessagesPerRuleset.putAll(expectedMessagesPerRuleset);
×
NEW
86
    }
×
87

88
    /**
89
     * Constructor used when a module that depends on another module wants to filter out the dependee's rulesets.
90
     *
91
     * @param languagesToSkip {@link Language}s terse names that appear in the classpath via a dependency, but should be
92
     * skipped because they aren't the primary language which the concrete instance of this class is testing.
93
     */
94
    public AbstractRuleSetFactoryTest(String... languagesToSkip) {
×
95
        this.languagesToSkip.add("dummy");
×
96
        this.languagesToSkip.addAll(Arrays.asList(languagesToSkip));
×
97
        validXPathClassNames.add(XPathRule.class.getName());
×
98
        validXPathClassNames.add(UnnecessaryPmdSuppressionRule.class.getName());
×
99
    }
×
100

101
    public AbstractRuleSetFactoryTest(Language... languagesToSkip) {
102
        this(Arrays.stream(languagesToSkip).map(Language::getId).toArray(String[]::new));
×
103
    }
×
104

105

106
    /**
107
     * Setups the XML parser with validation.
108
     *
109
     * @throws Exception
110
     *             any error
111
     */
112
    @BeforeAll
113
    static void init() throws Exception {
114
        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
×
115
        saxParserFactory.setValidating(true);
×
116
        saxParserFactory.setNamespaceAware(true);
×
117

118
        // Hope we're using Xerces, or this may not work!
119
        // Note: Features are listed here
120
        // http://xerces.apache.org/xerces2-j/features.html
121
        saxParserFactory.setFeature("http://xml.org/sax/features/validation", true);
×
122
        saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema", true);
×
123
        saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true);
×
124

125
        validateDefaultHandler = new ValidateDefaultHandler();
×
126

127
        saxParser = saxParserFactory.newSAXParser();
×
128
    }
×
129

130
    /**
131
     * Checks all rulesets of all languages on the classpath and verifies that
132
     * all required attributes for all rules are specified.
133
     *
134
     * @throws Exception
135
     *             any error
136
     */
137
    @ParameterizedTest
138
    @MethodSource("getRuleSetFileNames")
139
    void testAllPMDBuiltInRulesMeetConventions(String fileName) throws Exception {
140
        int invalidSinceAttributes = 0;
×
141
        int invalidExternalInfoURL = 0;
×
142
        int invalidClassName = 0;
×
143
        int invalidRegexSuppress = 0;
×
144
        int invalidXPathSuppress = 0;
×
145
        int invalidOrder = 0;
×
146
        StringBuilder messages = new StringBuilder();
×
147
        String lastName = null;
×
148
        RuleSet ruleSet = loadRuleSetByFileName(fileName);
×
149
        for (Rule rule : ruleSet.getRules()) {
×
150

151
            // Skip references
152
            if (rule instanceof RuleReference) {
×
153
                continue;
×
154
            }
155
            if (lastName != null
×
156
                    && String.CASE_INSENSITIVE_ORDER.compare(rule.getName(), lastName) < 0) {
×
157
                invalidOrder++;
×
158
                messages.append(rule.getName()).append(" should be before ")
×
159
                    .append(lastName).append("\n");
×
160
            }
161
            lastName = rule.getName();
×
162

163
            Language language = rule.getLanguage();
×
164
            String group = fileName.substring(fileName.lastIndexOf('/') + 1);
×
165
            group = group.substring(0, group.indexOf(".xml"));
×
166
            if (group.indexOf('-') >= 0) {
×
167
                group = group.substring(0, group.indexOf('-'));
×
168
            }
169

170
            // Is since missing ?
171
            if (rule.getSince() == null) {
×
172
                invalidSinceAttributes++;
×
173
                messages.append("Rule ")
×
174
                        .append(fileName)
×
175
                        .append("/")
×
176
                        .append(rule.getName())
×
177
                        .append(" is missing 'since' attribute\n");
×
178
            }
179
            // Is URL valid ?
180
            if (rule.getExternalInfoUrl() == null || "".equalsIgnoreCase(rule.getExternalInfoUrl())) {
×
181
                invalidExternalInfoURL++;
×
182
                messages.append("Rule ")
×
183
                        .append(fileName)
×
184
                        .append("/")
×
185
                        .append(rule.getName())
×
186
                        .append(" is missing 'externalInfoURL' attribute\n");
×
187
            } else {
188
                String expectedExternalInfoURL = "https://docs.pmd-code.org/.+/pmd_rules_"
×
189
                        + language.getId() + "_"
×
190
                        + IOUtil.getFilenameBase(fileName)
×
191
                        + ".html#"
192
                        + rule.getName().toLowerCase(Locale.ROOT);
×
193
                if (rule.getExternalInfoUrl() == null
×
194
                        || !rule.getExternalInfoUrl().matches(expectedExternalInfoURL)) {
×
195
                    invalidExternalInfoURL++;
×
196
                    messages.append("Rule ")
×
197
                            .append(fileName)
×
198
                            .append("/")
×
199
                            .append(rule.getName())
×
200
                            .append(" seems to have an invalid 'externalInfoURL' value (")
×
201
                            .append(rule.getExternalInfoUrl())
×
202
                            .append("), it should be:")
×
203
                            .append(expectedExternalInfoURL)
×
204
                            .append('\n');
×
205
                }
206
            }
207
            // Proper class name/packaging?
208
            String expectedClassName = "net.sourceforge.pmd.lang." + language.getId() + ".rule." + group
×
209
                    + "." + rule.getName() + "Rule";
×
210
            if (!rule.getRuleClass().equals(expectedClassName)
×
211
                    && !validXPathClassNames.contains(rule.getRuleClass())) {
×
212
                invalidClassName++;
×
213
                messages.append("Rule ")
×
214
                        .append(fileName)
×
215
                        .append("/")
×
216
                        .append(rule.getName())
×
217
                        .append(" seems to have an invalid 'class' value (")
×
218
                        .append(rule.getRuleClass())
×
219
                        .append("), it should be:")
×
220
                        .append(expectedClassName)
×
221
                        .append('\n');
×
222
            }
223
            // Should not have violation suppress regex property
224
            if (rule.getProperty(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR).isPresent()) {
×
225
                invalidRegexSuppress++;
×
226
                messages.append("Rule ")
×
227
                        .append(fileName)
×
228
                        .append("/")
×
229
                        .append(rule.getName())
×
230
                        .append(" should not have '")
×
231
                        .append(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name())
×
232
                        .append("', this is intended for end user customization only.\n");
×
233
            }
234
            // Should not have violation suppress xpath property
235
            if (rule.getProperty(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR).isPresent()) {
×
236
                invalidXPathSuppress++;
×
237
                messages.append("Rule ").append(fileName).append("/").append(rule.getName()).append(" should not have '").append(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name()).append("', this is intended for end user customization only.").append(System.lineSeparator());
×
238
            }
239
        }
×
240
        // We do this at the end to ensure we test ALL the rules before failing
241
        // the test
242
        if (invalidSinceAttributes > 0 || invalidExternalInfoURL > 0 || invalidClassName > 0 || invalidRegexSuppress > 0
×
243
                || invalidXPathSuppress > 0 || invalidOrder > 0) {
244
            fail("All built-in PMD rules need 'since' attribute (" + invalidSinceAttributes
×
245
                    + " are missing), a proper ExternalURLInfo (" + invalidExternalInfoURL
246
                    + " are invalid), a class name meeting conventions (" + invalidClassName + " are invalid), no '"
247
                    + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name() + "' property (" + invalidRegexSuppress
×
248
                    + " are invalid), and no '" + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name() + "' property ("
×
249
                    + invalidXPathSuppress + " are invalid) and be alphabetically sorted ("
250
                    + invalidOrder + " misplaced)\n" + messages);
251
        }
252
    }
×
253

254
    /**
255
     * Verifies that all rulesets are valid XML according to the xsd schema.
256
     *
257
     * @throws Exception
258
     *             any error
259
     */
260
    @Test
261
    void testXmlSchema() throws Exception {
262
        boolean allValid = true;
×
263
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
264
        for (String fileName : ruleSetFileNames) {
×
265
            boolean valid = validateAgainstSchema(fileName);
×
266
            allValid = allValid && valid;
×
267
        }
×
268
        assertTrue(allValid, "All XML must parse without producing validation messages.");
×
269
    }
×
270

271
    /**
272
     * @deprecated Since 7.17.0. This method will be removed. PMD has the rule "MissingEncoding" for XML files that
273
     *             is used instead.
274
     */
275
    @Deprecated
276
    public static boolean hasCorrectEncoding(String fileName) throws IOException {
277
        try (InputStream inputStream = loadResourceAsStream(fileName)) {
×
278
            // first bytes must be:
279
            byte[] expectedBytes = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>".getBytes(StandardCharsets.UTF_8);
×
280
            byte[] bytes = new byte[expectedBytes.length];
×
281
            int count = inputStream.read(bytes);
×
282
            if (count != expectedBytes.length || !Arrays.equals(expectedBytes, bytes)) {
×
283
                return false;
×
284
            }
285
        }
×
286
        return true;
×
287
    }
288

289
    /**
290
     * Verifies that all rulesets are valid XML according to the DTD.
291
     *
292
     * @throws Exception
293
     *             any error
294
     */
295
    @Test
296
    void testDtd() throws Exception {
297
        boolean allValid = true;
×
298
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
299
        for (String fileName : ruleSetFileNames) {
×
300
            boolean valid = validateAgainstDtd(fileName);
×
301
            allValid = allValid && valid;
×
302
        }
×
303
        assertTrue(allValid, "All XML must parse without producing validation messages.");
×
304
    }
×
305

306
    /**
307
     * Reads and writes the rulesets to make sure, that no data is lost if the
308
     * rulests are processed.
309
     *
310
     * @throws Exception
311
     *             any error
312
     */
313
    @Test
314
    void testReadWriteRoundTrip() throws Exception {
315

316
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
317
        for (String fileName : ruleSetFileNames) {
×
318
            testRuleSet(fileName);
×
319
        }
×
320

321
    }
×
322

323
    // Gets all test PMD Ruleset XML files
324
    private List<String> getRuleSetFileNames() throws IOException {
325
        List<String> result = new ArrayList<>();
×
326

327
        for (Language language : LanguageRegistry.PMD.getLanguages()) {
×
328
            if (this.languagesToSkip.contains(language.getId())) {
×
329
                continue;
×
330
            }
331
            result.addAll(getRuleSetFileNames(language.getId()));
×
332
        }
×
333

334
        return result;
×
335
    }
336

337
    private List<String> getRuleSetFileNames(String language) throws IOException {
338
        List<String> ruleSetFileNames = new ArrayList<>();
×
339
        ruleSetFileNames.addAll(getRuleSetFileNames(language, "rulesets/" + language + "/rulesets.properties"));
×
340
        ruleSetFileNames.addAll(getRuleSetFileNames(language, "category/" + language + "/categories.properties"));
×
341
        return ruleSetFileNames;
×
342
    }
343

344
    private List<String> getRuleSetFileNames(String language, String propertiesPath) throws IOException {
345
        List<String> ruleSetFileNames = new ArrayList<>();
×
346
        Properties properties = new Properties();
×
347
        @SuppressWarnings("PMD.CloseResource")
348
        InputStream input = loadResourceAsStream(propertiesPath);
×
349
        if (input == null) {
×
350
            // this might happen if a language is only support by CPD, but not
351
            // by PMD
352
            System.err.println("No rulesets found for language " + language + " at " + propertiesPath);
×
353
            return Collections.emptyList();
×
354
        }
355
        try (InputStream is = input) {
×
356
            properties.load(is);
×
357
        }
358
        String fileNames = properties.getProperty("rulesets.filenames");
×
359
        StringTokenizer st = new StringTokenizer(fileNames, ",");
×
360
        while (st.hasMoreTokens()) {
×
361
            ruleSetFileNames.add(st.nextToken());
×
362
        }
363
        return ruleSetFileNames;
×
364
    }
365

366
    private RuleSet loadRuleSetByFileName(String ruleSetFileName) {
367
        final StringBuilder messages = new StringBuilder();
×
368
        class Reporter extends MessageReporterBase {
×
369
            @Override
370
            protected void logImpl(Level level, String message) {
371
                messages.append(message).append(System.lineSeparator());
×
372
            }
×
373
        }
374

375
        RuleSet ruleSet = InternalApiBridge.withReporter(new RuleSetLoader(), new Reporter())
×
376
                .loadFromResource(ruleSetFileName);
×
377

378
        // normalize all line-endings to \n - in case we run under Windows...
NEW
379
        String allMessages = messages.toString().replaceAll("\\R", "\n");
×
380

NEW
381
        if (expectedMessagesPerRuleset.containsKey(ruleSetFileName)) {
×
NEW
382
            for (String expectedMessage : expectedMessagesPerRuleset.get(ruleSetFileName)) {
×
NEW
383
                assertThat(allMessages, containsString(expectedMessage));
×
NEW
384
                allMessages = allMessages.replace(expectedMessage, "");
×
NEW
385
            }
×
NEW
386
            assertThat("There should be no other warnings while loading the ruleset, but found: " + allMessages,
×
NEW
387
                    allMessages, emptyString());
×
388
        } else {
NEW
389
            assertThat("There should be no warnings while loading the ruleset, but found: " + allMessages,
×
NEW
390
                    allMessages, emptyString());
×
391
        }
392

393
        return ruleSet;
×
394
    }
395

396
    private boolean validateAgainstSchema(String fileName) throws IOException, SAXException {
397
        try (InputStream inputStream = loadResourceAsStream(fileName)) {
×
398
            boolean valid = validateAgainstSchema(inputStream);
×
399
            if (!valid) {
×
400
                System.err.println("Validation against XML Schema failed for: " + fileName);
×
401
            }
402
            return valid;
×
403
        }
404
    }
405

406
    private boolean validateAgainstSchema(InputStream inputStream) throws IOException, SAXException {
407

408
        saxParser.parse(inputStream, validateDefaultHandler.resetValid());
×
409
        inputStream.close();
×
410
        return validateDefaultHandler.isValid();
×
411
    }
412

413
    private boolean validateAgainstDtd(String fileName) throws IOException, SAXException {
414
        try (InputStream inputStream = loadResourceAsStream(fileName)) {
×
415
            boolean valid = validateAgainstDtd(inputStream);
×
416
            if (!valid) {
×
417
                System.err.println("Validation against DTD failed for: " + fileName);
×
418
            }
419
            return valid;
×
420
        }
421
    }
422

423
    private boolean validateAgainstDtd(InputStream inputStream) throws IOException, SAXException {
424

425
        // Read file into memory
426
        String file = readFullyToString(inputStream);
×
427
        inputStream.close();
×
428

429
        String rulesetNamespace = RuleSetWriter.RULESET_2_0_0_NS_URI;
×
430

431
        // Remove XML Schema stuff, replace with DTD
432
        file = file.replaceAll("<\\?xml [ a-zA-Z0-9=\".-]*\\?>", "");
×
433
        file = file.replaceAll("xmlns=\"" + rulesetNamespace + "\"", "");
×
434
        file = file.replaceAll("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", "");
×
435
        file = file.replaceAll("xsi:schemaLocation=\"" + rulesetNamespace
×
436
                + " https://pmd.sourceforge.io/ruleset_\\d_0_0.xsd\"", "");
437

438
        if (RuleSetWriter.RULESET_2_0_0_NS_URI.equals(rulesetNamespace)) {
×
439
            file = "<?xml version=\"1.0\"?>" + System.lineSeparator()
×
440
                + "<!DOCTYPE ruleset SYSTEM \"https://pmd.sourceforge.io/ruleset_2_0_0.dtd\">" + System.lineSeparator()
×
441
                + file;
442
        } else {
443
            file = "<?xml version=\"1.0\"?>" + System.lineSeparator()
×
444
                + "<!DOCTYPE ruleset>" + System.lineSeparator()
×
445
                + file;
446
        }
447

448
        try (InputStream modifiedStream = new ByteArrayInputStream(file.getBytes(StandardCharsets.UTF_8))) {
×
449
            saxParser.parse(modifiedStream, validateDefaultHandler.resetValid());
×
450
        }
451
        return validateDefaultHandler.isValid();
×
452
    }
453

454
    private String readFullyToString(InputStream inputStream) throws IOException {
455
        StringBuilder buf = new StringBuilder(64 * 1024);
×
456
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
×
457
            String line;
458
            while ((line = reader.readLine()) != null) {
×
459
                buf.append(line);
×
460
                buf.append(System.lineSeparator());
×
461
            }
462
            return buf.toString();
×
463
        }
464
    }
465

466
    private static InputStream loadResourceAsStream(String resource) {
467
        return AbstractRuleSetFactoryTest.class.getClassLoader().getResourceAsStream(resource);
×
468
    }
469

470
    private void testRuleSet(String fileName) throws IOException, SAXException {
471

472
        // Load original XML
473
        // String xml1 =
474
        // readFullyToString(ResourceLoader.loadResourceAsStream(fileName));
475
        // System.out.println("xml1: " + xml1);
476

477
        // Load the original RuleSet
478
        RuleSet ruleSet1 = loadRuleSetByFileName(fileName);
×
479

480
        // Write to XML, first time
481
        ByteArrayOutputStream outputStream1 = new ByteArrayOutputStream();
×
482
        RuleSetWriter writer1 = new RuleSetWriter(outputStream1);
×
483
        writer1.write(ruleSet1);
×
484
        writer1.close();
×
485
        String xml2 = new String(outputStream1.toByteArray(), StandardCharsets.UTF_8);
×
486
        // System.out.println("xml2: " + xml2);
487

488
        // Read RuleSet from XML, first time
489
        RuleSetLoader loader = new RuleSetLoader();
×
490
        RuleSet ruleSet2 = loader.loadFromString("readRuleSet1.xml", xml2);
×
491

492
        // Do write/read a 2nd time, just to be sure
493

494
        // Write to XML, second time
495
        ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
×
496
        RuleSetWriter writer2 = new RuleSetWriter(outputStream2);
×
497
        writer2.write(ruleSet2);
×
498
        writer2.close();
×
499
        String xml3 = new String(outputStream2.toByteArray(), StandardCharsets.UTF_8);
×
500
        // System.out.println("xml3: " + xml3);
501

502
        // Read RuleSet from XML, second time
503
        RuleSet ruleSet3 = loader.loadFromString("readRuleSet2.xml", xml3);
×
504

505
        // The 2 written XMLs should all be valid w.r.t Schema/DTD
506
        assertTrue(validateAgainstSchema(new ByteArrayInputStream(xml2.getBytes(StandardCharsets.UTF_8))),
×
507
                "1st roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")");
508
        assertTrue(validateAgainstSchema(new ByteArrayInputStream(xml3.getBytes(StandardCharsets.UTF_8))),
×
509
                "2nd roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")");
510
        assertTrue(validateAgainstDtd(new ByteArrayInputStream(xml2.getBytes(StandardCharsets.UTF_8))),
×
511
                "1st roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")");
512
        assertTrue(validateAgainstDtd(new ByteArrayInputStream(xml3.getBytes(StandardCharsets.UTF_8))),
×
513
                "2nd roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")");
514

515
        // All 3 versions of the RuleSet should be the same
516
        assertEqualsRuleSet("Original RuleSet and 1st roundtrip Ruleset not the same (filename: " + fileName + ")",
×
517
                ruleSet1, ruleSet2);
518
        assertEqualsRuleSet("1st roundtrip Ruleset and 2nd roundtrip RuleSet not the same (filename: " + fileName + ")",
×
519
                ruleSet2, ruleSet3);
520

521
        // It's hard to compare the XML DOMs. At least the roundtrip ones should
522
        // textually be the same.
523
        assertEquals(xml2, xml3,
×
524
                "1st roundtrip RuleSet XML and 2nd roundtrip RuleSet XML (filename: " + fileName + ")");
525
    }
×
526

527
    private void assertEqualsRuleSet(String message, RuleSet ruleSet1, RuleSet ruleSet2) {
528
        assertEquals(ruleSet1.getName(), ruleSet2.getName(), message + ", RuleSet name");
×
529
        assertEquals(ruleSet1.getDescription(), ruleSet2.getDescription(), message + ", RuleSet description");
×
530
        assertEquals(ruleSet1.getFileExclusions(), ruleSet2.getFileExclusions(),
×
531
                message + ", RuleSet exclude patterns");
532
        assertEquals(ruleSet1.getFileInclusions(), ruleSet2.getFileInclusions(),
×
533
                message + ", RuleSet include patterns");
534
        assertEquals(ruleSet1.getRules().size(), ruleSet2.getRules().size(), message + ", RuleSet rule count");
×
535

536
        for (int i = 0; i < ruleSet1.getRules().size(); i++) {
×
537
            Rule rule1 = ((List<Rule>) ruleSet1.getRules()).get(i);
×
538
            Rule rule2 = ((List<Rule>) ruleSet2.getRules()).get(i);
×
539

540
            assertFalse(rule1 instanceof RuleReference != rule2 instanceof RuleReference,
×
541
                    message + ", Different RuleReference");
542

543
            if (rule1 instanceof RuleReference) {
×
544
                RuleReference ruleReference1 = (RuleReference) rule1;
×
545
                RuleReference ruleReference2 = (RuleReference) rule2;
×
546
                assertEquals(ruleReference1.getOverriddenMinimumLanguageVersion(),
×
547
                        ruleReference2.getOverriddenMinimumLanguageVersion(),
×
548
                        message + ", RuleReference overridden minimum language version");
549
                assertEquals(ruleReference1.getOverriddenMaximumLanguageVersion(),
×
550
                        ruleReference2.getOverriddenMaximumLanguageVersion(),
×
551
                        message + ", RuleReference overridden maximum language version");
552
                assertEquals(ruleReference1.isOverriddenDeprecated(), ruleReference2.isOverriddenDeprecated(),
×
553
                        message + ", RuleReference overridden deprecated");
554
                assertEquals(ruleReference1.getOverriddenName(), ruleReference2.getOverriddenName(),
×
555
                        message + ", RuleReference overridden name");
556
                assertEquals(ruleReference1.getOverriddenDescription(), ruleReference2.getOverriddenDescription(),
×
557
                        message + ", RuleReference overridden description");
558
                assertEquals(ruleReference1.getOverriddenMessage(), ruleReference2.getOverriddenMessage(),
×
559
                        message + ", RuleReference overridden message");
560
                assertEquals(ruleReference1.getOverriddenExternalInfoUrl(), ruleReference2.getOverriddenExternalInfoUrl(),
×
561
                        message + ", RuleReference overridden external info url");
562
                assertEquals(ruleReference1.getOverriddenPriority(), ruleReference2.getOverriddenPriority(),
×
563
                        message + ", RuleReference overridden priority");
564
                assertEquals(ruleReference1.getOverriddenExamples(), ruleReference2.getOverriddenExamples(),
×
565
                        message + ", RuleReference overridden examples");
566
            }
567

568
            assertEquals(rule1.getName(), rule2.getName(), message + ", Rule name");
×
569
            assertEquals(rule1.getRuleClass(), rule2.getRuleClass(), message + ", Rule class");
×
570
            assertEquals(rule1.getDescription(), rule2.getDescription(),
×
571
                    message + ", Rule description " + rule1.getName());
×
572
            assertEquals(rule1.getMessage(), rule2.getMessage(), message + ", Rule message");
×
573
            assertEquals(rule1.getExternalInfoUrl(), rule2.getExternalInfoUrl(), message + ", Rule external info url");
×
574
            assertEquals(rule1.getPriority(), rule2.getPriority(), message + ", Rule priority");
×
575
            assertEquals(rule1.getExamples(), rule2.getExamples(), message + ", Rule examples");
×
576

577
            List<PropertyDescriptor<?>> propertyDescriptors1 = rule1.getPropertyDescriptors();
×
578
            List<PropertyDescriptor<?>> propertyDescriptors2 = rule2.getPropertyDescriptors();
×
579
            assertEquals(propertyDescriptors1, propertyDescriptors2, message + ", Rule property descriptor ");
×
580
            for (int j = 0; j < propertyDescriptors1.size(); j++) {
×
581
                Object value1 = rule1.getProperty(propertyDescriptors1.get(j));
×
582
                Object value2 = rule2.getProperty(propertyDescriptors2.get(j));
×
583
                // special case for Pattern, there is no equals method
584
                if (value1 instanceof Pattern && value2 instanceof Pattern) {
×
585
                    value1 = ((Pattern) value1).pattern();
×
586
                    value2 = ((Pattern) value2).pattern();
×
587
                }
588
                assertEquals(value1, value2, message + ", Rule " + rule1.getName() + " property "
×
589
                    + propertyDescriptors1.get(j).name());
×
590
            }
591
            assertEquals(propertyDescriptors1.size(), propertyDescriptors2.size(),
×
592
                    message + ", Rule property descriptor count");
593
        }
594
    }
×
595

596
    /**
597
     * Validator for the SAX parser
598
     */
599
    private static class ValidateDefaultHandler extends DefaultHandler {
600
        private boolean valid = true;
×
601
        private final Map<String, String> schemaMapping;
602

603
        ValidateDefaultHandler() {
×
604
            schemaMapping = new HashMap<>();
×
605
            schemaMapping.put("https://pmd.sourceforge.io/ruleset_2_0_0.xsd", "ruleset_2_0_0.xsd");
×
606
            schemaMapping.put("https://pmd.sourceforge.io/ruleset_2_0_0.dtd", "ruleset_2_0_0.dtd");
×
607
        }
×
608

609
        public ValidateDefaultHandler resetValid() {
610
            valid = true;
×
611
            return this;
×
612
        }
613

614
        public boolean isValid() {
615
            return valid;
×
616
        }
617

618
        @Override
619
        public void error(SAXParseException e) {
620
            log("Error", e);
×
621
        }
×
622

623
        @Override
624
        public void fatalError(SAXParseException e) {
625
            log("FatalError", e);
×
626
        }
×
627

628
        @Override
629
        public void warning(SAXParseException e) {
630
            log("Warning", e);
×
631
        }
×
632

633
        private void log(String prefix, SAXParseException e) {
634
            String message = prefix + " at (" + e.getLineNumber() + ", " + e.getColumnNumber() + "): " + e.getMessage();
×
635
            System.err.println(message);
×
636
            valid = false;
×
637
        }
×
638

639
        @Override
640
        public InputSource resolveEntity(String publicId, String systemId) throws IOException {
641
            String resource = schemaMapping.get(systemId);
×
642

643
            if (resource != null) {
×
644
                InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource);
×
645
                if (inputStream == null) {
×
646
                    throw new FileNotFoundException(resource);
×
647
                }
648
                return new InputSource(inputStream);
×
649
            }
650
            throw new IllegalArgumentException(
×
651
                    "No clue how to handle: publicId=" + publicId + ", systemId=" + systemId);
652
        }
653
    }
654

655
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc