diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/build.gradle.kts b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/build.gradle.kts similarity index 92% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/build.gradle.kts rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/build.gradle.kts index c35be986..e20f7b78 100644 --- a/packages/code-analyzer-pmd-engine/pmd-wrapper/build.gradle.kts +++ b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/build.gradle.kts @@ -42,7 +42,7 @@ application { // Directories of interest -val pmdWrapperDistDir: String = layout.projectDirectory.dir("../dist/pmd-wrapper").asFile.path; +val pmdCpdWrappersDistDir: String = layout.projectDirectory.dir("../dist/pmd-cpd-wrappers").asFile.path; val reportsDir: String = layout.buildDirectory.dir("reports").get().asFile.path @@ -59,7 +59,7 @@ tasks.distTar { // During assemble, we want to run the installDist task (which comes from the distribution plugin which comes from the application plugin) // instead ... but first we need to modify the location where the jar files should be placed. tasks.installDist { - into(pmdWrapperDistDir) + into(pmdCpdWrappersDistDir) includeEmptyDirs = false } tasks.assemble { @@ -102,9 +102,9 @@ tasks.register("showCoverageReport") { // ======== CLEAN RELATED TASKS ======================================================================================== -tasks.register("deletePmdWrapperFromDist") { - delete(pmdWrapperDistDir) +tasks.register("deletePmdCpdWrappersFromDist") { + delete(pmdCpdWrappersDistDir) } tasks.named("clean") { - dependsOn("deletePmdWrapperFromDist") + dependsOn("deletePmdCpdWrappersFromDist") } \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdLanguageRunResults.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdLanguageRunResults.java new file mode 100644 index 00000000..f2a55518 --- /dev/null +++ b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdLanguageRunResults.java @@ -0,0 +1,34 @@ +package com.salesforce.sfca.cpdwrapper; + +import java.util.ArrayList; +import java.util.List; + +/** + * Java object to help build cpd results for a specific language. + * We will serialize Map to json to create the overall results for all languages. + */ +public class CpdLanguageRunResults { + public List matches = new ArrayList<>(); + public List processingErrors = new ArrayList<>(); + + public static class Match { + public int numTokensInBlock; + public int numNonemptyLinesInBlock; + public int numBlocks; + public List blockLocations = new ArrayList<>(); + + public static class BlockLocation { + public String file; + public int startLine; + public int startCol; + public int endLine; + public int endCol; + } + } + + public static class ProcessingError { + public String file; + public String message; + public String detail; + } +} \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunInputData.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunInputData.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunInputData.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunInputData.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunner.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunner.java similarity index 75% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunner.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunner.java index 5a35e56e..b22f04ca 100644 --- a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunner.java +++ b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdRunner.java @@ -15,7 +15,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.text.MessageFormat; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -25,10 +24,10 @@ * Class to help us invoke CPD - once for each language that should be processed */ class CpdRunner { - public Map> run(CpdRunInputData runInputData) throws IOException { + public Map run(CpdRunInputData runInputData) throws IOException { validateRunInputData(runInputData); - Map> results = new HashMap<>(); + Map results = new HashMap<>(); for (Map.Entry> entry : runInputData.filesToScanPerLanguage.entrySet()) { String language = entry.getKey(); @@ -37,17 +36,17 @@ public Map> run(CpdRunInputData runInputData) throws IOEx continue; } List pathsToScan = filesToScan.stream().map(Paths::get).collect(Collectors.toList()); - List languageMatches = runLanguage(language, pathsToScan, runInputData.minimumTokens, runInputData.skipDuplicateFiles); + CpdLanguageRunResults languageRunResults = runLanguage(language, pathsToScan, runInputData.minimumTokens, runInputData.skipDuplicateFiles); - if (!languageMatches.isEmpty()) { - results.put(language, languageMatches); + if (!languageRunResults.matches.isEmpty() || !languageRunResults.processingErrors.isEmpty()) { + results.put(language, languageRunResults); } } return results; } - private List runLanguage(String language, List pathsToScan, int minimumTokens, boolean skipDuplicateFiles) throws IOException { + private CpdLanguageRunResults runLanguage(String language, List pathsToScan, int minimumTokens, boolean skipDuplicateFiles) throws IOException { // Note that the name "minimumTokens" comes from the public facing documentation and the cli but // behind the scenes, it maps to MinimumTileSize. To learn more about the mappings to the config, see: // https://github.com/pmd/pmd/blob/main/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/CpdCommand.java @@ -62,23 +61,27 @@ private List runLanguage(String language, List pathsToScan, int config.setSkipDuplicates(skipDuplicateFiles); config.setReporter(new CpdErrorListener()); - List cpdMatches = new ArrayList<>(); + CpdLanguageRunResults languageRunResults = new CpdLanguageRunResults(); try (CpdAnalysis cpd = CpdAnalysis.create(config)) { cpd.performAnalysis(report -> { - for (Report.ProcessingError processingError : report.getProcessingErrors()) { - // We don't expect any processing errors, but if there are any, then we can push them - // to stdOut so that they ultimately get logged. But we should continue as normal here. - System.out.println("Unexpected CPD processing error: " + processingError.getError().getMessage()); + + for (Report.ProcessingError reportProcessingError : report.getProcessingErrors()) { + CpdLanguageRunResults.ProcessingError processingErr = new CpdLanguageRunResults.ProcessingError(); + processingErr.file = reportProcessingError.getFileId().getAbsolutePath(); + processingErr.message = reportProcessingError.getMsg(); + processingErr.detail = reportProcessingError.getDetail(); + languageRunResults.processingErrors.add(processingErr); } + for (Match match : report.getMatches()) { - CpdMatch cpdMatch = new CpdMatch(); + CpdLanguageRunResults.Match cpdMatch = new CpdLanguageRunResults.Match(); cpdMatch.numBlocks = match.getMarkCount(); cpdMatch.numTokensInBlock = match.getTokenCount(); cpdMatch.numNonemptyLinesInBlock = match.getLineCount(); for (Mark mark : match.getMarkSet()) { - CpdMatch.BlockLocation blockLocation = new CpdMatch.BlockLocation(); + CpdLanguageRunResults.Match.BlockLocation blockLocation = new CpdLanguageRunResults.Match.BlockLocation(); FileLocation location = mark.getLocation(); blockLocation.file = location.getFileId().getAbsolutePath(); blockLocation.startLine = location.getStartLine(); @@ -89,12 +92,12 @@ private List runLanguage(String language, List pathsToScan, int cpdMatch.blockLocations.add(blockLocation); } - cpdMatches.add(cpdMatch); + languageRunResults.matches.add(cpdMatch); } }); } - return cpdMatches; + return languageRunResults; } private void validateRunInputData(CpdRunInputData runInputData) { diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdWrapper.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdWrapper.java similarity index 74% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdWrapper.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdWrapper.java index 3ab67ff3..6481146d 100644 --- a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdWrapper.java +++ b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/cpdwrapper/CpdWrapper.java @@ -4,7 +4,6 @@ import java.io.FileWriter; import java.io.IOException; import java.util.Arrays; -import java.util.List; import java.util.Map; import com.google.gson.Gson; @@ -29,24 +28,27 @@ * - {resultsOutputFile} is a JSON file to write CPD results to. * Example: * { - * "apex": [ - * { - * "numTokensInBlock": 18, - * "numNonemptyLinesInBlock": 5, - * "numBlocks": 2, - * "blockLocations": [ - * { - * "file": "/full/path/to/file1.cls", - * "startLine": 1, "startCol": 1, "endLine": 5, "endCol": 2 - * }, - * { - * "file": "/full/path/to/file2.cls", - * "startLine": 18, "startCol": 6, "endLine": 22, "endCol": 8 - * } - * ] - * }, - * ... - * ], + * "apex": { + * matches: [ + * { + * "numTokensInBlock": 18, + * "numNonemptyLinesInBlock": 5, + * "numBlocks": 2, + * "blockLocations": [ + * { + * "file": "/full/path/to/file1.cls", + * "startLine": 1, "startCol": 1, "endLine": 5, "endCol": 2 + * }, + * { + * "file": "/full/path/to/file2.cls", + * "startLine": 18, "startCol": 6, "endLine": 22, "endCol": 8 + * } + * ] + * }, + * ... + * ], + * processingErrors: [] + * }, * "xml": ... * } */ @@ -84,7 +86,7 @@ private static void invokeRunCommand(String[] args) { } CpdRunner cpdRunner = new CpdRunner(); - Map> results; + Map results; try { results = cpdRunner.run(inputData); } catch (Exception e) { diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriber.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriber.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriber.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriber.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleInfo.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleInfo.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleInfo.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleInfo.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleRunner.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleRunner.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleRunner.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdRuleRunner.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdWrapper.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdWrapper.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/pmdwrapper/PmdWrapper.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/main/java/com/salesforce/sfca/pmdwrapper/PmdWrapper.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/cpdwrapper/CpdWrapperTest.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/cpdwrapper/CpdWrapperTest.java similarity index 74% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/cpdwrapper/CpdWrapperTest.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/cpdwrapper/CpdWrapperTest.java index 012eafdb..bb57f6c7 100644 --- a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/cpdwrapper/CpdWrapperTest.java +++ b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/cpdwrapper/CpdWrapperTest.java @@ -224,87 +224,93 @@ void whenCallingRunWithValidFilesThatHaveDuplicates_thenJsonOutputShouldContainR String resultsJsonString = new String(Files.readAllBytes(Paths.get(outputFile))); String expectedOutput = "{\n" + - " \"apex\": [\n" + - " {\n" + - " \"numTokensInBlock\": 12,\n" + - " \"numNonemptyLinesInBlock\": 4,\n" + - " \"numBlocks\": 2,\n" + - " \"blockLocations\": [\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(apexFile1) + "\",\n" + - " \"startLine\": 2,\n" + - " \"startCol\": 17,\n" + - " \"endLine\": 5,\n" + - " \"endCol\": 2\n" + - " },\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(apexFile2) + "\",\n" + - " \"startLine\": 2,\n" + - " \"startCol\": 10,\n" + - " \"endLine\": 6,\n" + - " \"endCol\": 2\n" + - " }\n" + - " ]\n" + - " }\n" + - " ],\n" + - " \"ecmascript\": [\n" + - " {\n" + - " \"numTokensInBlock\": 36,\n" + - " \"numNonemptyLinesInBlock\": 10,\n" + - " \"numBlocks\": 2,\n" + - " \"blockLocations\": [\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(jsFile1) + "\",\n" + - " \"startLine\": 1,\n" + - " \"startCol\": 14,\n" + - " \"endLine\": 10,\n" + - " \"endCol\": 2\n" + - " },\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(jsFile2) + "\",\n" + - " \"startLine\": 1,\n" + - " \"startCol\": 14,\n" + - " \"endLine\": 10,\n" + - " \"endCol\": 2\n" + - " }\n" + - " ]\n" + - " },\n" + - " {\n" + - " \"numTokensInBlock\": 13,\n" + - " \"numNonemptyLinesInBlock\": 4,\n" + - " \"numBlocks\": 4,\n" + - " \"blockLocations\": [\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(jsFile1) + "\",\n" + - " \"startLine\": 1,\n" + - " \"startCol\": 15,\n" + - " \"endLine\": 4,\n" + - " \"endCol\": 2\n" + - " },\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(jsFile1) + "\",\n" + - " \"startLine\": 6,\n" + - " \"startCol\": 10,\n" + - " \"endLine\": 9,\n" + - " \"endCol\": 4\n" + - " },\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(jsFile2) + "\",\n" + - " \"startLine\": 1,\n" + - " \"startCol\": 15,\n" + - " \"endLine\": 4,\n" + - " \"endCol\": 2\n" + - " },\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(jsFile2) + "\",\n" + - " \"startLine\": 6,\n" + - " \"startCol\": 10,\n" + - " \"endLine\": 9,\n" + - " \"endCol\": 4\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + + " \"apex\": {" + + " \"matches\": [\n" + + " {\n" + + " \"numTokensInBlock\": 12,\n" + + " \"numNonemptyLinesInBlock\": 4,\n" + + " \"numBlocks\": 2,\n" + + " \"blockLocations\": [\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(apexFile1) + "\",\n" + + " \"startLine\": 2,\n" + + " \"startCol\": 17,\n" + + " \"endLine\": 5,\n" + + " \"endCol\": 2\n" + + " },\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(apexFile2) + "\",\n" + + " \"startLine\": 2,\n" + + " \"startCol\": 10,\n" + + " \"endLine\": 6,\n" + + " \"endCol\": 2\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"processingErrors\": []\n" + + " },\n" + + " \"ecmascript\": {" + + " \"matches\": [\n" + + " {\n" + + " \"numTokensInBlock\": 36,\n" + + " \"numNonemptyLinesInBlock\": 10,\n" + + " \"numBlocks\": 2,\n" + + " \"blockLocations\": [\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(jsFile1) + "\",\n" + + " \"startLine\": 1,\n" + + " \"startCol\": 14,\n" + + " \"endLine\": 10,\n" + + " \"endCol\": 2\n" + + " },\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(jsFile2) + "\",\n" + + " \"startLine\": 1,\n" + + " \"startCol\": 14,\n" + + " \"endLine\": 10,\n" + + " \"endCol\": 2\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"numTokensInBlock\": 13,\n" + + " \"numNonemptyLinesInBlock\": 4,\n" + + " \"numBlocks\": 4,\n" + + " \"blockLocations\": [\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(jsFile1) + "\",\n" + + " \"startLine\": 1,\n" + + " \"startCol\": 15,\n" + + " \"endLine\": 4,\n" + + " \"endCol\": 2\n" + + " },\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(jsFile1) + "\",\n" + + " \"startLine\": 6,\n" + + " \"startCol\": 10,\n" + + " \"endLine\": 9,\n" + + " \"endCol\": 4\n" + + " },\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(jsFile2) + "\",\n" + + " \"startLine\": 1,\n" + + " \"startCol\": 15,\n" + + " \"endLine\": 4,\n" + + " \"endCol\": 2\n" + + " },\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(jsFile2) + "\",\n" + + " \"startLine\": 6,\n" + + " \"startCol\": 10,\n" + + " \"endLine\": 9,\n" + + " \"endCol\": 4\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"processingErrors\": []\n" + + " }\n" + "}"; expectedOutput = expectedOutput.replaceAll("\\s+", ""); assertThat(resultsJsonString, is(expectedOutput)); @@ -363,29 +369,32 @@ void whenCallingRunWithTwoIdenticalFilesButSkipDuplicateFilesIsFalse_thenJsonOut String resultsJsonString = new String(Files.readAllBytes(Paths.get(outputFile))); String expectedOutput = "{\n" + - " \"apex\": [\n" + - " {\n" + - " \"numTokensInBlock\": 18,\n" + - " \"numNonemptyLinesInBlock\": 5,\n" + - " \"numBlocks\": 2,\n" + - " \"blockLocations\": [\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(apexFileInParentFolder) + "\",\n" + - " \"startLine\": 1,\n" + - " \"startCol\": 1,\n" + - " \"endLine\": 5,\n" + - " \"endCol\": 2\n" + - " },\n" + - " {\n" + - " \"file\": \"" + makePathJsonSafe(apexFileInSubFolder) + "\",\n" + - " \"startLine\": 1,\n" + - " \"startCol\": 1,\n" + - " \"endLine\": 5,\n" + - " \"endCol\": 2\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + + " \"apex\": {" + + " \"matches\": [\n" + + " {\n" + + " \"numTokensInBlock\": 18,\n" + + " \"numNonemptyLinesInBlock\": 5,\n" + + " \"numBlocks\": 2,\n" + + " \"blockLocations\": [\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(apexFileInParentFolder) + "\",\n" + + " \"startLine\": 1,\n" + + " \"startCol\": 1,\n" + + " \"endLine\": 5,\n" + + " \"endCol\": 2\n" + + " },\n" + + " {\n" + + " \"file\": \"" + makePathJsonSafe(apexFileInSubFolder) + "\",\n" + + " \"startLine\": 1,\n" + + " \"startCol\": 1,\n" + + " \"endLine\": 5,\n" + + " \"endCol\": 2\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"processingErrors\": []\n" + + " }\n" + "}"; expectedOutput = expectedOutput.replaceAll("\\s+", ""); diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriberTest.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriberTest.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriberTest.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/pmdwrapper/PmdRuleDescriberTest.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/pmdwrapper/PmdWrapperTest.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/pmdwrapper/PmdWrapperTest.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/pmdwrapper/PmdWrapperTest.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/pmdwrapper/PmdWrapperTest.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/testtools/StdOutCaptor.java b/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/testtools/StdOutCaptor.java similarity index 100% rename from packages/code-analyzer-pmd-engine/pmd-wrapper/src/test/java/com/salesforce/sfca/testtools/StdOutCaptor.java rename to packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/src/test/java/com/salesforce/sfca/testtools/StdOutCaptor.java diff --git a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdMatch.java b/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdMatch.java deleted file mode 100644 index 029bd25f..00000000 --- a/packages/code-analyzer-pmd-engine/pmd-wrapper/src/main/java/com/salesforce/sfca/cpdwrapper/CpdMatch.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.salesforce.sfca.cpdwrapper; - -import java.util.ArrayList; -import java.util.List; - -/** - * Java object to help us build cpd results that will be serializable to json format - * The data structure that we will serialize is Map> which will contain matches for each language. - */ -public class CpdMatch { - public int numTokensInBlock; - public int numNonemptyLinesInBlock; - public int numBlocks; - public List blockLocations = new ArrayList<>(); - - public static class BlockLocation { - public String file; - public int startLine; - public int startCol; - public int endLine; - public int endCol; - } -} \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/settings.gradle.kts b/packages/code-analyzer-pmd-engine/settings.gradle.kts index c9152907..aa389a60 100644 --- a/packages/code-analyzer-pmd-engine/settings.gradle.kts +++ b/packages/code-analyzer-pmd-engine/settings.gradle.kts @@ -1,2 +1,2 @@ rootProject.name = "code-analyzer-pmd-engine" -include("pmd-wrapper") +include("pmd-cpd-wrappers") diff --git a/packages/code-analyzer-pmd-engine/src/cpd-engine.ts b/packages/code-analyzer-pmd-engine/src/cpd-engine.ts index 39d65ba7..14e1b9fa 100644 --- a/packages/code-analyzer-pmd-engine/src/cpd-engine.ts +++ b/packages/code-analyzer-pmd-engine/src/cpd-engine.ts @@ -1,4 +1,5 @@ import { + CodeLocation, DescribeOptions, Engine, EngineRunResults, @@ -7,27 +8,45 @@ import { RuleType, RunOptions, SeverityLevel, + Violation, Workspace } from "@salesforce/code-analyzer-engine-api"; import {LanguageId} from "./constants"; import {getMessage} from "./messages"; -import {WorkspaceLiaison} from "./utils"; +import {indent, JavaCommandExecutor, WorkspaceLiaison} from "./utils"; import {CPD_AVAILABLE_LANGUAGES} from "./config"; +import { + CpdBlockLocation, + CpdLanguageRunResults, + CpdLanguageToFilesMap, + CpdMatch, + CpdRunInputData, + CpdRunResults, + CpdWrapperInvoker +} from "./cpd-wrapper"; const RULE_NAME_PREFIX: string = 'DetectCopyPasteFor'; export class CpdEngine extends Engine { static readonly NAME: string = "cpd"; - private readonly selectedLanguages: LanguageId[] + private readonly cpdWrapperInvoker: CpdWrapperInvoker; + private readonly selectedLanguages: LanguageId[]; + private readonly minimumTokens: number; + private readonly skipDuplicateFiles: boolean; private workspaceLiaisonCache: Map = new Map(); constructor() { super(); + const javaCommandExecutor: JavaCommandExecutor = new JavaCommandExecutor('java'); // TODO: Soon java_command will be configurable + this.cpdWrapperInvoker = new CpdWrapperInvoker(javaCommandExecutor, + (logLevel: LogLevel, message: string) => this.emitLogEvent(logLevel, message)); // We may pass this into the construct as a configurable option in the near future, at which point we'll need to decide on which languages to keep as default. this.selectedLanguages = CPD_AVAILABLE_LANGUAGES as LanguageId[]; // Using all languages for now + this.minimumTokens = 100; // Will be configurable soon + this.skipDuplicateFiles = false; // Will be configurable soon } getName(): string { @@ -40,10 +59,46 @@ export class CpdEngine extends Engine { return relevantLanguages.map(createRuleForLanguage); } - async runRules(ruleNames: string[], _runOptions: RunOptions): Promise { - this.emitLogEvent(LogLevel.Warn, `The '${CpdEngine.NAME}' engine's ability to run rules has not been implemented yet, so the following rules did not run: ${JSON.stringify(ruleNames)}`); + async runRules(ruleNames: string[], runOptions: RunOptions): Promise { + const workspaceLiaison: WorkspaceLiaison = this.getWorkspaceLiaison(runOptions.workspace); + const relevantLanguageToFilesMap: Map = await workspaceLiaison.getRelevantLanguageToFilesMap(); + + const filesToScanPerLanguage: CpdLanguageToFilesMap = {}; + for (const languageId of ruleNames.map(getLanguageFromRuleName)) { + if (relevantLanguageToFilesMap.has(languageId)) { + // Calling toCpdLanguage is needed to convert the LanguageId to the identifier that CPD recognizes + filesToScanPerLanguage[toCpdLanguage(languageId)] = relevantLanguageToFilesMap.get(languageId)!; + } + } + + if (Object.keys(filesToScanPerLanguage).length == 0) { + return { violations: [] }; + } + + const inputData: CpdRunInputData = { + filesToScanPerLanguage: filesToScanPerLanguage, + minimumTokens: this.minimumTokens, + skipDuplicateFiles: this.skipDuplicateFiles + } + + const cpdRunResults: CpdRunResults = await this.cpdWrapperInvoker.invokeRunCommand(inputData); + + const violations: Violation[] = []; + for (const cpdLanguage in cpdRunResults) { + const languageId: LanguageId = toLanguageId(cpdLanguage); + const cpdLanguageRunResults: CpdLanguageRunResults = cpdRunResults[cpdLanguage]; + for (const cpdMatch of cpdLanguageRunResults.matches) { + violations.push(toViolation(languageId, cpdMatch)); + } + for (const cpdProcessingError of cpdLanguageRunResults.processingErrors) { + /* istanbul ignore next */ + this.emitLogEvent(LogLevel.Error, getMessage('PmdProcessingErrorForFile', cpdProcessingError.file, + indent(cpdProcessingError.message))); + } + } + return { - violations: [] + violations: violations }; } @@ -75,6 +130,44 @@ function makeFirstCharUpperCase(text: string): string { return text.charAt(0).toUpperCase() + text.slice(1); } +function getLanguageFromRuleName(ruleName: string): LanguageId { + const language: string = ruleName.slice(RULE_NAME_PREFIX.length).toLowerCase(); + if (!CPD_AVAILABLE_LANGUAGES.includes(language)) { + throw new Error(`Unexpected error: The rule '${ruleName}' does not map to a supported CPD language: ${JSON.stringify(CPD_AVAILABLE_LANGUAGES)}`); + } + return language as LanguageId; +} + function getCacheKey(workspace?: Workspace) { return workspace? workspace.getWorkspaceId() : process.cwd(); +} + +function toCpdLanguage(languageId: LanguageId): string { + // We must convert 'javascript' to 'ecmascript' since CPD actually uses 'ecmascript' as the identifier instead of 'javascript' + return languageId == LanguageId.JAVASCRIPT ? 'ecmascript' : languageId; +} + +function toLanguageId(cpdLanguage: string): LanguageId { + // We must convert 'ecmascript' back to 'javascrijpt' + return cpdLanguage == 'ecmascript' ? LanguageId.JAVASCRIPT : cpdLanguage as LanguageId; +} + +function toViolation(languageId: LanguageId, cpdMatch: CpdMatch): Violation { + return { + ruleName: getRuleNameFromLanguage(languageId), + message: getMessage('DetectCopyPasteForLanguageViolationMessage', + languageId, cpdMatch.numBlocks, cpdMatch.numTokensInBlock, cpdMatch.numNonemptyLinesInBlock), + primaryLocationIndex: 0, + codeLocations: cpdMatch.blockLocations.map(toCodeLocation) + } +} + +function toCodeLocation(blockLocation: CpdBlockLocation): CodeLocation { + return { + file: blockLocation.file, + startLine: blockLocation.startLine, + startColumn: blockLocation.startCol, + endLine: blockLocation.endLine, + endColumn: blockLocation.endCol + } } \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/src/cpd-wrapper.ts b/packages/code-analyzer-pmd-engine/src/cpd-wrapper.ts new file mode 100644 index 00000000..bda02f06 --- /dev/null +++ b/packages/code-analyzer-pmd-engine/src/cpd-wrapper.ts @@ -0,0 +1,87 @@ +import {createTempDir, JavaCommandExecutor} from "./utils"; +import {LogLevel} from "@salesforce/code-analyzer-engine-api"; +import path from "node:path"; +import fs from "node:fs"; +import {getMessage} from "./messages"; + +const CPD_WRAPPER_JAVA_CLASS: string = "com.salesforce.sfca.cpdwrapper.CpdWrapper"; +const CPD_WRAPPER_LIB_FOLDER: string = path.resolve(__dirname, '..', 'dist', 'pmd-cpd-wrappers', 'lib'); + +export type CpdRunInputData = { + filesToScanPerLanguage: CpdLanguageToFilesMap, + minimumTokens: number, + skipDuplicateFiles: boolean +} +export type CpdLanguageToFilesMap = { // JSON.stringify doesn't support maps, so we can't just use Map + [language: string]: string[] +} + +export type CpdRunResults = { + [language: string]: CpdLanguageRunResults +} +export type CpdLanguageRunResults = { + matches: CpdMatch[], + processingErrors: CpdProcessingError[] +} +export type CpdMatch = { + numTokensInBlock: number, + numNonemptyLinesInBlock: number, + numBlocks: number, + blockLocations: CpdBlockLocation[] +} +export type CpdBlockLocation = { + file: string, + startLine: number, + startCol: number, + endLine: number, + endCol: number +} +export type CpdProcessingError = { + file: string, + message: string, + detail: string +} + +export class CpdWrapperInvoker { + private readonly javaCommandExecutor: JavaCommandExecutor; + private temporaryWorkingDir?: string; + private emitLogEvent: (logLevel: LogLevel, message: string) => void; + + constructor(javaCommandExecutor: JavaCommandExecutor, emitLogEvent: (logLevel: LogLevel, message: string) => void) { + this.javaCommandExecutor = javaCommandExecutor; + this.emitLogEvent = emitLogEvent; + } + + async invokeRunCommand(inputData: CpdRunInputData): Promise { + const tempDir: string = await this.getTemporaryWorkingDir(); + + const inputFile: string = path.join(tempDir, 'cpdRunInput.json'); + await fs.promises.writeFile(inputFile, JSON.stringify(inputData), 'utf-8'); + + const outputFile: string = path.join(tempDir, 'cpdRunOutput.json'); + + const javaCmdArgs: string[] = [CPD_WRAPPER_JAVA_CLASS, 'run', inputFile, outputFile]; + const javaClassPaths: string[] = [ + path.join(CPD_WRAPPER_LIB_FOLDER, '*'), + ]; + this.emitLogEvent(LogLevel.Fine, `Calling JAVA command with class path containing ${JSON.stringify(javaClassPaths)} and arguments: ${JSON.stringify(javaCmdArgs)}`); + await this.javaCommandExecutor.exec(javaCmdArgs, javaClassPaths, (stdOutMsg: string) => { + this.emitLogEvent(LogLevel.Fine, `[JAVA StdOut]: ${stdOutMsg}`); + }); + + try { + const resultsFileContents: string = await fs.promises.readFile(outputFile, 'utf-8'); + return JSON.parse(resultsFileContents); + } catch (err) /* istanbul ignore next */ { + const errMsg: string = err instanceof Error ? err.message : String(err); + throw new Error(getMessage('ErrorParsingOutputFile', outputFile, errMsg), {cause: err}); + } + } + + private async getTemporaryWorkingDir(): Promise { + if (this.temporaryWorkingDir === undefined) { + this.temporaryWorkingDir = await createTempDir(); + } + return this.temporaryWorkingDir!; + } +} \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/src/messages.ts b/packages/code-analyzer-pmd-engine/src/messages.ts index 24afbd29..d1faebc2 100644 --- a/packages/code-analyzer-pmd-engine/src/messages.ts +++ b/packages/code-analyzer-pmd-engine/src/messages.ts @@ -63,14 +63,17 @@ const MESSAGE_CATALOG : { [key: string]: string } = { InvalidJavaClasspathEntry: `The '%s' configuration value is invalid. The path must either be a '.jar' file or a folder.`, - ErrorParsingPmdWrapperOutputFile: - `An internal error was thrown when trying to read the internal PmdWrapper output file '%s':\n%s'`, + ErrorParsingOutputFile: + `An internal error was thrown when trying to read the internal output file '%s':\n%s'`, PmdProcessingErrorForFile: `PMD issued a processing error for file '%s':\n%s`, DetectCopyPasteForLanguageRuleDescription: - `Identify duplicate code blocks within your workspace files associated with the '%s' language.` + `Identify duplicate code blocks within your workspace files associated with the '%s' language.`, + + DetectCopyPasteForLanguageViolationMessage: + `Duplicate code detected for language '%s'. Found %d code locations containing the same block of code consisting of %d tokens across %d lines.` } /** diff --git a/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts b/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts index e4a7b1bd..e5729083 100644 --- a/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts +++ b/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts @@ -5,7 +5,7 @@ import {getMessage} from "./messages"; import {LogLevel} from "@salesforce/code-analyzer-engine-api"; const PMD_WRAPPER_JAVA_CLASS: string = "com.salesforce.sfca.pmdwrapper.PmdWrapper"; -const PMD_WRAPPER_LIB_FOLDER: string = path.resolve(__dirname, '..', 'dist', 'pmd-wrapper', 'lib'); +const PMD_WRAPPER_LIB_FOLDER: string = path.resolve(__dirname, '..', 'dist', 'pmd-cpd-wrappers', 'lib'); export type PmdRuleInfo = { name: string, @@ -86,7 +86,7 @@ export class PmdWrapperInvoker { return pmdRuleInfoList; } catch (err) /* istanbul ignore next */ { const errMsg: string = err instanceof Error ? err.message : String(err); - throw new Error(getMessage('ErrorParsingPmdWrapperOutputFile', pmdRulesOutputFile, errMsg), {cause: err}); + throw new Error(getMessage('ErrorParsingOutputFile', pmdRulesOutputFile, errMsg), {cause: err}); } } @@ -131,11 +131,11 @@ export class PmdWrapperInvoker { } catch (err) /* istanbul ignore next */ { const errMsg: string = err instanceof Error ? err.message : String(err); - throw new Error(getMessage('ErrorParsingPmdWrapperOutputFile', resultsOutputFile, errMsg), {cause: err}); + throw new Error(getMessage('ErrorParsingOutputFile', resultsOutputFile, errMsg), {cause: err}); } } - async getTemporaryWorkingDir(): Promise { + private async getTemporaryWorkingDir(): Promise { if (this.temporaryWorkingDir === undefined) { this.temporaryWorkingDir = await createTempDir(); } diff --git a/packages/code-analyzer-pmd-engine/src/utils.ts b/packages/code-analyzer-pmd-engine/src/utils.ts index 6bf0051c..db21b9e0 100644 --- a/packages/code-analyzer-pmd-engine/src/utils.ts +++ b/packages/code-analyzer-pmd-engine/src/utils.ts @@ -64,14 +64,13 @@ export class JavaCommandExecutor { // noinspection JSMismatchedCollectionQueryUpdate (IntelliJ is confused about how I am setting the private values, suppressing warnings) export class WorkspaceLiaison { private readonly workspace?: Workspace; - private readonly selectedLanguages: LanguageId[]; + private readonly selectedLanguages: Set; - private relevantLanguages?: LanguageId[]; - private relevantFiles?: string[]; + private relevantLanguageToFilesMap?: Map; constructor(workspace: Workspace | undefined, selectedLanguages: LanguageId[]) { this.workspace = workspace; - this.selectedLanguages = selectedLanguages; + this.selectedLanguages = new Set(selectedLanguages); } getWorkspace(): Workspace | undefined { @@ -79,36 +78,37 @@ export class WorkspaceLiaison { } async getRelevantFiles(): Promise { - if (this.relevantFiles === undefined) { - await this.processWorkspace(); - } - return this.relevantFiles!; + return [... (await this.getRelevantLanguageToFilesMap()).values()].flat(); } async getRelevantLanguages(): Promise { - if (this.relevantLanguages === undefined) { - await this.processWorkspace(); - } - return this.relevantLanguages!; + return [...(await this.getRelevantLanguageToFilesMap()).keys()].sort(); } - private async processWorkspace(): Promise { - this.relevantFiles = []; + async getRelevantLanguageToFilesMap(): Promise> { + if (this.relevantLanguageToFilesMap) { + return this.relevantLanguageToFilesMap; + } if (!this.workspace) { - this.relevantLanguages = [... this.selectedLanguages].sort(); - return; + this.relevantLanguageToFilesMap = new Map([...this.selectedLanguages].map(lang => [lang, []])); + return this.relevantLanguageToFilesMap; } - const relevantLanguagesSet: Set = new Set(); - for (const file of await this.workspace.getExpandedFiles()) { + const files: string[] = await this.workspace.getExpandedFiles(); + this.relevantLanguageToFilesMap = new Map(); + + for (const file of files) { const fileExt: string = path.extname(file).toLowerCase(); const lang: LanguageId | undefined = extensionToLanguageId[fileExt]; - if (lang && this.selectedLanguages.includes(lang)) { - this.relevantFiles.push(file); - relevantLanguagesSet.add(lang); + if (!lang || !this.selectedLanguages.has(lang)) { + continue; + } + if(!this.relevantLanguageToFilesMap.has(lang)) { + this.relevantLanguageToFilesMap.set(lang,[]); } + this.relevantLanguageToFilesMap.get(lang)!.push(file); } - this.relevantLanguages = [...relevantLanguagesSet].sort(); + return this.relevantLanguageToFilesMap; } } diff --git a/packages/code-analyzer-pmd-engine/test/cpd-engine.test.ts b/packages/code-analyzer-pmd-engine/test/cpd-engine.test.ts index 19e8e185..a451b98f 100644 --- a/packages/code-analyzer-pmd-engine/test/cpd-engine.test.ts +++ b/packages/code-analyzer-pmd-engine/test/cpd-engine.test.ts @@ -1,5 +1,5 @@ import {changeWorkingDirectoryToPackageRoot} from "./test-helpers"; -import {RuleDescription, Workspace} from "@salesforce/code-analyzer-engine-api"; +import {EngineRunResults, RuleDescription, Violation, Workspace} from "@salesforce/code-analyzer-engine-api"; import {CpdEngine} from "../src/cpd-engine"; import fs from "node:fs"; import path from "node:path"; @@ -28,7 +28,7 @@ describe('Tests for the describeRules method of PmdEngine', () => { it('When using defaults with workspace that only contains apex code, then only apex rule is returned', async () => { const engine: CpdEngine = new CpdEngine(); const workspace: Workspace = new Workspace([ - path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.cls') + path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls') ]); const ruleDescriptions: RuleDescription[] = await engine.describeRules({workspace: workspace}); @@ -44,9 +44,120 @@ async function expectRulesToMatchGoldFile(actualRuleDescriptions: RuleDescriptio } describe('Tests for the runRules method of CpdEngine', () => { - it('TEMPORARY TEST FOR CODE COVERAGE', async () => { - // Will delete this test as soon as we implement the CpdEngine. + it('When zero rules names are provided then return zero violations', async () => { const engine: CpdEngine = new CpdEngine(); expect(await engine.runRules([],{workspace: new Workspace([__dirname])})).toEqual({violations: []}); }); + + it('When rule name is not associated with a language that CPD knows about, then throw error', async () => { + const engine: CpdEngine = new CpdEngine(); + await expect(engine.runRules(['DetectCopyPasteForOops'], {workspace: new Workspace([__dirname])})).rejects.toThrow( + /Unexpected error: The rule 'DetectCopyPasteForOops' does not map to a supported CPD language:.*/); + }); + + it('When specified rules are not relevant to users workspace, then return zero violations', async () => { + const engine: CpdEngine = new CpdEngine(); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls')]); + const ruleNames: string[] = ['DetectCopyPasteForHtml']; + const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); + + expect(results.violations).toHaveLength(0); + }); + + it('When specified rules contain relevant files containing no duplicate blocks using the default minimumToken value, then return zero violations', async () => { + const engine: CpdEngine = new CpdEngine(); + const workspace: Workspace = new Workspace([ + path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'sampleJavascript1_ItselfContainsDuplicateBlocksButWithVeryFewTokens.js'), + path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'sampleJavascript2_ContainsNearlyAllTheSameTokensAsSampleJavascript1.js') // duplicate blocks are smaller than default 100 tokens + ]); + const ruleNames: string[] = ['DetectCopyPasteForJavascript']; + const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); + + expect(results.violations).toHaveLength(0); + }); + + it('When using defaults and workspace contains relevant files containing duplicate blocks, then return violations', async () => { + const engine: CpdEngine = new CpdEngine(); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace')]); + const ruleNames: string[] = ['DetectCopyPasteForApex', 'DetectCopyPasteForHtml', 'DetectCopyPasteForJavascript']; + const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); + + const expViolation1: Violation = { + ruleName: "DetectCopyPasteForHtml", + message: "Duplicate code detected for language 'html'. Found 2 code locations containing the same block of code consisting of 123 tokens across 43 lines.", + primaryLocationIndex: 0, + codeLocations: [ + { + file: path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'someReplicatedFileWithOver100Tokens.html'), + startLine: 1, + startColumn: 1, + endLine: 43, // This should be 44 - this might be a bug with CPD: https://github.com/pmd/pmd/issues/5313 + endColumn: 8 + }, + { + file: path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'subFolder', 'someReplicatedFileWithOver100Tokens.html'), + startLine: 1, + startColumn: 1, + endLine: 43, // This should be 44 - this might be a bug with CPD: https://github.com/pmd/pmd/issues/5313 + endColumn: 8 + } + ] + }; + + const expViolation2: Violation = { + ruleName: "DetectCopyPasteForApex", + message: "Duplicate code detected for language 'apex'. Found 2 code locations containing the same block of code consisting of 113 tokens across 27 lines.", + primaryLocationIndex: 0, + codeLocations: [ + { + file: path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls'), + startLine: 2, + startColumn: 34, + endLine: 28, + endColumn: 6 + }, + { + file: path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'ApexClass2_ContainsMoreThan100SameTokensAsApexClass1.cls'), + startLine: 2, + startColumn: 24, + endLine: 29, + endColumn: 4 + } + ] + }; + + const expViolation3: Violation = { + ruleName: "DetectCopyPasteForApex", + message: "Duplicate code detected for language 'apex'. Found 3 code locations containing the same block of code consisting of 104 tokens across 24 lines.", + primaryLocationIndex: 0, + codeLocations: [ + { + file: path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls'), + startLine: 5, + startColumn: 5, + endLine: 28, + endColumn: 6 + }, + { + file: path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls'), + startLine: 31, + startColumn: 9, + endLine: 54, + endColumn: 10 + }, + { + file: path.join(TEST_DATA_FOLDER, 'sampleCpdWorkspace', 'ApexClass2_ContainsMoreThan100SameTokensAsApexClass1.cls'), + startLine: 6, + startColumn: 3, + endLine: 29, + endColumn: 4 + } + ] + } + + expect(results.violations).toHaveLength(3); + expect(results.violations).toContainEqual(expViolation1); + expect(results.violations).toContainEqual(expViolation2); + expect(results.violations).toContainEqual(expViolation3); + }); }); \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/test/pmd-engine.test.ts b/packages/code-analyzer-pmd-engine/test/pmd-engine.test.ts index fb7cff34..6916172e 100644 --- a/packages/code-analyzer-pmd-engine/test/pmd-engine.test.ts +++ b/packages/code-analyzer-pmd-engine/test/pmd-engine.test.ts @@ -61,7 +61,7 @@ describe('Tests for the describeRules method of PmdEngine', () => { it('When using defaults with workspace containing only apex code, then only apex rules are returned', async () => { const engine: PmdEngine = new PmdEngine(DEFAULT_PMD_ENGINE_CONFIG); const workspace: Workspace = new Workspace([ - path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.cls') + path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.cls') ]); const ruleDescriptions: RuleDescription[] = await engine.describeRules({workspace: workspace}); await expectRulesToMatchGoldFile(ruleDescriptions, 'rules_apexOnly.goldfile.json'); @@ -70,8 +70,8 @@ describe('Tests for the describeRules method of PmdEngine', () => { it('When using defaults with workspace containing only apex and xml code, then only apex rules are returned', async () => { const engine: PmdEngine = new PmdEngine(DEFAULT_PMD_ENGINE_CONFIG); const workspace: Workspace = new Workspace([ - path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.trigger'), - path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.xml') + path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.trigger'), + path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.xml') ]); const ruleDescriptions: RuleDescription[] = await engine.describeRules({workspace: workspace}); await expectRulesToMatchGoldFile(ruleDescriptions, 'rules_apexOnly.goldfile.json'); @@ -80,7 +80,7 @@ describe('Tests for the describeRules method of PmdEngine', () => { it('When using defaults with workspace containing only visualforce code, then only visualforce rules are returned', async () => { const engine: PmdEngine = new PmdEngine(DEFAULT_PMD_ENGINE_CONFIG); const workspace: Workspace = new Workspace([ - path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.page') + path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.page') ]); const ruleDescriptions: RuleDescription[] = await engine.describeRules({workspace: workspace}); await expectRulesToMatchGoldFile(ruleDescriptions, 'rules_visualforceOnly.goldfile.json'); @@ -89,7 +89,7 @@ describe('Tests for the describeRules method of PmdEngine', () => { it('When using defaults with workspace containing no supported files, then no rules are returned', async () => { const engine: PmdEngine = new PmdEngine(DEFAULT_PMD_ENGINE_CONFIG); const workspace: Workspace = new Workspace([ - path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.txt') + path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.txt') ]); const ruleDescriptions: RuleDescription[] = await engine.describeRules({workspace: workspace}); expect(ruleDescriptions).toHaveLength(0); @@ -126,7 +126,7 @@ describe('Tests for the describeRules method of PmdEngine', () => { rule_languages: ['javascript', 'xml' /* not in workspace */] }); const workspace: Workspace = new Workspace([ - path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.js') + path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.js') ]); const ruleDescriptions: RuleDescription[] = await engine.describeRules({workspace: workspace}); await expectRulesToMatchGoldFile(ruleDescriptions, 'rules_javascriptOnly.goldfile.json'); @@ -282,7 +282,7 @@ describe('Tests for the runRules method of PmdEngine', () => { message: 'Avoid operations in loops that may hit governor limits', codeLocations: [ { - file: path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'sampleViolations', 'OperationWithLimitsInLoop.cls'), + file: path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'OperationWithLimitsInLoop.cls'), startLine: 4, startColumn: 38, endLine: 4, @@ -297,7 +297,7 @@ describe('Tests for the runRules method of PmdEngine', () => { message: 'Avoid unescaped user controlled content in EL', codeLocations: [ { - file: path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'sampleViolations', 'VfUnescapeEl.page'), + file: path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'VfUnescapeEl.page'), startLine: 3, startColumn: 19, endLine: 3, @@ -312,7 +312,7 @@ describe('Tests for the runRules method of PmdEngine', () => { message: 'Avoid unescaped user controlled content in EL', codeLocations: [ { - file: path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'sampleViolations', 'VfUnescapeEl.page'), + file: path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'VfUnescapeEl.page'), startLine: 5, startColumn: 19, endLine: 5, @@ -327,7 +327,7 @@ describe('Tests for the runRules method of PmdEngine', () => { message: 'A function should not mix return statements with and without a result.', codeLocations: [ { - file: path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'sampleViolations', 'ConsistentReturn.js'), + file: path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'ConsistentReturn.js'), startLine: 1, startColumn: 1, endLine: 6, @@ -342,7 +342,7 @@ describe('Tests for the runRules method of PmdEngine', () => { message: 'Avoid using while statements without curly braces', codeLocations: [ { - file: path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'sampleViolations', 'WhileLoopsMustUseBraces.js'), + file: path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'WhileLoopsMustUseBraces.js'), startLine: 2, startColumn: 1, endLine: 3, @@ -357,7 +357,7 @@ describe('Tests for the runRules method of PmdEngine', () => { message: 'Avoid debug statements since they impact on performance', codeLocations: [ { - file: path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls'), + file: path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls'), startLine: 4, startColumn: 9, endLine: 4, @@ -372,7 +372,7 @@ describe('Tests for the runRules method of PmdEngine', () => { message: 'Avoid debug statements since they impact on performance', codeLocations: [ { - file: path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls'), + file: path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls'), startLine: 4, startColumn: 9, endLine: 4, @@ -385,7 +385,7 @@ describe('Tests for the runRules method of PmdEngine', () => { it('When zero rule names are provided then return zero violations', async () => { const engine: PmdEngine = new PmdEngine(DEFAULT_PMD_ENGINE_CONFIG); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace')]); const results: EngineRunResults = await engine.runRules([], {workspace: workspace}); expect(results.violations).toHaveLength(0); }); @@ -395,7 +395,7 @@ describe('Tests for the runRules method of PmdEngine', () => { const progressEvents: RunRulesProgressEvent[] = []; engine.onEvent(EventType.RunRulesProgressEvent, (e: RunRulesProgressEvent) => progressEvents.push(e)); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.xml')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.xml')]); const ruleNames: string[] = ['OperationWithLimitsInLoop', 'VfUnescapeEl']; const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); @@ -410,7 +410,7 @@ describe('Tests for the runRules method of PmdEngine', () => { ...DEFAULT_PMD_ENGINE_CONFIG, rule_languages: ['xml'] }); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace', 'dummy.xml')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'dummy.xml')]); const ruleNames: string[] = ['OperationWithLimitsInLoop', 'VfUnescapeEl']; const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); @@ -424,7 +424,7 @@ describe('Tests for the runRules method of PmdEngine', () => { const progressEvents: RunRulesProgressEvent[] = []; engine.onEvent(EventType.RunRulesProgressEvent, (e: RunRulesProgressEvent) => progressEvents.push(e)); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace')]); const ruleNames: string[] = ['OperationWithLimitsInLoop', 'VfUnescapeEl']; const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); @@ -445,7 +445,7 @@ describe('Tests for the runRules method of PmdEngine', () => { it('When a single rule is selected, then return only violations for that rule', async () => { const engine: PmdEngine = new PmdEngine(DEFAULT_PMD_ENGINE_CONFIG); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace')]); const ruleNames: string[] = ['OperationWithLimitsInLoop']; const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); expect(results.violations).toHaveLength(1); @@ -454,7 +454,7 @@ describe('Tests for the runRules method of PmdEngine', () => { it('When selected rules are not violated, then return zero violations', async () => { const engine: PmdEngine = new PmdEngine(DEFAULT_PMD_ENGINE_CONFIG); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace')]); const ruleNames: string[] = ['WhileLoopsMustUseBraces', 'ExcessiveParameterList', 'VfCsrf']; const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); expect(results.violations).toHaveLength(0); @@ -465,7 +465,7 @@ describe('Tests for the runRules method of PmdEngine', () => { ... DEFAULT_PMD_ENGINE_CONFIG, rule_languages: ['javascript', 'xml' /* sanity check: not relevant to workspace */, 'apex'] }); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace')]); const ruleNames: string[] = ['ConsistentReturn', 'WhileLoopsMustUseBraces-javascript', 'MissingEncoding' /* sanity check: not relevant to workspace */, 'OperationWithLimitsInLoop']; const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); expect(results.violations).toHaveLength(3); @@ -485,7 +485,7 @@ describe('Tests for the runRules method of PmdEngine', () => { path.join(TEST_DATA_FOLDER, 'custom rules', 'somecat3.xml') ] }); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleWorkspace')]); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace')]); const ruleNames: string[] = ['fakerule1', 'fakerule2', 'fakerule7', 'fakerule8']; const results: EngineRunResults = await engine.runRules(ruleNames, {workspace: workspace}); expect(results.violations).toHaveLength(2); // Expecting fakerule1 and fakerule7 (which both have a definition equivalent to the AvoidDebugStatements rule) diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls new file mode 100644 index 00000000..4d088c28 --- /dev/null +++ b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens.cls @@ -0,0 +1,59 @@ +global class ApexClass1_ItselfContainsDuplicateBlocksOfMoreThan100Tokens { + global static void insertObj1(Account account) { + insert account; + } + global static void insertObj2(Object2 obj2) { + insert obj2; + } + global static void insertObj3(Object3 obj3) { + insert obj3; + } + global static void insertObj4(Object4 obj4) { + insert obj4; + } + global static void insertObj5(Object5 obj5) { + insert obj5; + } + global static void insertObj6(Object6 obj6) { + insert obj6; + } + global static void insertObj7(Object7 obj7) { + insert obj7; + } + global static void insertObj8(Object8 obj8) { + insert obj8; + } + global static void insertObj9(Object9 obj9) { + insert obj9; + } + + public class InnerClass { + global static void insertObj2(Object2 obj2) { + insert obj2; + } + global static void insertObj3(Object3 obj3) { + insert obj3; + } + global static void insertObj4(Object4 obj4) { + insert obj4; + } + global static void insertObj5(Object5 obj5) { + insert obj5; + } + global static void insertObj6(Object6 obj6) { + insert obj6; + } + global static void insertObj7(Object7 obj7) { + insert obj7; + } + global static void insertObj8(Object8 obj8) { + insert obj8; + } + global static void insertObj9(Object9 obj9) { + insert obj9; + } + global static void insertObj10(Object10 obj10) { + insert obj10; + } + } +} \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/ApexClass2_ContainsMoreThan100SameTokensAsApexClass1.cls b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/ApexClass2_ContainsMoreThan100SameTokensAsApexClass1.cls new file mode 100644 index 00000000..ec1a4af0 --- /dev/null +++ b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/ApexClass2_ContainsMoreThan100SameTokensAsApexClass1.cls @@ -0,0 +1,30 @@ +global class ApexClass2_ContainsMoreThan100SameTokensAsApexClass1 { + public void insertObj(Account account) { + insert account; + } + + global static void insertObj2(Object2 obj2) { + insert obj2; + } + global static void insertObj3(Object3 obj3) { + insert obj3; + } + global static void insertObj4(Object4 obj4) { + insert obj4; + } + global static void insertObj5(Object5 obj5) { + insert obj5; + } + global static void insertObj6(Object6 obj6) { + insert obj6; + } + global static void insertObj7(Object7 obj7) { + insert obj7; + } + global static void insertObj8(Object8 obj8) { + insert obj8; + } + global static void insertObj9(Object9 obj9) { + insert obj9; + } +} \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.txt b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/dummy.txt similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.txt rename to packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/dummy.txt diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/sampleJavascript1_ItselfContainsDuplicateBlocksButWithVeryFewTokens.js b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/sampleJavascript1_ItselfContainsDuplicateBlocksButWithVeryFewTokens.js new file mode 100644 index 00000000..ef1aa6bd --- /dev/null +++ b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/sampleJavascript1_ItselfContainsDuplicateBlocksButWithVeryFewTokens.js @@ -0,0 +1,10 @@ +function abc1() { + const a = 3; + const b = 4; +} +function abc2() { + if(true) { + const a = 3; + const b = 4; + } +} \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/sampleJavascript2_ContainsNearlyAllTheSameTokensAsSampleJavascript1.js b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/sampleJavascript2_ContainsNearlyAllTheSameTokensAsSampleJavascript1.js new file mode 100644 index 00000000..b01caca7 --- /dev/null +++ b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/sampleJavascript2_ContainsNearlyAllTheSameTokensAsSampleJavascript1.js @@ -0,0 +1,10 @@ +function abc3() { + const a = 3; + const b = 4; +} +function abc2() { + if(true) { + const a = 3; + const b = 4; + } +} \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/someReplicatedFileWithOver100Tokens.html b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/someReplicatedFileWithOver100Tokens.html new file mode 100644 index 00000000..0ef10b73 --- /dev/null +++ b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/someReplicatedFileWithOver100Tokens.html @@ -0,0 +1,44 @@ + + +
+ This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + hurray + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + okay + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder +
+ + \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/subFolder/someReplicatedFileWithOver100Tokens.html b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/subFolder/someReplicatedFileWithOver100Tokens.html new file mode 100644 index 00000000..0ef10b73 --- /dev/null +++ b/packages/code-analyzer-pmd-engine/test/test-data/sampleCpdWorkspace/subFolder/someReplicatedFileWithOver100Tokens.html @@ -0,0 +1,44 @@ + + +
+ This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + hurray + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + okay + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder + This file lives in the parent folder + And it lives in the sub folder +
+ + \ No newline at end of file diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.cls b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.cls similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.cls rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.cls diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.js b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.js similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.js rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.js diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.page b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.page similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.page rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.page diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.trigger b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.trigger similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.trigger rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.trigger diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.xml b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.txt similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/dummy.xml rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.txt diff --git a/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.xml b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/dummy.xml new file mode 100644 index 00000000..e69de29b diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/AvoidDebugStatements.cls b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/AvoidDebugStatements.cls similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/AvoidDebugStatements.cls rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/AvoidDebugStatements.cls diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/ConsistentReturn.js b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/ConsistentReturn.js similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/ConsistentReturn.js rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/ConsistentReturn.js diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/OperationWithLimitsInLoop.cls b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/OperationWithLimitsInLoop.cls similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/OperationWithLimitsInLoop.cls rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/OperationWithLimitsInLoop.cls diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/VfUnescapeEl.page b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/VfUnescapeEl.page similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/VfUnescapeEl.page rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/VfUnescapeEl.page diff --git a/packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/WhileLoopsMustUseBraces.js b/packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/WhileLoopsMustUseBraces.js similarity index 100% rename from packages/code-analyzer-pmd-engine/test/test-data/sampleWorkspace/sampleViolations/WhileLoopsMustUseBraces.js rename to packages/code-analyzer-pmd-engine/test/test-data/samplePmdWorkspace/sampleViolations/WhileLoopsMustUseBraces.js