Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -151,29 +151,65 @@ private static String getLimitedDescription(Rule rule) {
class PmdErrorListener implements PmdReporter {
@Override
public void logEx(Level level, @Nullable String s, Object[] objects, @Nullable Throwable throwable) {
// Unified handling for PMD log events:
// - If a Throwable is present, decide whether to surface it as a specific ruleset load error
// (more actionable message) or as a generic unexpected PMD exception.
// - If there is no Throwable:
// * A WARN containing a deprecation notice ("Discontinue using Rule ...") is surfaced
// as a non-fatal [Warning] to stdout so callers can display it.
// * Any other message is unexpected for our flows; fail fast with a RuntimeException
// so configuration/environment issues are not silently ignored.
if (throwable != null) {
String message = throwable.getMessage();
if (throwable instanceof RuleSetLoadException && message.contains("Cannot load ruleset ")) {
Pattern pattern = Pattern.compile("Cannot load ruleset (.+?): ");
Matcher matcher = pattern.matcher(message);
if (matcher.find()) {
String ruleset = matcher.group(1).trim();
String errorMessage = "PMD errored when attempting to load a custom ruleset \"" + ruleset + "\". " +
"Make sure the resource is a valid ruleset file on disk or on the Java classpath.\n\n" +
"PMD Exception: \n" + message.lines().map(l -> " | " + l).collect(Collectors.joining("n"));

// The typescript side can more easily handle error messages that come from stdout with "[Error] " marker
System.out.println("[Error] " + errorMessage.replaceAll("\n","{NEWLINE}"));
throw new RuntimeException(errorMessage, throwable);
}
}
throw new RuntimeException("PMD threw an unexpected exception:\n" + message, throwable);
} else if (s != null) {
String message = MessageFormat.format(s, objects);
throw new RuntimeException("PMD threw an unexpected exception:\n" + message);
handleThrowable(throwable);
return;
}
if (s == null) {
return; // nothing to report
}
final String message = MessageFormat.format(s, objects);
if (level == Level.WARN && isDeprecationWarning(message)) {
// Non-fatal deprecation: make it easy to capture and display without failing the operation
printStdout("Warning", message);
return;
}
// Any other logged message without a throwable is unexpected → fail fast
throw new RuntimeException("PMD threw an unexpected exception:\n" + message);
}

/**
* Handles PMD throwables emitted through the reporter.
* - For RuleSetLoadException we extract the ruleset reference and provide a clearer message.
* - Otherwise we surface a generic unexpected PMD exception.
*/
private static void handleThrowable(Throwable t) {
final String msg = t.getMessage();
if (t instanceof RuleSetLoadException && msg != null && msg.contains("Cannot load ruleset ")) {
final String ruleset = extractRuleset(msg);
final String formatted = "PMD errored when attempting to load a custom ruleset \"" + ruleset + "\". " +
"Make sure the resource is a valid ruleset file on disk or on the Java classpath.\n\n" +
"PMD Exception: \n" + msg.lines().map(l -> " | " + l).collect(Collectors.joining("n"));
// The TypeScript side can more easily handle error messages that come from stdout with "[Error]" marker.
printStdout("Error", formatted);
throw new RuntimeException(formatted, t);
}
throw new RuntimeException("PMD threw an unexpected exception:\n" + msg, t);
}

/** Returns true if this is a deprecation warning PMD emits for legacy rule references. */
private static boolean isDeprecationWarning(String msg) {
return msg.contains("Discontinue using Rule ");
}

/** Extracts the ruleset path from PMD's "Cannot load ruleset ..." message. */
private static String extractRuleset(String msg) {
Matcher m = Pattern.compile("Cannot load ruleset (.+?): ").matcher(msg);
return m.find() ? m.group(1).trim() : "<unknown>";
}

/** Prints a tagged message to stdout, replacing newlines for easy single-line capture. */
private static void printStdout(String kind, String msg) {
System.out.println("[" + kind + "] " + msg.replaceAll("\n","{NEWLINE}"));
}
// These methods aren't needed or used, but they are required to be implemented (since the interface does not give them default implementations)
@Override
public boolean isLoggable(Level level) {
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