Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/code-analyzer-pmd-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@salesforce/code-analyzer-pmd-engine",
"description": "Plugin package that adds 'pmd' and 'cpd' as engines into Salesforce Code Analyzer",
"version": "0.35.0",
"version": "0.36.0-SNAPSHOT",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,21 @@ public void logEx(Level level, @Nullable String s, Object[] objects, @Nullable T
}
}
throw new RuntimeException("PMD threw an unexpected exception:\n" + message, throwable);
} else if (level == Level.WARN && s != null){
// PMD sometimes logs deprecation notices as WARN without a Throwable.
// Example: "Discontinue using Rule category/... as it is scheduled for removal..."
// We surface these to stdout with a [Warning] marker so callers can display them,
// but we do not fail the operation.
String message = MessageFormat.format(s, objects);
if (message.contains("Discontinue using Rule ")) {
System.out.println("[Warning] " + message.replaceAll("\n","{NEWLINE}"));
} else {
// Any other WARN without a Throwable is unexpected in our workflows; treat as fatal
// so configuration/environment issues are not silently ignored.
throw new RuntimeException("PMD threw an unexpected exception:\n" + message);
}
} else if (s != null) {
// Non-WARN messages without a Throwable are unexpected; fail fast to aid diagnosis.
String message = MessageFormat.format(s, objects);
throw new RuntimeException("PMD threw an unexpected exception:\n" + message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,70 @@ void whenCallingMainWithDescribeWithCustomRulesetsFile_thenRulesetsAreApplied(@T
assertThat(ruleInfo2.ruleSetFile, is(sampleRulesetFile2.toAbsolutePath().toString()));
}

@Test
void whenCallingMainWithDescribeWithNcssCountRuleset_thenRuleAppearsInDescribe(@TempDir Path tempDir) throws Exception {
// Create a minimal ruleset that references Apex NcssCount (metrics)
String rulesetXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<ruleset name=\"Ruleset for NcssCount\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd\">\n" +
" <description>Include Apex NcssCount</description>\n" +
" <rule ref=\"category/apex/design.xml/NcssCount\" />\n" +
"</ruleset>";
Path ncssRuleset = tempDir.resolve("ncss-ruleset.xml");
Files.write(ncssRuleset, rulesetXml.getBytes());

// Prepare describe args with a custom rulesets list file
Path outputFile = tempDir.resolve("describe-output.json");
Path rulesetsList = tempDir.resolve("customRulesetsList.txt");
Files.write(rulesetsList, (ncssRuleset.toAbsolutePath().toString() + "\n").getBytes());

String[] args = {"describe", outputFile.toAbsolutePath().toString(),
rulesetsList.toAbsolutePath().toString(), "apex"};
callPmdWrapper(args);

// Parse output and assert NcssCount is present and references our ruleset file
String fileContents = Files.readString(outputFile);
Gson gson = new Gson();
Type pmdRuleInfoListType = new TypeToken<List<PmdRuleInfo>>(){}.getType();
List<PmdRuleInfo> pmdRuleInfoList = gson.fromJson(fileContents, pmdRuleInfoListType);
PmdRuleInfo ruleInfo = assertContainsOneRuleWithNameAndLanguage(pmdRuleInfoList, "NcssCount", "apex");
assertThat(ruleInfo.ruleSetFile, is(ncssRuleset.toAbsolutePath().toString()));
}

@Test
void whenCallingMainWithDescribeWithExcessiveClassLengthRuleset_thenRuleAppearsInDescribe(@TempDir Path tempDir) throws Exception {
// Create a minimal ruleset that references Apex ExcessiveClassLength (design)
String rulesetXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<ruleset name=\"Ruleset for ExcessiveClassLength\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd\">\n" +
" <description>Include Apex ExcessiveClassLength</description>\n" +
" <rule ref=\"category/apex/design.xml/ExcessiveClassLength\" />\n" +
"</ruleset>";
Path excessiveClassLengthRuleset = tempDir.resolve("excessive-class-length-ruleset.xml");
Files.write(excessiveClassLengthRuleset, rulesetXml.getBytes());

// Prepare describe args with a custom rulesets list file
Path outputFile = tempDir.resolve("describe-output.json");
Path rulesetsList = tempDir.resolve("customRulesetsList.txt");
Files.write(rulesetsList, (excessiveClassLengthRuleset.toAbsolutePath().toString() + "\n").getBytes());

String[] args = {"describe", outputFile.toAbsolutePath().toString(),
rulesetsList.toAbsolutePath().toString(), "apex"};
callPmdWrapper(args);

// Parse output and assert ExcessiveClassLength is present and references our ruleset file
String fileContents = Files.readString(outputFile);
Gson gson = new Gson();
Type pmdRuleInfoListType = new TypeToken<List<PmdRuleInfo>>(){}.getType();
List<PmdRuleInfo> pmdRuleInfoList = gson.fromJson(fileContents, pmdRuleInfoListType);
PmdRuleInfo ruleInfo = assertContainsOneRuleWithNameAndLanguage(pmdRuleInfoList, "ExcessiveClassLength", "apex");
assertThat(ruleInfo.ruleSetFile, is(excessiveClassLengthRuleset.toAbsolutePath().toString()));
}

@Test
void whenCallingMainWithRunAndTwoFewArgs_thenError() {
String[] args = {"run", "notEnough"};
Expand Down Expand Up @@ -373,6 +437,97 @@ void whenCallingRunWithAnInvalidApexFileWithValidApexFile_thenSkipInvalidApexFil
assertThat(resultsJsonString, containsString("\"processingErrors\":[{\"file\":")); // Contains the processing error for the invalid file
}

@Test
void whenRunningWithNcssCountRule_thenReportsOrExecutesSuccessfully(@TempDir Path tempDir) throws Exception {
// Create a minimal ruleset that references the Apex NcssCount rule with a very low threshold
String rulesetXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<ruleset name=\"Ruleset for NcssCount\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd\">\n" +
" <description>Run Apex NcssCount</description>\n" +
" <rule ref=\"category/apex/design.xml/NcssCount\">\n" +
" <properties>\n" +
" <property name=\"minimum\" value=\"1\"/>\n" +
" </properties>\n" +
" </rule>\n" +
"</ruleset>";
String rulesetFile = createTempFile(tempDir, "ncss-ruleset.xml", rulesetXml);

// Create a simple Apex file with a few statements; the low threshold should trigger a violation
String apexCode = "public class ManyStatements {\n" +
" public static void foo(){\n" +
" Integer a = 1; Integer b = 2; Integer c = 3; // multiple statements\n" +
" }\n" +
"}\n";
String apexFile = createTempFile(tempDir, "ManyStatements.cls", apexCode);

String inputJson = "{\n" +
" \"ruleSetInputFile\":\"" + makePathJsonSafe(rulesetFile) + "\",\n" +
" \"runDataPerLanguage\": {\n" +
" \"apex\": {\n" +
" \"filesToScan\": [\"" + makePathJsonSafe(apexFile) + "\"]\n" +
" }\n" +
" }\n" +
"}";
String inputFile = createTempFile(tempDir, "input.json", inputJson);

String resultsOutputFile = tempDir.resolve("results.json").toAbsolutePath().toString();
String[] args = {"run", inputFile, resultsOutputFile};
callPmdWrapper(args); // Should not error

String resultsJsonString = new String(Files.readAllBytes(Paths.get(resultsOutputFile)));
JsonElement element = JsonParser.parseString(resultsJsonString); // Should not error
assertThat(element.isJsonObject(), is(true));
}

@Test
void whenRunningWithDeprecatedExcessiveClassLengthRule_thenExecutesSuccessfully(@TempDir Path tempDir) throws Exception {
// Create a minimal ruleset referencing the Apex ExcessiveClassLength rule with a low threshold
String rulesetXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<ruleset name=\"Ruleset for ExcessiveClassLength\"\n" +
" xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd\">\n" +
" <description>Run Apex ExcessiveClassLength</description>\n" +
" <rule ref=\"category/apex/design.xml/ExcessiveClassLength\">\n" +
" <properties>\n" +
" <property name=\"minimum\" value=\"5\"/>\n" +
" </properties>\n" +
" </rule>\n" +
"</ruleset>";
String rulesetFile = createTempFile(tempDir, "excessive-class-length.xml", rulesetXml);

// Create an Apex class with enough lines to exceed the small threshold
StringBuilder apexBuilder = new StringBuilder();
apexBuilder.append("public class LargeClass {\n");
apexBuilder.append(" public static void m0(){ Integer x0 = 0; }\n");
apexBuilder.append(" public static void m1(){ Integer x1 = 1; }\n");
apexBuilder.append(" public static void m2(){ Integer x2 = 2; }\n");
apexBuilder.append(" public static void m3(){ Integer x3 = 3; }\n");
apexBuilder.append(" public static void m4(){ Integer x4 = 4; }\n");
apexBuilder.append("}\n");
String apexFile = createTempFile(tempDir, "LargeClass.cls", apexBuilder.toString());

String inputJson = "{\n" +
" \"ruleSetInputFile\":\"" + makePathJsonSafe(rulesetFile) + "\",\n" +
" \"runDataPerLanguage\": {\n" +
" \"apex\": {\n" +
" \"filesToScan\": [\"" + makePathJsonSafe(apexFile) + "\"]\n" +
" }\n" +
" }\n" +
"}";
String inputFile = createTempFile(tempDir, "input-excessive-class-length.json", inputJson);

String resultsOutputFile = tempDir.resolve("results-excessive-class-length.json").toAbsolutePath().toString();
String[] args = {"run", inputFile, resultsOutputFile};
callPmdWrapper(args); // Should not error

String resultsJsonString = new String(Files.readAllBytes(Paths.get(resultsOutputFile)));
JsonElement element = JsonParser.parseString(resultsJsonString); // Should not error
assertThat(element.isJsonObject(), is(true));
}


private static String createSampleRulesetFile(Path tempDir) throws Exception {
String ruleSetContents = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
Expand Down
7 changes: 6 additions & 1 deletion packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type PmdProcessingError = {

const STDOUT_PROGRESS_MARKER = '[Progress]';
const STDOUT_ERROR_MARKER = '[Error] ';
const STDOUT_WARNING_MARKER = '[Warning] ';

export class PmdWrapperInvoker {
private readonly javaCommandExecutor: JavaCommandExecutor;
Expand Down Expand Up @@ -77,7 +78,11 @@ export class PmdWrapperInvoker {
if (stdOutMsg.startsWith(STDOUT_ERROR_MARKER)) {
const errorMessage: string = stdOutMsg.slice(STDOUT_ERROR_MARKER.length).replaceAll('{NEWLINE}','\n');
throw new Error(errorMessage);
} else {
} else if (stdOutMsg.startsWith(STDOUT_WARNING_MARKER)) {
const warningMessage: string = stdOutMsg.slice(STDOUT_WARNING_MARKER.length).replaceAll('{NEWLINE}','\n');
this.emitLogEvent(LogLevel.Warn, `[JAVA StdOut]: ${warningMessage}`);
}
else {
this.emitLogEvent(LogLevel.Fine, `[JAVA StdOut]: ${stdOutMsg}`)
}
});
Expand Down