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

pmd / pmd / 217

24 Oct 2025 12:40PM UTC coverage: 78.683% (+0.02%) from 78.668%
217

push

github

web-flow
[doc] Search improvements (#6073)

18268 of 24067 branches covered (75.9%)

Branch coverage included in aggregate %.

53 of 54 new or added lines in 1 file covered. (98.15%)

39804 of 49738 relevant lines covered (80.03%)

0.81 hits per line

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

86.43
/pmd-doc/src/main/java/net/sourceforge/pmd/doc/internal/RuleDocGenerator.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.doc.internal;
6

7
import java.io.File;
8
import java.io.IOException;
9
import java.nio.file.FileVisitResult;
10
import java.nio.file.Files;
11
import java.nio.file.Path;
12
import java.nio.file.SimpleFileVisitor;
13
import java.nio.file.attribute.BasicFileAttributes;
14
import java.util.ArrayList;
15
import java.util.Arrays;
16
import java.util.Collection;
17
import java.util.Collections;
18
import java.util.Comparator;
19
import java.util.HashMap;
20
import java.util.LinkedList;
21
import java.util.List;
22
import java.util.Locale;
23
import java.util.Map;
24
import java.util.Objects;
25
import java.util.SortedMap;
26
import java.util.TreeMap;
27
import java.util.regex.Matcher;
28
import java.util.stream.Collectors;
29

30
import org.apache.commons.lang3.StringUtils;
31
import org.apache.commons.text.StringEscapeUtils;
32
import org.slf4j.Logger;
33
import org.slf4j.LoggerFactory;
34

35
import net.sourceforge.pmd.internal.util.IOUtil;
36
import net.sourceforge.pmd.lang.Language;
37
import net.sourceforge.pmd.lang.LanguageRegistry;
38
import net.sourceforge.pmd.lang.rule.Rule;
39
import net.sourceforge.pmd.lang.rule.RuleReference;
40
import net.sourceforge.pmd.lang.rule.RuleSet;
41
import net.sourceforge.pmd.lang.rule.RuleSetLoadException;
42
import net.sourceforge.pmd.lang.rule.RuleSetLoader;
43
import net.sourceforge.pmd.lang.rule.xpath.XPathRule;
44
import net.sourceforge.pmd.properties.PropertyDescriptor;
45

46
public class RuleDocGenerator {
47
    private static final Logger LOG = LoggerFactory.getLogger(RuleDocGenerator.class);
1✔
48

49
    private static final String GENERATED_WARNING = "<!-- DO NOT EDIT THIS FILE. This file is generated from file ${source}. -->";
50
    private static final String GENERATED_WARNING_NO_SOURCE = "<!-- DO NOT EDIT THIS FILE. This file is generated. -->";
51

52
    private static final String LANGUAGE_INDEX_FILENAME_PATTERN = "docs/pages/pmd/rules/${language.tersename}.md";
53
    private static final String LANGUAGE_INDEX_PERMALINK_PATTERN = "pmd_rules_${language.tersename}.html";
54
    private static final String RULESET_INDEX_FILENAME_PATTERN = "docs/pages/pmd/rules/${language.tersename}/${ruleset.name}.md";
55
    private static final String RULESET_INDEX_PERMALINK_PATTERN = "pmd_rules_${language.tersename}_${ruleset.name}.html";
56

57
    private static final String DEPRECATION_LABEL_SMALL = "<span style=\"border-radius: 0.25em; color: #fff; padding: 0.2em 0.6em 0.3em; display: inline; background-color: #d9534f; font-size: 75%;\">Deprecated</span> ";
58
    private static final String DEPRECATION_LABEL = "<span style=\"border-radius: 0.25em; color: #fff; padding: 0.2em 0.6em 0.3em; display: inline; background-color: #d9534f;\">Deprecated</span>";
59
    private static final String DEPRECATED_RULE_PROPERTY_MARKER = "deprecated!";
60

61
    private static final String GITHUB_SOURCE_LINK = "https://github.com/pmd/pmd/blob/main/";
62

63
    /** Maintains mapping from pmd terse language name to rouge highlighter language. */
64
    private static final Map<String, String> LANGUAGE_HIGHLIGHT_MAPPER = new HashMap<>();
1✔
65

66
    static {
67
        LANGUAGE_HIGHLIGHT_MAPPER.put("ecmascript", "javascript");
1✔
68
        LANGUAGE_HIGHLIGHT_MAPPER.put("pom", "xml");
1✔
69
        LANGUAGE_HIGHLIGHT_MAPPER.put("apex", "java");
1✔
70
        LANGUAGE_HIGHLIGHT_MAPPER.put("plsql", "sql");
1✔
71
    }
1✔
72

73
    private final Path root;
74
    private final FileWriter writer;
75

76
    /** Caches rule class name to java source file mapping. */
77
    private final Map<String, String> allRules = new HashMap<>();
1✔
78
    /** Caches ruleset to ruleset xml file mapping. */
79
    private final Map<String, String> allRulesets = new HashMap<>();
1✔
80

81

82
    public RuleDocGenerator(FileWriter writer, Path root) {
1✔
83
        this.writer = Objects.requireNonNull(writer, "A file writer must be provided");
1✔
84
        this.root = Objects.requireNonNull(root, "Root directory must be provided");
1✔
85

86
        Path docsDir = root.resolve("docs");
1✔
87
        if (!Files.exists(docsDir) || !Files.isDirectory(docsDir)) {
1!
88
            throw new IllegalArgumentException("Couldn't find \"docs\" subdirectory");
×
89
        }
90
    }
1✔
91

92
    public void generate(List<RuleSet> registeredRulesets, List<String> additionalRulesets) throws IOException {
93
        removeExistingRuleDocs();
1✔
94
        Map<Language, List<RuleSet>> sortedRulesets;
95
        Map<Language, List<RuleSet>> sortedAdditionalRulesets;
96
        sortedRulesets = sortRulesets(registeredRulesets);
1✔
97
        sortedAdditionalRulesets = sortRulesets(resolveAdditionalRulesets(additionalRulesets));
1✔
98
        determineRuleClassSourceFiles(sortedRulesets);
1✔
99
        generateLanguageIndex(sortedRulesets, sortedAdditionalRulesets);
1✔
100
        generateRuleSetIndex(sortedRulesets);
1✔
101

102
        ensureAllLanguages(sortedRulesets);
1✔
103
        generateSidebar(sortedRulesets);
1✔
104
    }
1✔
105

106
    private void removeExistingRuleDocs() throws IOException {
107
        Path directory = root.resolve("docs/pages/pmd/rules");
1✔
108
        if (!Files.isDirectory(directory)) {
1!
109
            // no old files exist yet
110
            return;
1✔
111
        }
112

113
        System.out.println("Deleting old rule docs in " + directory);
×
114
        Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
×
115
            @Override
116
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
117
                if (file.toString().endsWith("scala.md")) {
×
118
                    // don't delete scala.md, since we don't have any rules yet...
119
                    return FileVisitResult.CONTINUE;
×
120
                }
121
                Files.delete(file);
×
122
                return FileVisitResult.CONTINUE;
×
123
            }
124

125
            @Override
126
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
127
                if (dir.equals(directory)) {
×
128
                    // don't delete the whole directory, keep it empty
129
                    // or almost empty (scala.md is still present)
130
                    return FileVisitResult.CONTINUE;
×
131
                }
132
                Files.delete(dir);
×
133
                return FileVisitResult.CONTINUE;
×
134
            }
135
        });
136
    }
×
137

138
    private void ensureAllLanguages(Map<Language, List<RuleSet>> sortedRulesets) {
139
        for (Language language : LanguageRegistry.PMD.getLanguages()) {
1✔
140
            sortedRulesets.putIfAbsent(language, Collections.emptyList());
1✔
141
        }
1✔
142
    }
1✔
143

144
    private void generateSidebar(Map<Language, List<RuleSet>> sortedRulesets) throws IOException {
145
        SidebarGenerator generator = new SidebarGenerator(writer, root);
1✔
146
        generator.generateSidebar(sortedRulesets);
1✔
147
    }
1✔
148

149
    private List<RuleSet> resolveAdditionalRulesets(List<String> additionalRulesets) {
150
        if (additionalRulesets == null) {
1!
151
            return Collections.emptyList();
×
152
        }
153

154
        List<RuleSet> rulesets = new ArrayList<>();
1✔
155
        RuleSetLoader ruleSetLoader = new RuleSetLoader().warnDeprecated(false);
1✔
156
        for (String filename : additionalRulesets) {
1✔
157
            try {
158
                // do not take rulesets from pmd-test or pmd-core
159
                if (!filename.contains("pmd-test") && !filename.contains("pmd-core")) {
1!
160
                    rulesets.add(ruleSetLoader.loadFromResource(filename));
1✔
161
                } else {
162
                    LOG.debug("Ignoring ruleset {}", filename);
×
163
                }
164
            } catch (RuleSetLoadException e) {
×
165
                // ignore rulesets, we can't read
166
                LOG.warn("ruleset file {} ignored ({})", filename, e.getMessage(), e);
×
167
            }
1✔
168
        }
1✔
169
        return rulesets;
1✔
170
    }
171

172
    private Path getAbsoluteOutputPath(String filename) {
173
        return root.resolve(IOUtil.normalizePath(filename));
1✔
174
    }
175

176
    private Map<Language, List<RuleSet>> sortRulesets(List<RuleSet> rulesets) {
177
        SortedMap<Language, List<RuleSet>> rulesetsByLanguage = rulesets.stream().collect(Collectors.groupingBy(RuleDocGenerator::getRuleSetLanguage,
1✔
178
                                                                                                                TreeMap::new,
179
                                                                                                                Collectors.toCollection(ArrayList::new)));
1✔
180

181
        for (List<RuleSet> rulesetsOfOneLanguage : rulesetsByLanguage.values()) {
1✔
182
            rulesetsOfOneLanguage.sort((o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName()));
1✔
183
        }
1✔
184
        return rulesetsByLanguage;
1✔
185
    }
186

187
    /**
188
     * Rulesets could potentially contain rules from various languages.
189
     * But for built-in rulesets, all rules within one ruleset belong to
190
     * one language. So we take the language of the first rule.
191
     * @param ruleset
192
     * @return the terse name of the ruleset's language
193
     */
194
    private static Language getRuleSetLanguage(RuleSet ruleset) {
195
        Collection<Rule> rules = ruleset.getRules();
1✔
196
        if (rules.isEmpty()) {
1!
197
            throw new RuntimeException("Ruleset " + ruleset.getFileName() + " is empty!");
×
198
        }
199
        return rules.iterator().next().getLanguage();
1✔
200
    }
201

202
    /**
203
     * Writes for each language an index file, which lists the rulesets, the rules
204
     * and links to the ruleset pages.
205
     * @param rulesets all registered/built-in rulesets
206
     * @param sortedAdditionalRulesets additional rulesets
207
     * @throws IOException
208
     */
209
    private void generateLanguageIndex(Map<Language, List<RuleSet>> rulesets, Map<Language, List<RuleSet>> sortedAdditionalRulesets) throws IOException {
210
        for (Map.Entry<Language, List<RuleSet>> entry : rulesets.entrySet()) {
1✔
211
            String languageTersename = entry.getKey().getId();
1✔
212
            String filename = LANGUAGE_INDEX_FILENAME_PATTERN
1✔
213
                    .replace("${language.tersename}", languageTersename);
1✔
214
            Path path = getAbsoluteOutputPath(filename);
1✔
215

216
            List<String> lines = new LinkedList<>();
1✔
217
            lines.add("---");
1✔
218
            lines.add("title: " + entry.getKey().getName() + " Rules");
1✔
219
            lines.add("tags: [rule_references, " + languageTersename + "]");
1✔
220
            lines.add("summary: Index of all built-in rules available for " + entry.getKey().getName());
1✔
221
            lines.add("language_name: " + entry.getKey().getName());
1✔
222
            lines.add("permalink: " + LANGUAGE_INDEX_PERMALINK_PATTERN.replace("${language.tersename}", languageTersename));
1✔
223
            lines.add("folder: pmd/rules");
1✔
224
            lines.add("editmepath: false");
1✔
225
            lines.add("---");
1✔
226
            lines.add(GENERATED_WARNING_NO_SOURCE);
1✔
227

228
            for (RuleSet ruleset : entry.getValue()) {
1✔
229
                lines.add("## " + ruleset.getName());
1✔
230
                lines.add("");
1✔
231
                lines.add("{% include callout.html content=\"" + getRuleSetDescriptionSingleLine(ruleset) + "\" %}");
1✔
232
                lines.add("");
1✔
233

234
                for (Rule rule : getSortedRules(ruleset)) {
1✔
235
                    String link = RULESET_INDEX_PERMALINK_PATTERN
1✔
236
                            .replace("${language.tersename}", languageTersename)
1✔
237
                            .replace("${ruleset.name}", RuleSetUtils.getRuleSetFilename(ruleset));
1✔
238
                    if (rule instanceof RuleReference) {
1✔
239
                        RuleReference ref = (RuleReference) rule;
1✔
240
                        if (ruleset.getFileName().equals(ref.getRuleSetReference().getRuleSetFileName())) {
1✔
241
                            // rule renamed within same ruleset
242
                            lines.add("*   [" + rule.getName() + "](" + link + "#" + rule.getName().toLowerCase(Locale.ROOT) + "): "
1✔
243
                                    + DEPRECATION_LABEL_SMALL
244
                                    + "The rule has been renamed. Use instead "
245
                                    + "[" + ref.getRule().getName() + "](" + link + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ").");
1✔
246
                        } else {
247
                            // rule moved to another ruleset...
248
                            String otherLink = RULESET_INDEX_PERMALINK_PATTERN
1✔
249
                                    .replace("${language.tersename}", languageTersename)
1✔
250
                                    .replace("${ruleset.name}", RuleSetUtils.getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));
1✔
251
                            lines.add("*   [" + rule.getName() + "](" + link + "#" + rule.getName().toLowerCase(Locale.ROOT) + "): "
1✔
252
                                    + DEPRECATION_LABEL_SMALL
253
                                    + "The rule has been moved to another ruleset. Use instead "
254
                                    + "[" + ref.getRule().getName() + "](" + otherLink + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ").");
1✔
255
                        }
256
                    } else {
1✔
257
                        link += "#" + rule.getName().toLowerCase(Locale.ROOT);
1✔
258
                        lines.add("*   [" + rule.getName() + "](" + link + "): "
1✔
259
                                + (rule.isDeprecated() ? DEPRECATION_LABEL_SMALL : "")
1✔
260
                                + getShortRuleDescription(rule));
1✔
261
                    }
262
                }
1✔
263
                lines.add("");
1✔
264
            }
1✔
265

266
            List<RuleSet> additionalRulesetsForLanguage = sortedAdditionalRulesets.get(entry.getKey());
1✔
267
            if (additionalRulesetsForLanguage != null) {
1!
268
                lines.add("## Additional rulesets");
1✔
269
                lines.add("");
1✔
270

271
                for (RuleSet ruleset : additionalRulesetsForLanguage) {
1✔
272
                    boolean deprecated = RuleSetUtils.isRuleSetDeprecated(ruleset);
1✔
273

274
                    String rulesetName = ruleset.getName() + " (`" + RuleSetUtils.getRuleSetClasspath(ruleset) + "`)";
1✔
275

276
                    if (!deprecated) {
1✔
277
                        lines.add("*   " + rulesetName + ":");
1✔
278
                        lines.add("");
1✔
279
                        lines.add("    " + getRuleSetDescriptionSingleLine(ruleset));
1✔
280
                        lines.add("");
1✔
281
                    } else {
282
                        lines.add("*   " + rulesetName + ":");
1✔
283
                        lines.add("");
1✔
284
                        lines.add("    " + DEPRECATION_LABEL_SMALL + " This ruleset is for backwards compatibility.");
1✔
285
                        lines.add("");
1✔
286
                    }
287

288
                    lines.add("    It contains the following rules:");
1✔
289
                    lines.add("");
1✔
290
                    StringBuilder rules = new StringBuilder();
1✔
291
                    for (Rule rule : getSortedRules(ruleset)) {
1✔
292
                        if (rules.length() == 0) {
1✔
293
                            rules.append("    ");
1✔
294
                        } else {
295
                            rules.append(", ");
1✔
296
                        }
297

298
                        Rule resolvedRule = RuleSetUtils.resolveRuleReferences(rule);
1✔
299
                        if (resolvedRule instanceof RuleReference) {
1!
300
                            // Note: deprecated rulesets contain by definition only rule references
301
                            RuleReference ref = (RuleReference) resolvedRule;
1✔
302
                            String otherLink = RULESET_INDEX_PERMALINK_PATTERN
1✔
303
                                    .replace("${language.tersename}", languageTersename)
1✔
304
                                    .replace("${ruleset.name}", RuleSetUtils.getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));
1✔
305

306
                            rules.append("[").append(ref.getName()).append("](");
1✔
307
                            rules.append(otherLink).append("#").append(ref.getRule().getName().toLowerCase(Locale.ROOT)).append(")");
1✔
308
                        } else {
1✔
309
                            rules.append(rule.getName());
×
310
                        }
311
                    }
1✔
312
                    lines.add(rules.toString());
1✔
313
                    lines.add("");
1✔
314
                }
1✔
315
                lines.add("");
1✔
316
            }
317

318
            System.out.println("Generated " + path);
1✔
319
            writer.write(path, lines);
1✔
320
        }
1✔
321
    }
1✔
322

323
    /**
324
     * Shortens and escapes (for markdown) some special characters. Otherwise the shortened text
325
     * could contain some unfinished sequences.
326
     * @param rule
327
     * @return
328
     */
329
    private static String getShortRuleDescription(Rule rule) {
330
        String htmlEscaped = StringEscapeUtils.escapeHtml4(
1✔
331
            StringUtils.abbreviate(
1✔
332
                StringUtils.stripToEmpty(
1✔
333
                    rule.getDescription()
1✔
334
                        .replaceAll("\n+|\r+", " ")
1✔
335
                        .replaceAll("\\|", "\\\\|")
1✔
336
                        .replaceAll("`", "'")
1✔
337
                        .replaceAll("\\*", "")),
1✔
338
                100));
339
        return EscapeUtils.preserveRuleTagQuotes(htmlEscaped);
1✔
340
    }
341

342
    private static String getRuleSetDescriptionSingleLine(RuleSet ruleset) {
343
        String description = ruleset.getDescription();
1✔
344
        description = StringEscapeUtils.escapeHtml4(description);
1✔
345
        description = description.replaceAll("\\n|\\r", " ");
1✔
346
        description = StringUtils.stripToEmpty(description);
1✔
347
        return EscapeUtils.preserveRuleTagQuotes(description);
1✔
348
    }
349

350
    private static List<String> toLines(String s) {
351
        return Arrays.asList(s.split("\r\n|\n"));
1✔
352
    }
353

354
    /**
355
     * Generates for each ruleset a page. The page contains the details for each rule.
356
     *
357
     * @param rulesets all rulesets
358
     * @throws IOException
359
     */
360
    private void generateRuleSetIndex(Map<Language, List<RuleSet>> rulesets) throws IOException {
361
        for (Map.Entry<Language, List<RuleSet>> entry : rulesets.entrySet()) {
1✔
362
            Language language = entry.getKey();
1✔
363
            String languageTersename = language.getId();
1✔
364
            String languageName = language.getName();
1✔
365
            for (RuleSet ruleset : entry.getValue()) {
1✔
366
                String rulesetFilename = RuleSetUtils.getRuleSetFilename(ruleset);
1✔
367
                String filename = RULESET_INDEX_FILENAME_PATTERN
1✔
368
                    .replace("${language.tersename}", languageTersename)
1✔
369
                    .replace("${ruleset.name}", rulesetFilename);
1✔
370

371
                Path path = getAbsoluteOutputPath(filename);
1✔
372

373
                String permalink = RULESET_INDEX_PERMALINK_PATTERN
1✔
374
                        .replace("${language.tersename}", languageTersename)
1✔
375
                        .replace("${ruleset.name}", rulesetFilename);
1✔
376
                String ruleSetSourceFilepath = "../" + allRulesets.get(ruleset.getFileName());
1✔
377

378
                List<Rule> sortedRules = getSortedRules(ruleset);
1✔
379

380
                List<String> lines = new LinkedList<>();
1✔
381
                lines.add("---");
1✔
382
                lines.add("title: " + ruleset.getName());
1✔
383
                lines.add("summary: " + getRuleSetDescriptionSingleLine(ruleset));
1✔
384
                lines.add("permalink: " + permalink);
1✔
385
                lines.add("folder: pmd/rules/" + languageTersename);
1✔
386
                lines.add("sidebaractiveurl: /" + LANGUAGE_INDEX_PERMALINK_PATTERN.replace("${language.tersename}", languageTersename));
1✔
387
                lines.add("editmepath: " + ruleSetSourceFilepath);
1✔
388
                lines.add("keywords: " + ruleset.getName() + getRuleSetKeywords(sortedRules));
1✔
389
                lines.add("rules:");
1✔
390
                for (Rule rule : sortedRules) {
1✔
391
                    lines.add("  " + rule.getName() + ": |");
1✔
392

393
                    if (isRuleRenamed(rule, ruleset)) {
1✔
394
                        getRuleRenamedDeprecationNotice(rule).stream()
1✔
395
                                .map(line -> "    " + line)
1✔
396
                                .forEachOrdered(lines::add);
1✔
397
                    } else if (isRuleMoved(rule, ruleset)) {
1✔
398
                        getRuleMovedDeprecationNotice(rule, languageTersename).stream()
1✔
399
                                .map(line -> "    " + line)
1✔
400
                                .forEachOrdered(lines::add);
1✔
401
                    } else {
402
                        List<String> description = EscapeUtils.escapeLines(toLines(stripIndentation(rule.getDescription())));
1✔
403
                        description.stream()
1✔
404
                                .map(line -> "    " + line)
1✔
405
                                .forEachOrdered(lines::add);
1✔
406
                    }
407
                }
1✔
408
                lines.add("language: " + languageName);
1✔
409
                lines.add("---");
1✔
410
                lines.add(GENERATED_WARNING.replace("${source}", ruleSetSourceFilepath));
1✔
411

412
                for (Rule rule : sortedRules) {
1✔
413
                    lines.add("## " + rule.getName());
1✔
414
                    lines.add("");
1✔
415

416
                    if (isRuleRenamed(rule, ruleset)) {
1✔
417
                        // rule renamed within same ruleset
418
                        lines.addAll(getRuleRenamedDeprecationNotice(rule));
1✔
419
                    } else if (isRuleMoved(rule, ruleset)) {
1✔
420
                        // rule moved to another ruleset
421
                        lines.addAll(getRuleMovedDeprecationNotice(rule, languageTersename));
1✔
422
                    }
423

424
                    if (rule.isDeprecated()) {
1✔
425
                        lines.add(DEPRECATION_LABEL);
1✔
426
                        lines.add("");
1✔
427
                    }
428
                    if (rule.getSince() != null) {
1!
429
                        lines.add("**Since:** PMD " + rule.getSince());
1✔
430
                        lines.add("");
1✔
431
                    }
432
                    lines.add("**Priority:** " + rule.getPriority() + " (" + rule.getPriority().getPriority() + ")");
1✔
433
                    lines.add("");
1✔
434

435
                    if (rule.getMinimumLanguageVersion() != null) {
1✔
436
                        lines.add("**Minimum Language Version:** "
1✔
437
                                + rule.getLanguage().getName() + " " + rule.getMinimumLanguageVersion().getVersion());
1✔
438
                        lines.add("");
1✔
439
                    }
440

441
                    if (rule.getMaximumLanguageVersion() != null) {
1✔
442
                        lines.add("**Maximum Language Version:** "
1✔
443
                                + rule.getLanguage().getName() + " " + rule.getMaximumLanguageVersion().getVersion());
1✔
444
                        lines.add("");
1✔
445
                    }
446

447
                    lines.addAll(EscapeUtils.escapeLines(toLines(stripIndentation(rule.getDescription()))));
1✔
448
                    lines.add("");
1✔
449

450
                    XPathRule xpathRule = asXPathRule(rule);
1✔
451
                    if (xpathRule != null) {
1✔
452
                        lines.add("**This rule is defined by the following XPath expression:**");
1✔
453
                        lines.add("``` xpath");
1✔
454
                        lines.addAll(toLines(StringUtils.stripToEmpty(xpathRule.getXPathExpression())));
1✔
455
                        lines.add("```");
1✔
456
                    } else {
457
                        lines.add("**This rule is defined by the following Java class:** "
1✔
458
                                + "[" + rule.getRuleClass() + "]("
1✔
459
                                + GITHUB_SOURCE_LINK + allRules.get(rule.getRuleClass())
1✔
460
                                + ")");
461
                    }
462
                    lines.add("");
1✔
463

464
                    if (!rule.getExamples().isEmpty()) {
1✔
465
                        lines.add("**Example(s):**");
1✔
466
                        lines.add("");
1✔
467
                        for (String example : rule.getExamples()) {
1✔
468
                            lines.add("``` " + mapLanguageForHighlighting(languageTersename));
1✔
469
                            lines.addAll(toLines("{%raw%}" + StringUtils.stripToEmpty(example) + "{%endraw%}"));
1✔
470
                            lines.add("```");
1✔
471
                            lines.add("");
1✔
472
                        }
1✔
473
                    }
474

475
                    List<PropertyDescriptor<?>> properties = new ArrayList<>(rule.getPropertyDescriptors());
1✔
476
                    // filter out standard properties
477
                    properties.remove(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR);
1✔
478
                    properties.remove(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR);
1✔
479
                    properties.removeIf(p -> "xpath".equals(p.name())); // this is XPathRule.XPATH_DESCRIPTOR
1✔
480

481
                    if (!properties.isEmpty()) {
1✔
482
                        lines.add("**This rule has the following properties:**");
1✔
483
                        lines.add("");
1✔
484
                        lines.add("|Name|Default Value|Description|");
1✔
485
                        lines.add("|----|-------------|-----------|");
1✔
486
                        for (PropertyDescriptor<?> propertyDescriptor : properties) {
1✔
487
                            String description = propertyDescriptor.description();
1✔
488
                            final boolean isDeprecated = isDeprecated(propertyDescriptor);
1✔
489
                            if (isDeprecated) {
1✔
490
                                description = description.substring(DEPRECATED_RULE_PROPERTY_MARKER.length());
1✔
491
                            }
492

493
                            String defaultValue = determineDefaultValueAsString(propertyDescriptor, rule, true);
1✔
494

495
                            lines.add("|"
1✔
496
                                    + EscapeUtils.escapeMarkdown(StringEscapeUtils.escapeHtml4(propertyDescriptor.name()))
1✔
497
                                    + "|"
498
                                    + EscapeUtils.escapeMarkdown(defaultValue)
1✔
499
                                    + "|"
500
                                    + EscapeUtils.escapeMarkdown((isDeprecated ? DEPRECATION_LABEL_SMALL : "") + StringEscapeUtils.escapeHtml4(description))
1✔
501
                                    + "|"
502
                            );
503
                        }
1✔
504
                        lines.add("");
1✔
505
                    }
506

507
                    if (properties.isEmpty()) {
1✔
508
                        lines.add("**Use this rule by referencing it:**");
1✔
509
                    } else {
510
                        lines.add("**Use this rule with the default properties by just referencing it:**");
1✔
511
                    }
512
                    lines.add("``` xml");
1✔
513
                    lines.add("<rule ref=\"category/" + languageTersename + "/" + rulesetFilename + ".xml/" + rule.getName() + "\" />");
1✔
514
                    lines.add("```");
1✔
515
                    lines.add("");
1✔
516

517
                    if (properties.stream().anyMatch(it -> !isDeprecated(it))) {
1!
518
                        lines.add("**Use this rule and customize it:**");
1✔
519
                        lines.add("``` xml");
1✔
520
                        lines.add("<rule ref=\"category/" + languageTersename + "/" + rulesetFilename + ".xml/" + rule.getName() + "\">");
1✔
521
                        lines.add("    <properties>");
1✔
522
                        for (PropertyDescriptor<?> propertyDescriptor : properties) {
1✔
523
                            if (!isDeprecated(propertyDescriptor)) {
1✔
524
                                String defaultValue = determineDefaultValueAsString(propertyDescriptor, rule, false);
1✔
525
                                lines.add("        <property name=\"" + propertyDescriptor.name() + "\" value=\""
1✔
526
                                              + defaultValue + "\" />");
527
                            }
528
                        }
1✔
529
                        lines.add("    </properties>");
1✔
530
                        lines.add("</rule>");
1✔
531
                        lines.add("```");
1✔
532
                        lines.add("");
1✔
533
                    }
534
                }
1✔
535

536
                writer.write(path, lines);
1✔
537
                System.out.println("Generated " + path);
1✔
538
            }
1✔
539
        }
1✔
540
    }
1✔
541

542
    private Collection<String> getRuleMovedDeprecationNotice(Rule rule, String languageTersename) {
543
        RuleReference ref = (RuleReference) rule;
1✔
544
        List<String> lines = new LinkedList<>();
1✔
545
        String otherLink = RULESET_INDEX_PERMALINK_PATTERN
1✔
546
                .replace("${language.tersename}", languageTersename)
1✔
547
                .replace("${ruleset.name}", RuleSetUtils.getRuleSetFilename(ref.getRuleSetReference().getRuleSetFileName()));
1✔
548
        lines.add(DEPRECATION_LABEL);
1✔
549
        lines.add("");
1✔
550
        lines.add("The rule has been moved to another ruleset. Use instead: ["
1✔
551
                + ref.getRule().getName() + "](" + otherLink + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ")");
1✔
552
        lines.add("");
1✔
553
        return lines;
1✔
554
    }
555

556
    private Collection<String> getRuleRenamedDeprecationNotice(Rule rule) {
557
        RuleReference ref = (RuleReference) rule;
1✔
558
        List<String> lines = new LinkedList<>();
1✔
559
        lines.add(DEPRECATION_LABEL);
1✔
560
        lines.add("");
1✔
561
        lines.add("This rule has been renamed. Use instead: ["
1✔
562
                + ref.getRule().getName() + "](" + "#" + ref.getRule().getName().toLowerCase(Locale.ROOT) + ")");
1✔
563
        lines.add("");
1✔
564
        return lines;
1✔
565
    }
566

567
    private boolean isRuleRenamed(Rule rule, RuleSet ruleset) {
568
        if (rule instanceof RuleReference) {
1✔
569
            RuleReference ref = (RuleReference) rule;
1✔
570
            return ruleset.getFileName().equals(ref.getRuleSetReference().getRuleSetFileName());
1✔
571
        }
572
        return false;
1✔
573
    }
574

575
    private boolean isRuleMoved(Rule rule, RuleSet ruleset) {
576
        if (rule instanceof RuleReference) {
1✔
577
            RuleReference ref = (RuleReference) rule;
1✔
578
            return !ruleset.getFileName().equals(ref.getRuleSetReference().getRuleSetFileName());
1!
579
        }
580
        return false;
1✔
581
    }
582

583
    private XPathRule asXPathRule(Rule rule) {
584
        if (rule instanceof XPathRule) {
1✔
585
            return (XPathRule) rule;
1✔
586
        } else if (rule instanceof RuleReference && ((RuleReference) rule).getRule() instanceof XPathRule) {
1!
587
            return (XPathRule) ((RuleReference) rule).getRule();
1✔
588
        }
589
        return null;
1✔
590
    }
591

592
    private static boolean isDeprecated(PropertyDescriptor<?> propertyDescriptor) {
593
        return propertyDescriptor.description() != null
1!
594
            && propertyDescriptor.description().toLowerCase(Locale.ROOT).startsWith(DEPRECATED_RULE_PROPERTY_MARKER);
1✔
595
    }
596

597
    private <T> String determineDefaultValueAsString(PropertyDescriptor<T> propertyDescriptor, Rule rule, boolean pad) {
598
        String defaultValue = "";
1✔
599
        T realDefaultValue = rule.getProperty(propertyDescriptor);
1✔
600

601
        if (realDefaultValue != null) {
1!
602
            defaultValue = propertyDescriptor.serializer().toString(realDefaultValue);
1✔
603
            if (pad && realDefaultValue instanceof Collection) {
1✔
604
                // surround the delimiter with spaces, so that the browser can wrap
605
                // the value nicely
606
                defaultValue = defaultValue.replaceAll(",", " , ");
1✔
607
            }
608
        }
609
        defaultValue = StringEscapeUtils.escapeHtml4(defaultValue);
1✔
610
        return defaultValue;
1✔
611
    }
612

613
    private static String stripIndentation(String description) {
614
        if (description == null || description.isEmpty()) {
1!
615
            return "";
×
616
        }
617

618
        String stripped = StringUtils.stripStart(description, "\n\r");
1✔
619
        stripped = StringUtils.stripEnd(stripped, "\n\r ");
1✔
620

621
        int indentation = 0;
1✔
622
        int strLen = stripped.length();
1✔
623
        while (indentation < strLen && Character.isWhitespace(stripped.charAt(indentation))) {
1!
624
            indentation++;
1✔
625
        }
626

627
        String[] lines = stripped.split("\\n");
1✔
628
        String prefix = StringUtils.repeat(' ', indentation);
1✔
629
        StringBuilder result = new StringBuilder(stripped.length());
1✔
630

631
        if (StringUtils.isNotEmpty(prefix)) {
1✔
632
            for (int i = 0; i < lines.length; i++) {
1✔
633
                String line = lines[i];
1✔
634
                if (i > 0) {
1✔
635
                    result.append(StringUtils.LF);
1✔
636
                }
637
                result.append(StringUtils.removeStart(line, prefix));
1✔
638
            }
639
        } else {
640
            result.append(stripped);
1✔
641
        }
642
        return result.toString();
1✔
643
    }
644

645
    /**
646
     * Simply maps PMD languages to rouge languages
647
     *
648
     * @param languageTersename
649
     * @return
650
     * @see <a href="https://github.com/jneen/rouge/wiki/List-of-supported-languages-and-lexers">List of supported languages</a>
651
     */
652
    private static String mapLanguageForHighlighting(String languageTersename) {
653
        if (LANGUAGE_HIGHLIGHT_MAPPER.containsKey(languageTersename)) {
1!
654
            return LANGUAGE_HIGHLIGHT_MAPPER.get(languageTersename);
×
655
        }
656
        return languageTersename;
1✔
657
    }
658

659
    private String getRuleSetKeywords(List<Rule> rules) {
660
        if (rules.isEmpty()) {
1!
NEW
661
            return "";
×
662
        }
663

664
        List<String> ruleNames = new LinkedList<>();
1✔
665
        for (Rule rule : rules) {
1✔
666
            ruleNames.add(rule.getName());
1✔
667
        }
1✔
668
        return ", " + StringUtils.join(ruleNames, ", ");
1✔
669
    }
670

671
    private List<Rule> getSortedRules(RuleSet ruleset) {
672
        List<Rule> sortedRules = new ArrayList<>(ruleset.getRules());
1✔
673
        Collections.sort(sortedRules, new Comparator<Rule>() {
1✔
674
            @Override
675
            public int compare(Rule o1, Rule o2) {
676
                return o1.getName().compareToIgnoreCase(o2.getName());
1✔
677
            }
678
        });
679
        return sortedRules;
1✔
680
    }
681

682
    /**
683
     * Walks through the root directory once to get all rule source file path names and ruleset names.
684
     * This provides the information for the "editme" links.
685
     *
686
     * @param sortedRulesets all the rulesets and rules
687
     */
688
    private void determineRuleClassSourceFiles(Map<Language, List<RuleSet>> sortedRulesets) {
689
        // first collect all the classes, we need to resolve and the rulesets
690
        // this also provides a default fallback path, which is used in unit tests.
691
        // if the actual file is found during walkFileTree, then the default fallback path
692
        // is replaced by a correct path.
693
        for (List<RuleSet> rulesets : sortedRulesets.values()) {
1✔
694
            for (RuleSet ruleset : rulesets) {
1✔
695
                String rulesetFilename = RuleSetUtils.normalizeForwardSlashes(StringUtils.chomp(ruleset.getFileName()));
1✔
696
                allRulesets.put(ruleset.getFileName(), rulesetFilename);
1✔
697
                for (Rule rule : ruleset.getRules()) {
1✔
698
                    String ruleClass = rule.getRuleClass();
1✔
699
                    String relativeSourceFilename = ruleClass.replaceAll("\\.", Matcher.quoteReplacement(File.separator))
1✔
700
                            + ".java";
701
                    allRules.put(ruleClass, RuleSetUtils.normalizeForwardSlashes(relativeSourceFilename));
1✔
702
                }
1✔
703
            }
1✔
704
        }
1✔
705

706
        // then go and search the actual files
707
        try {
708
            Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
1✔
709
                @Override
710
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
711
                    String path = RuleSetUtils.normalizeForwardSlashes(file.toString());
1✔
712

713
                    if (path.contains("src")) {
1!
714
                        String foundRuleClass = null;
×
715
                        for (Map.Entry<String, String> entry : allRules.entrySet()) {
×
716
                            if (path.endsWith(entry.getValue())) {
×
717
                                foundRuleClass = entry.getKey();
×
718
                                break;
×
719
                            }
720
                        }
×
721
                        if (foundRuleClass != null) {
×
722
                            Path foundPath = root.relativize(file);
×
723
                            allRules.put(foundRuleClass, RuleSetUtils.normalizeForwardSlashes(foundPath.toString()));
×
724
                        }
725

726
                        String foundRuleset = null;
×
727
                        for (Map.Entry<String, String> entry : allRulesets.entrySet()) {
×
728
                            if (path.endsWith(entry.getValue())) {
×
729
                                foundRuleset = entry.getKey();
×
730
                                break;
×
731
                            }
732
                        }
×
733
                        if (foundRuleset != null) {
×
734
                            Path foundPath = root.relativize(file);
×
735
                            allRulesets.put(foundRuleset, RuleSetUtils.normalizeForwardSlashes(foundPath.toString()));
×
736
                        }
737
                    }
738
                    return FileVisitResult.CONTINUE;
1✔
739
                }
740
            });
741
        } catch (IOException e) {
×
742
            throw new RuntimeException(e);
×
743
        }
1✔
744
    }
1✔
745
}
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