diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/DefaultFileWriter.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/DefaultFileWriter.java new file mode 100644 index 0000000000..5c7e84ad2f --- /dev/null +++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/DefaultFileWriter.java @@ -0,0 +1,19 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.docs; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class DefaultFileWriter implements FileWriter { + @Override + public void write(Path path, List lines) throws IOException { + Files.createDirectories(path.getParent()); + Files.write(path, lines, StandardCharsets.UTF_8); + } +} diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/FileWriter.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/FileWriter.java new file mode 100644 index 0000000000..d47452754d --- /dev/null +++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/FileWriter.java @@ -0,0 +1,14 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.docs; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public interface FileWriter { + + void write(Path path, List lines) throws IOException; +} diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java index 5a34b910ac..64f04bc962 100644 --- a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java +++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java @@ -6,18 +6,27 @@ package net.sourceforge.pmd.docs; import java.nio.file.FileSystems; import java.nio.file.Path; +import java.util.Iterator; + +import net.sourceforge.pmd.RuleSet; +import net.sourceforge.pmd.RuleSetFactory; +import net.sourceforge.pmd.RuleSetNotFoundException; public class GenerateRuleDocsCmd { private GenerateRuleDocsCmd() { // Utility class } - public static void main(String[] args) { + public static void main(String[] args) throws RuleSetNotFoundException { long start = System.currentTimeMillis(); - RuleDocGenerator generator = new RuleDocGenerator(); Path output = FileSystems.getDefault().getPath(args[0]).resolve("..").toAbsolutePath().normalize(); System.out.println("Generating docs into " + output); - generator.generate(output); + RuleDocGenerator generator = new RuleDocGenerator(new DefaultFileWriter(), output); + + RuleSetFactory ruleSetFactory = new RuleSetFactory(); + Iterator registeredRuleSets = ruleSetFactory.getRegisteredRuleSets(); + + generator.generate(registeredRuleSets); System.out.println("Generated docs in " + (System.currentTimeMillis() - start) + " ms"); } } diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java index d999dd5d10..ade2c239a1 100644 --- a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java +++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java @@ -5,7 +5,6 @@ package net.sourceforge.pmd.docs; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -29,7 +28,6 @@ import org.apache.commons.lang3.StringUtils; import net.sourceforge.pmd.PropertyDescriptor; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSetNotFoundException; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.rule.XPathRule; @@ -40,21 +38,25 @@ public class RuleDocGenerator { private static final String RULESET_INDEX_FILENAME_PATTERN = "docs/pages/pmd/rules/${language.tersename}/${ruleset.name}.md"; private static final String RULESET_INDEX_PERMALINK_PATTERN = "pmd_rules_${language.tersename}_${ruleset.name}.html"; - private Path root; + private final Path root; + private final FileWriter writer; - public void generate(Path root) { + public RuleDocGenerator(FileWriter writer, Path root) { this.root = Objects.requireNonNull(root, "Root directory must be provided"); + this.writer = Objects.requireNonNull(writer, "A file writer must be provided"); Path docsDir = root.resolve("docs"); if (!Files.exists(docsDir) || !Files.isDirectory(docsDir)) { throw new IllegalArgumentException("Couldn't find \"docs\" subdirectory"); } + } - Map> rulesets; + public void generate(Iterator rulesets) { + Map> sortedRulesets; try { - rulesets = loadAndSortRulesets(); - generateLanguageIndex(rulesets); - generateRuleSetIndex(rulesets); + sortedRulesets = sortRulesets(rulesets); + generateLanguageIndex(sortedRulesets); + generateRuleSetIndex(sortedRulesets); } catch (RuleSetNotFoundException | IOException e) { throw new RuntimeException(e); @@ -65,23 +67,20 @@ public class RuleDocGenerator { return root.resolve(FilenameUtils.normalize(filename)); } - private Map> loadAndSortRulesets() throws RuleSetNotFoundException { - RuleSetFactory ruleSetFactory = new RuleSetFactory(); - Iterator registeredRuleSets = ruleSetFactory.getRegisteredRuleSets(); + private Map> sortRulesets(Iterator rulesets) throws RuleSetNotFoundException { + Map> rulesetsByLanguage = new HashMap<>(); - Map> rulesets = new HashMap<>(); - - while (registeredRuleSets.hasNext()) { - RuleSet ruleset = registeredRuleSets.next(); + while (rulesets.hasNext()) { + RuleSet ruleset = rulesets.next(); Language language = getRuleSetLanguage(ruleset); - if (!rulesets.containsKey(language)) { - rulesets.put(language, new ArrayList()); + if (!rulesetsByLanguage.containsKey(language)) { + rulesetsByLanguage.put(language, new ArrayList()); } - rulesets.get(language).add(ruleset); + rulesetsByLanguage.get(language).add(ruleset); } - for (List rulesetsOfOneLanguage : rulesets.values()) { + for (List rulesetsOfOneLanguage : rulesetsByLanguage.values()) { Collections.sort(rulesetsOfOneLanguage, new Comparator() { @Override public int compare(RuleSet o1, RuleSet o2) { @@ -89,7 +88,7 @@ public class RuleDocGenerator { } }); } - return rulesets; + return rulesetsByLanguage; } /** @@ -152,7 +151,7 @@ public class RuleDocGenerator { } System.out.println("Generated " + path); - Files.write(path, lines, StandardCharsets.UTF_8); + writer.write(path, lines); } } @@ -219,6 +218,7 @@ public class RuleDocGenerator { for (Rule rule : getSortedRules(ruleset)) { lines.add("## " + rule.getName()); + lines.add(""); if (rule.getSince() != null) { lines.add("**Since:** " + rule.getSince()); lines.add(""); @@ -230,6 +230,7 @@ public class RuleDocGenerator { lines.add(""); if (!rule.getExamples().isEmpty()) { lines.add("**Example(s):**"); + lines.add(""); for (String example : rule.getExamples()) { lines.add("```"); lines.add(StringUtils.stripToEmpty(example)); @@ -260,8 +261,7 @@ public class RuleDocGenerator { } } - Files.createDirectories(path.getParent()); - Files.write(path, lines, StandardCharsets.UTF_8); + writer.write(path, lines); System.out.println("Generated " + path); } } diff --git a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/MockedFileWriter.java b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/MockedFileWriter.java new file mode 100644 index 0000000000..a533e136f2 --- /dev/null +++ b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/MockedFileWriter.java @@ -0,0 +1,46 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.docs; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +public class MockedFileWriter implements FileWriter { + + public static class FileEntry { + private String filename; + private String content; + + public String getFilename() { + return filename; + } + + public String getContent() { + return content; + } + } + + private List data = new ArrayList<>(); + + @Override + public void write(Path path, List lines) throws IOException { + FileEntry entry = new FileEntry(); + entry.filename = path.toString(); + entry.content = StringUtils.join(lines, System.getProperty("line.separator")); + data.add(entry); + } + + public List getData() { + return data; + } + + public void reset() { + data.clear(); + } +} diff --git a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java new file mode 100644 index 0000000000..43e64bb1d6 --- /dev/null +++ b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java @@ -0,0 +1,64 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.docs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import org.apache.commons.io.IOUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import net.sourceforge.pmd.RuleSet; +import net.sourceforge.pmd.RuleSetFactory; +import net.sourceforge.pmd.RuleSetNotFoundException; +import net.sourceforge.pmd.docs.MockedFileWriter.FileEntry; + +public class RuleDocGeneratorTest { + + private MockedFileWriter writer = new MockedFileWriter(); + private Path root; + + @Before + public void setup() throws IOException { + writer.reset(); + + root = Files.createTempDirectory("pmd-ruledocgenerator-test"); + Files.createDirectory(root.resolve("docs")); + } + + @After + public void cleanup() throws IOException { + Files.delete(root.resolve("docs")); + Files.delete(root); + } + + @Test + public void testSingleRuleset() throws RuleSetNotFoundException, IOException { + RuleDocGenerator generator = new RuleDocGenerator(writer, root); + + RuleSetFactory rsf = new RuleSetFactory(); + RuleSet ruleset = rsf.createRuleSet("rulesets/ruledoctest/sample.xml"); + + generator.generate(Arrays.asList(ruleset).iterator()); + + assertEquals(2, writer.getData().size()); + FileEntry languageIndex = writer.getData().get(0); + assertTrue(languageIndex.getFilename().endsWith("docs/pages/pmd/rules/java.md")); + assertEquals(IOUtils.toString(RuleDocGeneratorTest.class.getResourceAsStream("/expected/java.md")), + languageIndex.getContent()); + + FileEntry ruleSetIndex = writer.getData().get(1); + assertTrue(ruleSetIndex.getFilename().endsWith("docs/pages/pmd/rules/java/sample.md")); + assertEquals(IOUtils.toString(RuleDocGeneratorTest.class.getResourceAsStream("/expected/sample.md")), + ruleSetIndex.getContent()); + } +} diff --git a/pmd-doc/src/test/resources/expected/java.md b/pmd-doc/src/test/resources/expected/java.md new file mode 100644 index 0000000000..97278fac94 --- /dev/null +++ b/pmd-doc/src/test/resources/expected/java.md @@ -0,0 +1,12 @@ +--- +title: Java Rules +permalink: pmd_rules_java.html +folder: pmd/rules +--- +List of rulesets and rules contained in each ruleset. + +* [Sample](pmd_rules_java_sample.html): Sample ruleset to test rule doc generation. + +## Sample +* [JumbledIncrementer](pmd_rules_java_sample.html#jumbledincrementer): Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional. +* [OverrideBothEqualsAndHashcode](pmd_rules_java_sample.html#overridebothequalsandhashcode): Override both public boolean Object.equals(Object other), and public int Object.hashCode(), or ov... diff --git a/pmd-doc/src/test/resources/expected/sample.md b/pmd-doc/src/test/resources/expected/sample.md new file mode 100644 index 0000000000..356d5197da --- /dev/null +++ b/pmd-doc/src/test/resources/expected/sample.md @@ -0,0 +1,62 @@ +--- +title: Sample +summary: Sample ruleset to test rule doc generation. +permalink: pmd_rules_java_sample.html +folder: pmd/rules/java +sidebaractiveurl: /pmd_rules_java.html +editmepath: ../rulesets/ruledoctest/sample.xml +--- +## JumbledIncrementer + +**Since:** 1.0 + +**Priority:** Medium (3) + +Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional. + +**Example(s):** + +``` +public class JumbledIncrementerRule1 { + public void foo() { + for (int i = 0; i < 10; i++) { // only references 'i' + for (int k = 0; k < 20; i++) { // references both 'i' and 'k' + System.out.println("Hello"); + } + } + } +} +``` + +## OverrideBothEqualsAndHashcode + +**Since:** 0.4 + +**Priority:** Medium (3) + +Override both public boolean Object.equals(Object other), and public int Object.hashCode(), or override neither. Even if you are inheriting a hashCode() from a parent class, consider implementing hashCode and explicitly delegating to your superclass. + +**Example(s):** + +``` +public class Bar { // poor, missing a hashcode() method + public boolean equals(Object o) { + // do some comparison + } +} + +public class Baz { // poor, missing an equals() method + public int hashCode() { + // return some hash value + } +} + +public class Foo { // perfect, both methods provided + public boolean equals(Object other) { + // do some comparison + } + public int hashCode() { + // return some hash value + } +} +``` diff --git a/pmd-doc/src/test/resources/rulesets/ruledoctest/sample.xml b/pmd-doc/src/test/resources/rulesets/ruledoctest/sample.xml new file mode 100644 index 0000000000..e5d35152e1 --- /dev/null +++ b/pmd-doc/src/test/resources/rulesets/ruledoctest/sample.xml @@ -0,0 +1,85 @@ + + + + +Sample ruleset to test rule doc generation. + + + + +Override both public boolean Object.equals(Object other), and public int Object.hashCode(), or override neither. Even if you are inheriting a hashCode() from a parent class, consider implementing hashCode and explicitly delegating to your superclass. + + 3 + + + + + + + +Avoid jumbled loop incrementers - its usually a mistake, and is confusing even if intentional. + + 3 + + + + + + + + + + + +