[doc] Fix rule tags in the rule docs

If using quotes, there was a html escape done, which made
the rule tag renderer to spit out "quot".
This commit is contained in:
Andreas Dangel
2020-05-23 13:07:25 +02:00
parent 6edd6c3ebb
commit 561825703f
9 changed files with 71 additions and 13 deletions
pmd-doc/src
main/java/net/sourceforge/pmd/docs
test
java/net/sourceforge/pmd/docs
resources
expected
rulesets/ruledoctest
ruletagchecker/docs/pages
pmd-java/src/main/resources/category/java

@ -5,6 +5,8 @@
package net.sourceforge.pmd.docs;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.text.StringEscapeUtils;
@ -12,6 +14,7 @@ public final class EscapeUtils {
private static final String BACKTICK = "`";
private static final String URL_START = "<http";
private static final String QUOTE_START = ">";
private static final Pattern RULE_TAG = Pattern.compile("\\{%\\s+rule\\s+&quot;([^&]+)&quot;\\s*\\%}");
private EscapeUtils() {
// This is a utility class
@ -83,9 +86,22 @@ public final class EscapeUtils {
}
if (needsEscape && !line.startsWith(" ")) {
line = escapeSingleLine(line);
line = preserveRuleTagQuotes(line);
}
lines.set(i, line);
}
return lines;
}
/**
* If quotes are used for rule tags, e.g. {@code {% rule "OtherRule" %}}, these
* quotes might have been escaped for html, but it's actually markdown/jekyll/liquid
* and not html. This undoes the escaping.
* @param text the already escaped text that might contain rule tags
* @return text with the fixed rule tags
*/
public static String preserveRuleTagQuotes(String text) {
Matcher m = RULE_TAG.matcher(text);
return m.replaceAll("{% rule \"$1\" %}");
}
}

@ -299,15 +299,16 @@ public class RuleDocGenerator {
* @return
*/
private static String getShortRuleDescription(Rule rule) {
return StringEscapeUtils.escapeHtml4(
String htmlEscaped = StringEscapeUtils.escapeHtml4(
StringUtils.abbreviate(
StringUtils.stripToEmpty(
rule.getDescription()
.replaceAll("\n|\r", "")
.replaceAll("\n+|\r+", " ")
.replaceAll("\\|", "\\\\|")
.replaceAll("`", "'")
.replaceAll("\\*", "")),
100));
return EscapeUtils.preserveRuleTagQuotes(htmlEscaped);
}
private static String getRuleSetDescriptionSingleLine(RuleSet ruleset) {
@ -315,7 +316,7 @@ public class RuleDocGenerator {
description = StringEscapeUtils.escapeHtml4(description);
description = description.replaceAll("\\n|\\r", " ");
description = StringUtils.stripToEmpty(description);
return description;
return EscapeUtils.preserveRuleTagQuotes(description);
}
private static List<String> toLines(String s) {

@ -28,8 +28,10 @@ import java.util.regex.Pattern;
public class RuleTagChecker {
private static final Logger LOG = Logger.getLogger(DeadLinksChecker.class.getName());
private static final Pattern RULE_TAG = Pattern.compile("\\{%\\s*rule\\s+\"(.*?)\"\\s*");
private static final Pattern RULE_REFERENCE = Pattern.compile("(\\w+)\\/(\\w+)\\/(\\w+)");
private static final String QUOTE = "\"";
private static final Pattern RULE_TAG = Pattern.compile("\\{%\\s*rule\\s+(\"?[^\"%\\}\\s]+\"?)\\s*");
private static final Pattern RULE_REFERENCE = Pattern.compile("\"?(\\w+)\\/(\\w+)\\/(\\w+)\"?");
private static final Pattern RULE_SIMPLE_REFERENCE = Pattern.compile("\"?(\\w+)\"?");
private final Path pagesDirectory;
private final List<String> issues = new ArrayList<>();
@ -72,6 +74,9 @@ public class RuleTagChecker {
int pos = ruleTagMatcher.end();
if (line.charAt(pos) != '%' || line.charAt(pos + 1) != '}') {
addIssue(file, lineNo, "Rule tag for " + ruleReference + " is not closed properly");
} else if (ruleReference.startsWith(QUOTE) && !ruleReference.endsWith(QUOTE)
|| !ruleReference.startsWith(QUOTE) && ruleReference.endsWith(QUOTE)) {
addIssue(file, lineNo, "Rule tag for " + ruleReference + " has a missing quote");
} else if (!ruleReferenceTargetExists(ruleReference)) {
addIssue(file, lineNo, "Rule " + ruleReference + " is not found");
}
@ -81,6 +86,7 @@ public class RuleTagChecker {
private boolean ruleReferenceTargetExists(String ruleReference) {
Matcher ruleRefMatcher = RULE_REFERENCE.matcher(ruleReference);
Matcher simpleRefMatcher = RULE_SIMPLE_REFERENCE.matcher(ruleReference);
if (ruleRefMatcher.matches()) {
String language = ruleRefMatcher.group(1);
String category = ruleRefMatcher.group(2);
@ -89,6 +95,9 @@ public class RuleTagChecker {
Path ruleDocPage = pagesDirectory.resolve("pmd/rules/" + language + "/" + category.toLowerCase(Locale.ROOT) + ".md");
Set<String> rules = getRules(ruleDocPage);
return rules.contains(rule);
} else if (simpleRefMatcher.matches()) {
// can't check - would need to know the current language + category
return true;
}
return false;
}

@ -18,12 +18,18 @@ public class RuleTagCheckerTest {
RuleTagChecker checker = new RuleTagChecker(FileSystems.getDefault().getPath("src/test/resources/ruletagchecker"));
List<String> issues = checker.check();
Assert.assertEquals(3, issues.size());
Assert.assertEquals("ruletag-examples.md: 8: Rule tag for java/bestpractices/AvoidPrintStackTrace is not closed properly",
Assert.assertEquals(7, issues.size());
Assert.assertEquals("ruletag-examples.md: 9: Rule tag for \"java/bestpractices/AvoidPrintStackTrace\" is not closed properly",
issues.get(0));
Assert.assertEquals("ruletag-examples.md:10: Rule java/notexistingcategory/AvoidPrintStackTrace is not found",
Assert.assertEquals("ruletag-examples.md:12: Rule \"java/notexistingcategory/AvoidPrintStackTrace\" is not found",
issues.get(1));
Assert.assertEquals("ruletag-examples.md:12: Rule java/bestpractices/NotExistingRule is not found",
Assert.assertEquals("ruletag-examples.md:14: Rule \"java/bestpractices/NotExistingRule\" is not found",
issues.get(2));
Assert.assertEquals("ruletag-examples.md:16: Rule tag for \"java/bestpractices/OtherRule has a missing quote",
issues.get(3));
Assert.assertEquals("ruletag-examples.md:17: Rule tag for java/bestpractices/OtherRule\" has a missing quote",
issues.get(4));
Assert.assertEquals("ruletag-examples.md:21: Rule tag for \"OtherRule has a missing quote", issues.get(5));
Assert.assertEquals("ruletag-examples.md:22: Rule tag for OtherRule\" has a missing quote", issues.get(6));
}
}

@ -11,7 +11,7 @@ folder: pmd/rules
{% include callout.html content="Sample ruleset to test rule doc generation. Here might be &lt;script&gt;alert('XSS');&lt;/script&gt; as well. And &quot;quotes&quot;. Inside CDATA &lt;script&gt;alert('XSS');&lt;/script&gt;." %}
* [DeprecatedSample](pmd_rules_java_sample.html#deprecatedsample): <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> Just some description of a deprecated rule.
* [DeprecatedSample](pmd_rules_java_sample.html#deprecatedsample): <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> Just some description of a deprecated rule. RuleTag with quotes: Use {% rule "RenamedRule" %} ins...
* [JumbledIncrementer](pmd_rules_java_sample.html#jumbledincrementer): Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional.
* [MovedRule](pmd_rules_java_sample.html#movedrule): <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> The rule has been moved to another ruleset. Use instead [JumbledIncrementer](pmd_rules_java_sample2.html#jumbledincrementer).
* [OverrideBothEqualsAndHashcode](pmd_rules_java_sample.html#overridebothequalsandhashcode): Override both 'public boolean Object.equals(Object other)', and 'public int Object.hashCode()', o...

@ -19,6 +19,14 @@ language: Java
Just some description of a deprecated rule.
RuleTag with quotes: Use {% rule "RenamedRule" %} instead.
RuleTag without quotes: Use {% rule RenamedRule %} instead.
RuleTag with full category and quotes: Use {% rule "java/sample/RenamedRule" %} instead.
RuleTag with full category and without quotes: Use {% rule java/sample/RenamedRule %} instead.
**This rule is defined by the following XPath expression:**
``` xpath
//ForStatement

@ -194,6 +194,14 @@ public class JumbledIncrementerRule1 {
deprecated="true">
<description>
Just some description of a deprecated rule.
RuleTag with quotes: Use {% rule "RenamedRule" %} instead.
RuleTag without quotes: Use {% rule RenamedRule %} instead.
RuleTag with full category and quotes: Use {% rule "java/sample/RenamedRule" %} instead.
RuleTag with full category and without quotes: Use {% rule java/sample/RenamedRule %} instead.
</description>
<priority>3</priority>
<properties>

@ -4,9 +4,19 @@ permalink: rule_tag_samples.html
---
This is a link to the rule AvoidPrintStackTrace: {% rule "java/bestpractices/AvoidPrintStackTrace" %}.
This is a link to the rule AvoidPrintStackTrace without quotes: {% rule java/bestpractices/AvoidPrintStackTrace %}.
This is the same link, but the rule tag is not closed properly: {% rule "java/bestpractices/AvoidPrintStackTrace" %).
Now this is link to a rule inside a category, which doesn't exist: {% rule "java/notexistingcategory/AvoidPrintStackTrace" %}.
Now this is link to a rule inside a category, which doesn't exist:
{% rule "java/notexistingcategory/AvoidPrintStackTrace" %}.
This is link to a rule, which doesn't exist: {% rule "java/bestpractices/NotExistingRule" %}.
This is a rule tag, which has only one quote: {% rule "java/bestpractices/OtherRule %}.
This is a rule tag, which has only one quote: {% rule java/bestpractices/OtherRule" %}.
This is a rule tag, that references a rule in the same category: {% rule OtherRule %} (correct).
This is a rule tag, that references a rule in the same category: {% rule "OtherRule" %} (correct).
This is a rule tag, that references a rule in the same category: {% rule "OtherRule %} (missing quote).
This is a rule tag, that references a rule in the same category: {% rule OtherRule" %} (missing quote).

@ -1059,7 +1059,7 @@ String name,
Position literals first in comparisons, if the second argument is null then NullPointerExceptions
can be avoided, they will just return false.
This rule is replaced by the more general rule {% rule "LiteralsFirstInComparisons %}.
This rule is replaced by the more general rule {% rule "LiteralsFirstInComparisons" %}.
</description>
<priority>3</priority>
<example>
@ -1084,7 +1084,7 @@ class Foo {
Position literals first in comparisons, if the second argument is null then NullPointerExceptions
can be avoided, they will just return false.
This rule is replaced by the more general rule {% rule "LiteralsFirstInComparisons %}.
This rule is replaced by the more general rule {% rule "LiteralsFirstInComparisons" %}.
</description>
<priority>3</priority>
<example>