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.36.0",
"version": "0.37.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
@@ -0,0 +1,22 @@
package com.salesforce.sfca.pmdwrapper;

/**
* Input data structure for AST dump command
*/
public class PmdAstDumpInputData {
/**
* The language of the file to dump AST for (e.g., "apex", "xml", "visualforce")
*/
public String language;

/**
* Single file to generate AST for
*/
public String fileToDump;

/**
* Character encoding for reading the file
* Defaults to "UTF-8" if not specified
*/
public String encoding = "UTF-8";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.salesforce.sfca.pmdwrapper;

import com.salesforce.sfca.shared.ProcessingError;

/**
* Results structure for AST dump command.
* Contains either the AST (if successful) or an error (if failed), but never both.
*/
public class PmdAstDumpResults {
/**
* Full path to the file that was processed
*/
public String file;

/**
* The AST representation in XML format
* This is populated if the AST generation was successful, null otherwise
*/
public String ast;

/**
* Error details if AST generation failed
* This is populated if the AST generation failed, null otherwise
*/
public ProcessingError error;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.salesforce.sfca.pmdwrapper;

import com.salesforce.sfca.shared.ProcessingError;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.util.treeexport.TreeExportConfiguration;
import net.sourceforge.pmd.util.treeexport.TreeExporter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
* Core class that performs AST dumping using PMD's TreeExporter API
*/
public class PmdAstDumper {

/**
* Dumps the AST for a single file in XML format
*
* @param inputData Input data containing language, file path, and encoding
* @return Results containing either the AST (if successful) or error details (if failed)
*/
public PmdAstDumpResults dump(PmdAstDumpInputData inputData) {
validateInputData(inputData);

PmdAstDumpResults results = new PmdAstDumpResults();
results.file = inputData.fileToDump;

try {
System.out.println("Generating AST for file '" + inputData.fileToDump + "' with language '" + inputData.language + "'");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to log this instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are intentionally using System.out.println here to maintain consistency
with the rest of the codebase (see PmdRuleDescriber.java:99 and
CpdRunner.java:55). The project uses slf4j-nop and relies on stdout for
inter-process communication with the TypeScript side.

am I missing something @namrata111f ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, was not aware that inter-process communication relies on stdout. Just curious for debugging do we log these stdout on the TypeScript side?


// Verify file exists and is valid (lightweight validation without reading content)
Path filePath = Paths.get(inputData.fileToDump);
validateFilePath(filePath, inputData.encoding);

// Get language
Language language = LanguageRegistry.PMD.getLanguageById(inputData.language);
if (language == null) {
throw new RuntimeException("Language not supported: " + inputData.language);
}

// Create TreeExportConfiguration
TreeExportConfiguration config = new TreeExportConfiguration();
config.setLanguage(language);
config.setFormat("xml"); // Always XML format for v1
config.setFile(filePath);

// Capture output to string
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintStream capturedPrintStream = new PrintStream(outputStream, true, StandardCharsets.UTF_8);
PrintStream originalOut = System.out;

try {
// Redirect System.out to capture XML output
System.setOut(capturedPrintStream);

// Create and export AST (TreeExporter writes to System.out)
TreeExporter exporter = new TreeExporter(config);
exporter.export();

// Get the XML output
results.ast = outputStream.toString(StandardCharsets.UTF_8);

} finally {
// Restore original System.out
System.setOut(originalOut);
capturedPrintStream.close();
}

System.out.println("Successfully generated AST for file '" + inputData.fileToDump + "'");

} catch (Exception e) {
// Store processing error
System.err.println("Error generating AST for file '" + inputData.fileToDump + "': " + e.getMessage());
ProcessingError error = new ProcessingError();
error.file = inputData.fileToDump;
error.message = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
error.detail = e.toString();
results.error = error;
}

return results;
}

/**
* Validates the input data
*/
private void validateInputData(PmdAstDumpInputData inputData) {
if (inputData.language == null || inputData.language.trim().isEmpty()) {
throw new RuntimeException("The 'language' field is required");
}
if (inputData.fileToDump == null || inputData.fileToDump.trim().isEmpty()) {
throw new RuntimeException("The 'fileToDump' field is required");
}
if (inputData.encoding == null || inputData.encoding.trim().isEmpty()) {
inputData.encoding = "UTF-8";
}
}

/**
* Validates file path and encoding without reading file content.
* This avoids loading large files into memory unnecessarily.
* PMD's TreeExporter will handle reading the file content.
*/
private void validateFilePath(Path filePath, String encoding) throws IOException {
if (!Files.exists(filePath)) {
throw new IOException("File not found: " + filePath);
}
if (!Files.isRegularFile(filePath)) {
throw new IOException("Not a regular file: " + filePath);
}

// Validate encoding by attempting to get the Charset (throws if invalid)
Charset.forName(encoding);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@
* }
* ]
* }
* AST-DUMP:
* - Generates Abstract Syntax Tree representation of a source file in XML format
* - Invocation: java -cp {classPath} com.salesforce.sfca.pmdwrapper.PmdWrapper ast-dump {argsInputFile} {resultsOutputFile}
* - {classPath} is the list of entries to add to the class path
* - {argsInputFile} is a JSON file containing the input arguments for the ast-dump command.
* Example:
* {
* "language": "apex",
* "fileToDump": "/full/path/to/MyClass.cls",
* "encoding": "UTF-8"
* }
* - {resultsOutputFile} is a file to write the JSON formatted AST dump results to
* Example (success):
* {
* "file": "/full/path/to/MyClass.cls",
* "ast": "<?xml version='1.0'?>\n<ApexClass>...",
* "error": null
* }
* Example (error):
* {
* "file": "/full/path/to/MyClass.cls",
* "ast": null,
* "error": {
* "file": "/full/path/to/MyClass.cls",
* "message": "ParseException: Unexpected token",
* "detail": "..."
* }
* }
*/

public class PmdWrapper {
Expand All @@ -82,8 +110,10 @@ public static void main(String[] args) {
invokeDescribeCommand(Arrays.copyOfRange(args, 1, args.length));
} else if(args[0].equalsIgnoreCase("run")) {
invokeRunCommand(Arrays.copyOfRange(args, 1, args.length));
} else if(args[0].equalsIgnoreCase("ast-dump")) {
invokeAstDumpCommand(Arrays.copyOfRange(args, 1, args.length));
} else {
throw new RuntimeException("Bad first argument to PmdWrapper. Expected \"describe\" or \"run\". Received: \"" + args[0] + "\"");
throw new RuntimeException("Bad first argument to PmdWrapper. Expected \"describe\", \"run\", or \"ast-dump\". Received: \"" + args[0] + "\"");
}

long endTime = System.currentTimeMillis();
Expand Down Expand Up @@ -135,7 +165,7 @@ private static void invokeRunCommand(String[] args) {
try (FileReader reader = new FileReader(argsInputFile)) {
inputData = gson.fromJson(reader, PmdRunInputData.class);
} catch (Exception e) {
throw new RuntimeException("Could not read contents from \"" + argsInputFile + "\"", e);
throw new RuntimeException("Could not read contents from \"" + argsInputFile + "\": " + e.getMessage(), e);
}

PmdRunner pmdRunner = new PmdRunner();
Expand All @@ -152,4 +182,38 @@ private static void invokeRunCommand(String[] args) {
throw new RuntimeException(e);
}
}

private static void invokeAstDumpCommand(String[] args) {
if (args.length != 2) {
throw new RuntimeException("Invalid number of arguments following the \"ast-dump\" command. Expected 2 but received: " + args.length);
}
String argsInputFile = args[0];
String resultsOutputFile = args[1];

Gson gson = new Gson();

// Read input data
PmdAstDumpInputData inputData;
try (FileReader reader = new FileReader(argsInputFile)) {
inputData = gson.fromJson(reader, PmdAstDumpInputData.class);
} catch (Exception e) {
throw new RuntimeException("Could not read contents from \"" + argsInputFile + "\": " + e.getMessage(), e);
}

// Execute AST dump
PmdAstDumper astDumper = new PmdAstDumper();
PmdAstDumpResults results;
try {
results = astDumper.dump(inputData);
} catch (Exception e) {
throw new RuntimeException("Error while attempting to invoke PmdAstDumper.dump: " + e.getMessage(), e);
}

// Write results
try (FileWriter fileWriter = new FileWriter(resultsOutputFile)) {
gson.toJson(results, fileWriter);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Loading