From 0c8e34c50ae8cc5034a6da36bcb49af101fe29ea Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Tue, 27 Jan 2026 12:18:14 +0530 Subject: [PATCH 1/4] add warning marker in PMDRule describer --- .../com/salesforce/sfca/pmdwrapper/PmdRuleDescriber.java | 5 ++++- packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 index 649cba21..4c14f02d 100644 --- a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 @@ -168,7 +168,10 @@ 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 (s != null) { + } else if (level == Level.WARN && s != null){ + String message = MessageFormat.format(s, objects); + System.out.println("[Warning] " + message.replaceAll("\n","{NEWLINE}")); + }else if (s != null) { String message = MessageFormat.format(s, objects); throw new RuntimeException("PMD threw an unexpected exception:\n" + message); } diff --git a/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts b/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts index d0b5d735..a80b2049 100644 --- a/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts +++ b/packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts @@ -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; @@ -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}`) } }); From 546b193f33e48fb3a79cfc2bb5c53c44bbd8de82 Mon Sep 17 00:00:00 2001 From: aruntyagiTutu Date: Tue, 27 Jan 2026 13:37:45 +0530 Subject: [PATCH 2/4] CHANGE @W-20736388@ - Add PMD run tests for NcssCount and ExcessiveClassLength (#403) --- .../sfca/pmdwrapper/PmdRuleDescriber.java | 6 +- .../sfca/pmdwrapper/PmdWrapperTest.java | 155 ++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 index 4c14f02d..10337e6c 100644 --- a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 @@ -170,7 +170,11 @@ 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){ String message = MessageFormat.format(s, objects); - System.out.println("[Warning] " + message.replaceAll("\n","{NEWLINE}")); + if (message.contains("Discontinue using Rule ")) { + System.out.println("[Warning] " + message.replaceAll("\n","{NEWLINE}")); + } else { + throw new RuntimeException("PMD threw an unexpected exception:\n" + message); + } }else if (s != null) { String message = MessageFormat.format(s, objects); throw new RuntimeException("PMD threw an unexpected exception:\n" + message); diff --git a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 index 02a78f3c..49120e8e 100644 --- a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 @@ -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 = "\n" + + "\n" + + " Include Apex NcssCount\n" + + " \n" + + ""; + 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>(){}.getType(); + List 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 = "\n" + + "\n" + + " Include Apex ExcessiveClassLength\n" + + " \n" + + ""; + 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>(){}.getType(); + List 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"}; @@ -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 = "\n" + + "\n" + + " Run Apex NcssCount\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + 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 = "\n" + + "\n" + + " Run Apex ExcessiveClassLength\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + 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 = "\n" + From d6731f96cc596e30bfda851537ec16584ecc0806 Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Tue, 27 Jan 2026 17:23:41 +0530 Subject: [PATCH 3/4] update package version --- packages/code-analyzer-pmd-engine/package.json | 2 +- .../com/salesforce/sfca/pmdwrapper/PmdRuleDescriber.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/code-analyzer-pmd-engine/package.json b/packages/code-analyzer-pmd-engine/package.json index 4391c211..331a98cb 100644 --- a/packages/code-analyzer-pmd-engine/package.json +++ b/packages/code-analyzer-pmd-engine/package.json @@ -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", diff --git a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 index 10337e6c..6e37ea2a 100644 --- a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 @@ -169,13 +169,20 @@ 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) { + } 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); } From 8b45e5e943fbf5348bd5bea91146c699af7be9f5 Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Wed, 28 Jan 2026 10:16:15 +0530 Subject: [PATCH 4/4] Refactored the PMD reporter handling in PmdRuleDescriber --- .../sfca/pmdwrapper/PmdRuleDescriber.java | 88 ++++++++++++------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 index 6e37ea2a..ff8674e1 100644 --- a/packages/code-analyzer-pmd-engine/pmd-cpd-wrappers/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 @@ -151,43 +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 (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); + 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() : ""; + } + + /** 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) {