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

pmd / pmd / 23

30 May 2025 04:25PM UTC coverage: 78.377% (-0.2%) from 78.601%
23

push

github

adangel
[core] Add rule to report unnecessary suppression comments/annotations (#5609)

Merge pull request #5609 from oowekyala:new-rule-UnnecessarySuppression

17712 of 23434 branches covered (75.58%)

Branch coverage included in aggregate %.

159 of 328 new or added lines in 22 files covered. (48.48%)

36 existing lines in 4 files now uncovered.

38902 of 48799 relevant lines covered (79.72%)

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.emptyString;
9
import static org.junit.jupiter.api.Assertions.assertEquals;
10
import static org.junit.jupiter.api.Assertions.assertFalse;
11
import static org.junit.jupiter.api.Assertions.assertTrue;
12
import static org.junit.jupiter.api.Assertions.fail;
13

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

37
import org.junit.jupiter.api.BeforeAll;
38
import org.junit.jupiter.api.Test;
39
import org.slf4j.event.Level;
40
import org.xml.sax.InputSource;
41
import org.xml.sax.SAXException;
42
import org.xml.sax.SAXParseException;
43
import org.xml.sax.helpers.DefaultHandler;
44

45
import net.sourceforge.pmd.internal.util.IOUtil;
46
import net.sourceforge.pmd.lang.Language;
47
import net.sourceforge.pmd.lang.LanguageRegistry;
48
import net.sourceforge.pmd.lang.rule.InternalApiBridge;
49
import net.sourceforge.pmd.lang.rule.Rule;
50
import net.sourceforge.pmd.lang.rule.RuleReference;
51
import net.sourceforge.pmd.lang.rule.RuleSet;
52
import net.sourceforge.pmd.lang.rule.RuleSetLoader;
53
import net.sourceforge.pmd.lang.rule.RuleSetWriter;
54
import net.sourceforge.pmd.lang.rule.impl.UnnecessaryPmdSuppressionRule;
55
import net.sourceforge.pmd.lang.rule.xpath.XPathRule;
56
import net.sourceforge.pmd.properties.PropertyDescriptor;
57
import net.sourceforge.pmd.util.log.internal.MessageReporterBase;
58

59
/**
60
 * Base test class to verify the language's rulesets. This class should be
61
 * subclassed for each language.
62
 */
63
public abstract class AbstractRuleSetFactoryTest {
64

65
    private static ValidateDefaultHandler validateDefaultHandler;
66
    private static SAXParser saxParser;
67

68
    // todo rename this field to validCoreRules or something. Make private.
69
    protected Set<String> validXPathClassNames = new HashSet<>();
×
70
    private final Set<String> languagesToSkip = new HashSet<>();
×
71

72
    public AbstractRuleSetFactoryTest() {
73
        this(new String[0]);
×
74
    }
×
75

76
    /**
77
     * Constructor used when a module that depends on another module wants to filter out the dependee's rulesets.
78
     *
79
     * @param languagesToSkip {@link Language}s terse names that appear in the classpath via a dependency, but should be
80
     * skipped because they aren't the primary language which the concrete instance of this class is testing.
81
     */
82
    public AbstractRuleSetFactoryTest(String... languagesToSkip) {
×
83
        this.languagesToSkip.add("dummy");
×
84
        this.languagesToSkip.addAll(Arrays.asList(languagesToSkip));
×
85
        validXPathClassNames.add(XPathRule.class.getName());
×
NEW
86
        validXPathClassNames.add(UnnecessaryPmdSuppressionRule.class.getName());
×
UNCOV
87
    }
×
88

89
    public AbstractRuleSetFactoryTest(Language... languagesToSkip) {
90
        this(Arrays.stream(languagesToSkip).map(Language::getId).toArray(String[]::new));
×
91
    }
×
92

93

94
    /**
95
     * Setups the XML parser with validation.
96
     *
97
     * @throws Exception
98
     *             any error
99
     */
100
    @BeforeAll
101
    static void init() throws Exception {
102
        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
×
103
        saxParserFactory.setValidating(true);
×
104
        saxParserFactory.setNamespaceAware(true);
×
105

106
        // Hope we're using Xerces, or this may not work!
107
        // Note: Features are listed here
108
        // http://xerces.apache.org/xerces2-j/features.html
109
        saxParserFactory.setFeature("http://xml.org/sax/features/validation", true);
×
110
        saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema", true);
×
111
        saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true);
×
112

113
        validateDefaultHandler = new ValidateDefaultHandler();
×
114

115
        saxParser = saxParserFactory.newSAXParser();
×
116
    }
×
117

118
    /**
119
     * Checks all rulesets of all languages on the classpath and verifies that
120
     * all required attributes for all rules are specified.
121
     *
122
     * @throws Exception
123
     *             any error
124
     */
125
    @Test
126
    void testAllPMDBuiltInRulesMeetConventions() throws Exception {
127
        int invalidSinceAttributes = 0;
×
128
        int invalidExternalInfoURL = 0;
×
129
        int invalidClassName = 0;
×
130
        int invalidRegexSuppress = 0;
×
131
        int invalidXPathSuppress = 0;
×
132
        StringBuilder messages = new StringBuilder();
×
133
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
134
        for (String fileName : ruleSetFileNames) {
×
135
            RuleSet ruleSet = loadRuleSetByFileName(fileName);
×
136
            for (Rule rule : ruleSet.getRules()) {
×
137

138
                // Skip references
139
                if (rule instanceof RuleReference) {
×
140
                    continue;
×
141
                }
142

143
                Language language = rule.getLanguage();
×
144
                String group = fileName.substring(fileName.lastIndexOf('/') + 1);
×
145
                group = group.substring(0, group.indexOf(".xml"));
×
146
                if (group.indexOf('-') >= 0) {
×
147
                    group = group.substring(0, group.indexOf('-'));
×
148
                }
149

150
                // Is since missing ?
151
                if (rule.getSince() == null) {
×
152
                    invalidSinceAttributes++;
×
153
                    messages.append("Rule ")
×
154
                            .append(fileName)
×
155
                            .append("/")
×
156
                            .append(rule.getName())
×
157
                            .append(" is missing 'since' attribute\n");
×
158
                }
159
                // Is URL valid ?
160
                if (rule.getExternalInfoUrl() == null || "".equalsIgnoreCase(rule.getExternalInfoUrl())) {
×
161
                    invalidExternalInfoURL++;
×
162
                    messages.append("Rule ")
×
163
                            .append(fileName)
×
164
                            .append("/")
×
165
                            .append(rule.getName())
×
166
                            .append(" is missing 'externalInfoURL' attribute\n");
×
167
                } else {
168
                    String expectedExternalInfoURL = "https://docs.pmd-code.org/.+/pmd_rules_"
×
169
                            + language.getId() + "_"
×
170
                            + IOUtil.getFilenameBase(fileName)
×
171
                            + ".html#"
172
                            + rule.getName().toLowerCase(Locale.ROOT);
×
173
                    if (rule.getExternalInfoUrl() == null
×
174
                            || !rule.getExternalInfoUrl().matches(expectedExternalInfoURL)) {
×
175
                        invalidExternalInfoURL++;
×
176
                        messages.append("Rule ")
×
177
                                .append(fileName)
×
178
                                .append("/")
×
179
                                .append(rule.getName())
×
180
                                .append(" seems to have an invalid 'externalInfoURL' value (")
×
181
                                .append(rule.getExternalInfoUrl())
×
182
                                .append("), it should be:")
×
183
                                .append(expectedExternalInfoURL)
×
184
                                .append('\n');
×
185
                    }
186
                }
187
                // Proper class name/packaging?
188
                String expectedClassName = "net.sourceforge.pmd.lang." + language.getId() + ".rule." + group
×
189
                        + "." + rule.getName() + "Rule";
×
190
                if (!rule.getRuleClass().equals(expectedClassName)
×
191
                        && !validXPathClassNames.contains(rule.getRuleClass())) {
×
192
                    invalidClassName++;
×
193
                    messages.append("Rule ")
×
194
                            .append(fileName)
×
195
                            .append("/")
×
196
                            .append(rule.getName())
×
197
                            .append(" seems to have an invalid 'class' value (")
×
198
                            .append(rule.getRuleClass())
×
199
                            .append("), it should be:")
×
200
                            .append(expectedClassName)
×
201
                            .append('\n');
×
202
                }
203
                // Should not have violation suppress regex property
204
                if (rule.getProperty(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR).isPresent()) {
×
205
                    invalidRegexSuppress++;
×
206
                    messages.append("Rule ")
×
207
                            .append(fileName)
×
208
                            .append("/")
×
209
                            .append(rule.getName())
×
210
                            .append(" should not have '")
×
211
                            .append(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name())
×
212
                            .append("', this is intended for end user customization only.\n");
×
213
                }
214
                // Should not have violation suppress xpath property
215
                if (rule.getProperty(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR).isPresent()) {
×
216
                    invalidXPathSuppress++;
×
217
                    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());
×
218
                }
219
            }
×
220
        }
×
221
        // We do this at the end to ensure we test ALL the rules before failing
222
        // the test
223
        if (invalidSinceAttributes > 0 || invalidExternalInfoURL > 0 || invalidClassName > 0 || invalidRegexSuppress > 0
×
224
                || invalidXPathSuppress > 0) {
225
            fail("All built-in PMD rules need 'since' attribute (" + invalidSinceAttributes
×
226
                    + " are missing), a proper ExternalURLInfo (" + invalidExternalInfoURL
227
                    + " are invalid), a class name meeting conventions (" + invalidClassName + " are invalid), no '"
228
                    + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name() + "' property (" + invalidRegexSuppress
×
229
                    + " are invalid), and no '" + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name() + "' property ("
×
230
                    + invalidXPathSuppress + " are invalid)\n" + messages);
231
        }
232
    }
×
233

234
    /**
235
     * Verifies that all rulesets are valid XML according to the xsd schema.
236
     *
237
     * @throws Exception
238
     *             any error
239
     */
240
    @Test
241
    void testXmlSchema() throws Exception {
242
        boolean allValid = true;
×
243
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
244
        for (String fileName : ruleSetFileNames) {
×
245
            boolean valid = validateAgainstSchema(fileName);
×
246
            allValid = allValid && valid;
×
247
        }
×
248
        assertTrue(allValid, "All XML must parse without producing validation messages.");
×
249
    }
×
250

251
    @Test
252
    void verifyCorrectXmlEncoding() throws Exception {
253
        boolean allValid = true;
×
254
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
255
        StringBuilder messages = new StringBuilder();
×
256
        for (String fileName : ruleSetFileNames) {
×
257
            boolean valid = hasCorrectEncoding(fileName);
×
258
            allValid = allValid && valid;
×
259
            if (!valid) {
×
260
                messages.append("RuleSet ")
×
261
                        .append(fileName)
×
262
                        .append(" is missing XML encoding or not using UTF8\n");
×
263
            }
264
        }
×
265
        assertTrue(allValid, "All XML must use correct XML encoding\n" + messages);
×
266
    }
×
267

268
    public static boolean hasCorrectEncoding(String fileName) throws IOException {
269
        try (InputStream inputStream = loadResourceAsStream(fileName)) {
×
270
            // first bytes must be:
271
            byte[] expectedBytes = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>".getBytes(StandardCharsets.UTF_8);
×
272
            byte[] bytes = new byte[expectedBytes.length];
×
273
            int count = inputStream.read(bytes);
×
274
            if (count != expectedBytes.length || !Arrays.equals(expectedBytes, bytes)) {
×
275
                return false;
×
276
            }
277
        }
×
278
        return true;
×
279
    }
280

281
    /**
282
     * Verifies that all rulesets are valid XML according to the DTD.
283
     *
284
     * @throws Exception
285
     *             any error
286
     */
287
    @Test
288
    void testDtd() throws Exception {
289
        boolean allValid = true;
×
290
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
291
        for (String fileName : ruleSetFileNames) {
×
292
            boolean valid = validateAgainstDtd(fileName);
×
293
            allValid = allValid && valid;
×
294
        }
×
295
        assertTrue(allValid, "All XML must parse without producing validation messages.");
×
296
    }
×
297

298
    /**
299
     * Reads and writes the rulesets to make sure, that no data is lost if the
300
     * rulests are processed.
301
     *
302
     * @throws Exception
303
     *             any error
304
     */
305
    @Test
306
    void testReadWriteRoundTrip() throws Exception {
307

308
        List<String> ruleSetFileNames = getRuleSetFileNames();
×
309
        for (String fileName : ruleSetFileNames) {
×
310
            testRuleSet(fileName);
×
311
        }
×
312

313
    }
×
314

315
    // Gets all test PMD Ruleset XML files
316
    private List<String> getRuleSetFileNames() throws IOException {
317
        List<String> result = new ArrayList<>();
×
318

319
        for (Language language : LanguageRegistry.PMD.getLanguages()) {
×
320
            if (this.languagesToSkip.contains(language.getId())) {
×
321
                continue;
×
322
            }
323
            result.addAll(getRuleSetFileNames(language.getId()));
×
324
        }
×
325

326
        return result;
×
327
    }
328

329
    private List<String> getRuleSetFileNames(String language) throws IOException {
330
        List<String> ruleSetFileNames = new ArrayList<>();
×
331
        ruleSetFileNames.addAll(getRuleSetFileNames(language, "rulesets/" + language + "/rulesets.properties"));
×
332
        ruleSetFileNames.addAll(getRuleSetFileNames(language, "category/" + language + "/categories.properties"));
×
333
        return ruleSetFileNames;
×
334
    }
335

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

358
    private RuleSet loadRuleSetByFileName(String ruleSetFileName) {
359
        final StringBuilder messages = new StringBuilder();
×
360
        class Reporter extends MessageReporterBase {
×
361
            @Override
362
            protected void logImpl(Level level, String message) {
363
                messages.append(message).append(System.lineSeparator());
×
364
            }
×
365
        }
366

367
        RuleSet ruleSet = InternalApiBridge.withReporter(new RuleSetLoader(), new Reporter())
×
368
                .loadFromResource(ruleSetFileName);
×
369

370
        assertThat("There should be no warnings while loading the ruleset",
×
371
                messages.toString(), emptyString());
×
372

373
        return ruleSet;
×
374
    }
375

376
    private boolean validateAgainstSchema(String fileName) throws IOException, SAXException {
377
        try (InputStream inputStream = loadResourceAsStream(fileName)) {
×
378
            boolean valid = validateAgainstSchema(inputStream);
×
379
            if (!valid) {
×
380
                System.err.println("Validation against XML Schema failed for: " + fileName);
×
381
            }
382
            return valid;
×
383
        }
384
    }
385

386
    private boolean validateAgainstSchema(InputStream inputStream) throws IOException, SAXException {
387

388
        saxParser.parse(inputStream, validateDefaultHandler.resetValid());
×
389
        inputStream.close();
×
390
        return validateDefaultHandler.isValid();
×
391
    }
392

393
    private boolean validateAgainstDtd(String fileName) throws IOException, SAXException {
394
        try (InputStream inputStream = loadResourceAsStream(fileName)) {
×
395
            boolean valid = validateAgainstDtd(inputStream);
×
396
            if (!valid) {
×
397
                System.err.println("Validation against DTD failed for: " + fileName);
×
398
            }
399
            return valid;
×
400
        }
401
    }
402

403
    private boolean validateAgainstDtd(InputStream inputStream) throws IOException, SAXException {
404

405
        // Read file into memory
406
        String file = readFullyToString(inputStream);
×
407
        inputStream.close();
×
408

409
        String rulesetNamespace = RuleSetWriter.RULESET_2_0_0_NS_URI;
×
410

411
        // Remove XML Schema stuff, replace with DTD
412
        file = file.replaceAll("<\\?xml [ a-zA-Z0-9=\".-]*\\?>", "");
×
413
        file = file.replaceAll("xmlns=\"" + rulesetNamespace + "\"", "");
×
414
        file = file.replaceAll("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", "");
×
415
        file = file.replaceAll("xsi:schemaLocation=\"" + rulesetNamespace
×
416
                + " https://pmd.sourceforge.io/ruleset_\\d_0_0.xsd\"", "");
417

418
        if (RuleSetWriter.RULESET_2_0_0_NS_URI.equals(rulesetNamespace)) {
×
419
            file = "<?xml version=\"1.0\"?>" + System.lineSeparator()
×
420
                + "<!DOCTYPE ruleset SYSTEM \"https://pmd.sourceforge.io/ruleset_2_0_0.dtd\">" + System.lineSeparator()
×
421
                + file;
422
        } else {
423
            file = "<?xml version=\"1.0\"?>" + System.lineSeparator()
×
424
                + "<!DOCTYPE ruleset>" + System.lineSeparator()
×
425
                + file;
426
        }
427

428
        try (InputStream modifiedStream = new ByteArrayInputStream(file.getBytes())) {
×
429
            saxParser.parse(modifiedStream, validateDefaultHandler.resetValid());
×
430
        }
431
        return validateDefaultHandler.isValid();
×
432
    }
433

434
    private String readFullyToString(InputStream inputStream) throws IOException {
435
        StringBuilder buf = new StringBuilder(64 * 1024);
×
436
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
×
437
            String line;
438
            while ((line = reader.readLine()) != null) {
×
439
                buf.append(line);
×
440
                buf.append(System.lineSeparator());
×
441
            }
442
            return buf.toString();
×
443
        }
444
    }
445

446
    private static InputStream loadResourceAsStream(String resource) {
447
        return AbstractRuleSetFactoryTest.class.getClassLoader().getResourceAsStream(resource);
×
448
    }
449

450
    private void testRuleSet(String fileName) throws IOException, SAXException {
451

452
        // Load original XML
453
        // String xml1 =
454
        // readFullyToString(ResourceLoader.loadResourceAsStream(fileName));
455
        // System.out.println("xml1: " + xml1);
456

457
        // Load the original RuleSet
458
        RuleSet ruleSet1 = loadRuleSetByFileName(fileName);
×
459

460
        // Write to XML, first time
461
        ByteArrayOutputStream outputStream1 = new ByteArrayOutputStream();
×
462
        RuleSetWriter writer1 = new RuleSetWriter(outputStream1);
×
463
        writer1.write(ruleSet1);
×
464
        writer1.close();
×
465
        String xml2 = new String(outputStream1.toByteArray());
×
466
        // System.out.println("xml2: " + xml2);
467

468
        // Read RuleSet from XML, first time
469
        RuleSetLoader loader = new RuleSetLoader();
×
470
        RuleSet ruleSet2 = loader.loadFromString("readRuleSet1.xml", xml2);
×
471

472
        // Do write/read a 2nd time, just to be sure
473

474
        // Write to XML, second time
475
        ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
×
476
        RuleSetWriter writer2 = new RuleSetWriter(outputStream2);
×
477
        writer2.write(ruleSet2);
×
478
        writer2.close();
×
479
        String xml3 = new String(outputStream2.toByteArray());
×
480
        // System.out.println("xml3: " + xml3);
481

482
        // Read RuleSet from XML, second time
483
        RuleSet ruleSet3 = loader.loadFromString("readRuleSet2.xml", xml3);
×
484

485
        // The 2 written XMLs should all be valid w.r.t Schema/DTD
486
        assertTrue(validateAgainstSchema(new ByteArrayInputStream(xml2.getBytes())),
×
487
                "1st roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")");
488
        assertTrue(validateAgainstSchema(new ByteArrayInputStream(xml3.getBytes())),
×
489
                "2nd roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")");
490
        assertTrue(validateAgainstDtd(new ByteArrayInputStream(xml2.getBytes())),
×
491
                "1st roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")");
492
        assertTrue(validateAgainstDtd(new ByteArrayInputStream(xml3.getBytes())),
×
493
                "2nd roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")");
494

495
        // All 3 versions of the RuleSet should be the same
496
        assertEqualsRuleSet("Original RuleSet and 1st roundtrip Ruleset not the same (filename: " + fileName + ")",
×
497
                ruleSet1, ruleSet2);
498
        assertEqualsRuleSet("1st roundtrip Ruleset and 2nd roundtrip RuleSet not the same (filename: " + fileName + ")",
×
499
                ruleSet2, ruleSet3);
500

501
        // It's hard to compare the XML DOMs. At least the roundtrip ones should
502
        // textually be the same.
503
        assertEquals(xml2, xml3,
×
504
                "1st roundtrip RuleSet XML and 2nd roundtrip RuleSet XML (filename: " + fileName + ")");
505
    }
×
506

507
    private void assertEqualsRuleSet(String message, RuleSet ruleSet1, RuleSet ruleSet2) {
508
        assertEquals(ruleSet1.getName(), ruleSet2.getName(), message + ", RuleSet name");
×
509
        assertEquals(ruleSet1.getDescription(), ruleSet2.getDescription(), message + ", RuleSet description");
×
510
        assertEquals(ruleSet1.getFileExclusions(), ruleSet2.getFileExclusions(),
×
511
                message + ", RuleSet exclude patterns");
512
        assertEquals(ruleSet1.getFileInclusions(), ruleSet2.getFileInclusions(),
×
513
                message + ", RuleSet include patterns");
514
        assertEquals(ruleSet1.getRules().size(), ruleSet2.getRules().size(), message + ", RuleSet rule count");
×
515

516
        for (int i = 0; i < ruleSet1.getRules().size(); i++) {
×
517
            Rule rule1 = ((List<Rule>) ruleSet1.getRules()).get(i);
×
518
            Rule rule2 = ((List<Rule>) ruleSet2.getRules()).get(i);
×
519

520
            assertFalse(rule1 instanceof RuleReference != rule2 instanceof RuleReference,
×
521
                    message + ", Different RuleReference");
522

523
            if (rule1 instanceof RuleReference) {
×
524
                RuleReference ruleReference1 = (RuleReference) rule1;
×
525
                RuleReference ruleReference2 = (RuleReference) rule2;
×
526
                assertEquals(ruleReference1.getOverriddenMinimumLanguageVersion(),
×
527
                        ruleReference2.getOverriddenMinimumLanguageVersion(),
×
528
                        message + ", RuleReference overridden minimum language version");
529
                assertEquals(ruleReference1.getOverriddenMaximumLanguageVersion(),
×
530
                        ruleReference2.getOverriddenMaximumLanguageVersion(),
×
531
                        message + ", RuleReference overridden maximum language version");
532
                assertEquals(ruleReference1.isOverriddenDeprecated(), ruleReference2.isOverriddenDeprecated(),
×
533
                        message + ", RuleReference overridden deprecated");
534
                assertEquals(ruleReference1.getOverriddenName(), ruleReference2.getOverriddenName(),
×
535
                        message + ", RuleReference overridden name");
536
                assertEquals(ruleReference1.getOverriddenDescription(), ruleReference2.getOverriddenDescription(),
×
537
                        message + ", RuleReference overridden description");
538
                assertEquals(ruleReference1.getOverriddenMessage(), ruleReference2.getOverriddenMessage(),
×
539
                        message + ", RuleReference overridden message");
540
                assertEquals(ruleReference1.getOverriddenExternalInfoUrl(), ruleReference2.getOverriddenExternalInfoUrl(),
×
541
                        message + ", RuleReference overridden external info url");
542
                assertEquals(ruleReference1.getOverriddenPriority(), ruleReference2.getOverriddenPriority(),
×
543
                        message + ", RuleReference overridden priority");
544
                assertEquals(ruleReference1.getOverriddenExamples(), ruleReference2.getOverriddenExamples(),
×
545
                        message + ", RuleReference overridden examples");
546
            }
547

548
            assertEquals(rule1.getName(), rule2.getName(), message + ", Rule name");
×
549
            assertEquals(rule1.getRuleClass(), rule2.getRuleClass(), message + ", Rule class");
×
550
            assertEquals(rule1.getDescription(), rule2.getDescription(),
×
551
                    message + ", Rule description " + rule1.getName());
×
552
            assertEquals(rule1.getMessage(), rule2.getMessage(), message + ", Rule message");
×
553
            assertEquals(rule1.getExternalInfoUrl(), rule2.getExternalInfoUrl(), message + ", Rule external info url");
×
554
            assertEquals(rule1.getPriority(), rule2.getPriority(), message + ", Rule priority");
×
555
            assertEquals(rule1.getExamples(), rule2.getExamples(), message + ", Rule examples");
×
556

557
            List<PropertyDescriptor<?>> propertyDescriptors1 = rule1.getPropertyDescriptors();
×
558
            List<PropertyDescriptor<?>> propertyDescriptors2 = rule2.getPropertyDescriptors();
×
559
            assertEquals(propertyDescriptors1, propertyDescriptors2, message + ", Rule property descriptor ");
×
560
            for (int j = 0; j < propertyDescriptors1.size(); j++) {
×
561
                Object value1 = rule1.getProperty(propertyDescriptors1.get(j));
×
562
                Object value2 = rule2.getProperty(propertyDescriptors2.get(j));
×
563
                // special case for Pattern, there is no equals method
564
                if (value1 instanceof Pattern && value2 instanceof Pattern) {
×
565
                    value1 = ((Pattern) value1).pattern();
×
566
                    value2 = ((Pattern) value2).pattern();
×
567
                }
568
                assertEquals(value1, value2, message + ", Rule " + rule1.getName() + " property "
×
569
                    + propertyDescriptors1.get(j).name());
×
570
            }
571
            assertEquals(propertyDescriptors1.size(), propertyDescriptors2.size(),
×
572
                    message + ", Rule property descriptor count");
573
        }
574
    }
×
575

576
    /**
577
     * Validator for the SAX parser
578
     */
579
    private static class ValidateDefaultHandler extends DefaultHandler {
580
        private boolean valid = true;
×
581
        private final Map<String, String> schemaMapping;
582

583
        ValidateDefaultHandler() {
×
584
            schemaMapping = new HashMap<>();
×
585
            schemaMapping.put("https://pmd.sourceforge.io/ruleset_2_0_0.xsd", "ruleset_2_0_0.xsd");
×
586
            schemaMapping.put("https://pmd.sourceforge.io/ruleset_2_0_0.dtd", "ruleset_2_0_0.dtd");
×
587
        }
×
588

589
        public ValidateDefaultHandler resetValid() {
590
            valid = true;
×
591
            return this;
×
592
        }
593

594
        public boolean isValid() {
595
            return valid;
×
596
        }
597

598
        @Override
599
        public void error(SAXParseException e) {
600
            log("Error", e);
×
601
        }
×
602

603
        @Override
604
        public void fatalError(SAXParseException e) {
605
            log("FatalError", e);
×
606
        }
×
607

608
        @Override
609
        public void warning(SAXParseException e) {
610
            log("Warning", e);
×
611
        }
×
612

613
        private void log(String prefix, SAXParseException e) {
614
            String message = prefix + " at (" + e.getLineNumber() + ", " + e.getColumnNumber() + "): " + e.getMessage();
×
615
            System.err.println(message);
×
616
            valid = false;
×
617
        }
×
618

619
        @Override
620
        public InputSource resolveEntity(String publicId, String systemId) throws IOException {
621
            String resource = schemaMapping.get(systemId);
×
622

623
            if (resource != null) {
×
624
                InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource);
×
625
                if (inputStream == null) {
×
626
                    throw new FileNotFoundException(resource);
×
627
                }
628
                return new InputSource(inputStream);
×
629
            }
630
            throw new IllegalArgumentException(
×
631
                    "No clue how to handle: publicId=" + publicId + ", systemId=" + systemId);
632
        }
633
    }
634

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

© 2026 Coveralls, Inc