Skip to content

Commit

Permalink
CHANGE(pmd): @W-17310830@: Update pmd-wrapper to run rules by languag…
Browse files Browse the repository at this point in the history
…e forcefully
  • Loading branch information
stephen-carter-at-sf committed Dec 9, 2024
1 parent ced5efe commit 70ba652
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
class CpdRunInputData {
public Map<String, LanguageSpecificRunData> runDataPerLanguage;
public boolean skipDuplicateFiles;
}

class LanguageSpecificRunData {
public List<String> filesToScan;
public int minimumTokens;
static class LanguageSpecificRunData {
public List<String> filesToScan;
public int minimumTokens;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import com.salesforce.sfca.shared.CodeLocation;
import com.salesforce.sfca.shared.ProcessingError;
import com.salesforce.sfca.shared.ProgressReporter;
import net.sourceforge.pmd.cpd.CPDConfiguration;
import net.sourceforge.pmd.cpd.CPDListener;
import net.sourceforge.pmd.cpd.CpdAnalysis;
import net.sourceforge.pmd.cpd.Mark;
import net.sourceforge.pmd.cpd.Match;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.document.FileLocation;
import net.sourceforge.pmd.reporting.Report;
import net.sourceforge.pmd.util.log.PmdReporter;
import org.slf4j.event.Level;
Expand Down Expand Up @@ -37,7 +37,7 @@ public Map<String, CpdLanguageRunResults> run(CpdRunInputData runInputData) thro

Map<String, CpdLanguageRunResults> results = new HashMap<>();
for (String language : languagesToProcess) {
LanguageSpecificRunData languageSpecificRunData = runInputData.runDataPerLanguage.get(language);
CpdRunInputData.LanguageSpecificRunData languageSpecificRunData = runInputData.runDataPerLanguage.get(language);
List<Path> pathsToScan = languageSpecificRunData.filesToScan.stream().map(Paths::get).collect(Collectors.toList());
CpdLanguageRunResults languageRunResults = runLanguage(
language, pathsToScan, languageSpecificRunData.minimumTokens, runInputData.skipDuplicateFiles);
Expand Down Expand Up @@ -116,14 +116,14 @@ private void validateRunInputData(CpdRunInputData runInputData) {
throw new RuntimeException("The \"runDataPerLanguage\" field was not set.");
}

Set<Map.Entry<String, LanguageSpecificRunData>> entries = runInputData.runDataPerLanguage.entrySet();
Set<Map.Entry<String, CpdRunInputData.LanguageSpecificRunData>> entries = runInputData.runDataPerLanguage.entrySet();
if (entries.isEmpty()) {
throw new RuntimeException("The \"runDataPerLanguage\" field didn't have any languages listed.");
}

for (Map.Entry<String, LanguageSpecificRunData> entry: entries) {
for (Map.Entry<String, CpdRunInputData.LanguageSpecificRunData> entry: entries) {
String language = entry.getKey();
LanguageSpecificRunData languageSpecificRunData = entry.getValue();
CpdRunInputData.LanguageSpecificRunData languageSpecificRunData = entry.getValue();

if (languageSpecificRunData.filesToScan == null || languageSpecificRunData.filesToScan.isEmpty()) {
throw new RuntimeException(("The \"filesToScan\" field was missing or empty for language: " + language));
Expand Down Expand Up @@ -168,38 +168,6 @@ public int numErrors() {
}
}

// This class helps us track the overall progress of all language runs
class ProgressReporter {
private Map<String, Float> progressPerLanguage = new HashMap<>();
private float lastReportedProgress = 0.0f;

public void initialize(List<String> languages) {
progressPerLanguage = new HashMap<>();
languages.forEach(l -> this.updateProgressForLanguage(l, 0.0f));
}

public void updateProgressForLanguage(String language, float percComplete) {
progressPerLanguage.put(language, percComplete);
}

public void reportOverallProgress() {
float currentProgress = this.calculateOverallPercentage();
// The progress goes very fast, so we make sure to only report progress if there has been a significant enough increase (at least 1%)
if (currentProgress >= lastReportedProgress + 1) {
System.out.println("[Progress]" + currentProgress);
lastReportedProgress = currentProgress;
}
}

private float calculateOverallPercentage() {
float sum = 0.0f;
for (float progress : progressPerLanguage.values()) {
sum += progress;
}
return sum / progressPerLanguage.size();
}
}

// This class is a specific listener for a run of cpd for a single language.
class CpdLanguageRunListener implements CPDListener {
private final ProgressReporter progressReporter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.salesforce.sfca.pmdwrapper;

import java.util.List;
import java.util.Map;

public class PmdRunInputData {
public String ruleSetInputFile;
public Map<String, LanguageSpecificRunData> runDataPerLanguage;

public static class LanguageSpecificRunData {
public List<String> filesToScan;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,58 @@

import com.salesforce.sfca.shared.CodeLocation;
import com.salesforce.sfca.shared.ProcessingError;
import com.salesforce.sfca.shared.ProgressReporter;
import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.PmdAnalysis;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.document.TextFile;
import net.sourceforge.pmd.reporting.*;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class PmdRunner {
PmdRunResults runRules(String ruleSetInputFile, String filesToScanInputFile) {
PMDConfiguration config = new PMDConfiguration();
config.addRuleSet(ruleSetInputFile);
config.setInputFilePath(Paths.get(filesToScanInputFile));
private final ProgressReporter progressReporter = new ProgressReporter();

PmdRunResults run(PmdRunInputData inputData) {
validateRunInputData(inputData);

List<String> languagesToProcess = new ArrayList<>(inputData.runDataPerLanguage.keySet());
progressReporter.initialize(languagesToProcess);

PmdRunResults runResults = new PmdRunResults();
for (String language: languagesToProcess) {
runLanguage(language, inputData, runResults);
}
return runResults;
}

private void runLanguage(String language, PmdRunInputData inputData, PmdRunResults runResults) {
PmdRunInputData.LanguageSpecificRunData languageSpecificRunData = inputData.runDataPerLanguage.get(language);

PMDConfiguration config = new PMDConfiguration();
config.addRuleSet(inputData.ruleSetInputFile);
List<Path> inputPathList = languageSpecificRunData.filesToScan.stream().map(Paths::get).collect(Collectors.toList());
config.setInputPathList(inputPathList);

// Force the language so that pmd doesn't look at file extensions. Note: we already associated the files based
// on their file extensions to the correct languages the typescript side.
Language pmdLanguageId = config.getLanguageRegistry().getLanguageById(language);
if (pmdLanguageId == null) {
throw new RuntimeException("The language \"" + language + "\" is not recognized by PMD.");
}
LanguageVersion forcedLangVer = config.getLanguageVersionDiscoverer()
.getDefaultLanguageVersion(pmdLanguageId);
config.setForceLanguageVersion(forcedLangVer);

// TODO: This is temporary. Soon we'll be looping over the languages.
try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
pmd.addListener(new PmdRunProgressListener());
pmd.addListener(new PmdRunProgressListener(progressReporter, language));
Report report = pmd.performAnalysisAndCollectReport();
for (Report.ProcessingError reportProcessingError : report.getProcessingErrors()) {
runResults.processingErrors.add(
Expand All @@ -33,15 +67,44 @@ PmdRunResults runRules(String ruleSetInputFile, String filesToScanInputFile) {
runResults.violations.add(violation);
}
}
}

private void validateRunInputData(PmdRunInputData inputData) {
if (inputData.ruleSetInputFile == null) {
throw new RuntimeException(("The \"ruleSetInputFile\" field was missing."));
}

return runResults;
if (inputData.runDataPerLanguage == null) {
throw new RuntimeException("The \"runDataPerLanguage\" field was not set.");
}

Set<Map.Entry<String, PmdRunInputData.LanguageSpecificRunData>> entries = inputData.runDataPerLanguage.entrySet();
if (entries.isEmpty()) {
throw new RuntimeException("The \"runDataPerLanguage\" field didn't have any languages listed.");
}

for (Map.Entry<String, PmdRunInputData.LanguageSpecificRunData> entry: entries) {
String language = entry.getKey();
PmdRunInputData.LanguageSpecificRunData languageSpecificRunData = entry.getValue();
if (languageSpecificRunData.filesToScan == null || languageSpecificRunData.filesToScan.isEmpty()) {
throw new RuntimeException(("The \"filesToScan\" field was missing or empty for language: " + language));
}
}
}
}

class PmdRunProgressListener implements GlobalAnalysisListener {
private final ProgressReporter progressReporter;
private final String language;

private int totalNumFiles = 0;
private int fileCount = 0;

public PmdRunProgressListener(ProgressReporter progressReporter, String language) {
this.progressReporter = progressReporter;
this.language = language;
}

@Override
public ListenerInitializer initializer() {
return new ListenerInitializer() {
Expand All @@ -57,7 +120,8 @@ public synchronized FileAnalysisListener startFileAnalysis(TextFile textFile) {
// Note that this method must be synchronized so that multiple threads cannot mess
// up the order of progress with race conditions.
fileCount++;
System.out.println("[Progress]" + fileCount + "::" + totalNumFiles);
progressReporter.updateProgressForLanguage(language, 100 * ((float) fileCount / totalNumFiles));
progressReporter.reportOverallProgress();
return FileAnalysisListener.noop();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.gson.Gson;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -22,19 +23,18 @@
* - {languages} is a comma separated list of languages associated with the rules to describe
* RUN:
* - Runs the rules provided by the input ruleset file on a set of files and writes results to a JSON file
* - Invocation: java -cp {classPath} com.salesforce.sfca.pmdwrapper.PmdWrapper run {ruleSetInputFile} {filesToScanInputFile} {resultsOutputFile}
* - Invocation: java -cp {classPath} com.salesforce.sfca.pmdwrapper.PmdWrapper run {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 run command.
* Example:
* {
* "ruleSetInputFile": "/some/rulesetFileForRulesToRun.xml",
* "runDataPerLanguage": {
* "apex": {
* "ruleSetInputFile": "/some/rulesetFileForApexRules.xml",
* "filesToScan": ["/full/path/to/apex_file1.cls", "/full/path/to/apex_file2.trigger", ...]
* },
* ...,
* "xml": {
* "ruleSetInputFile": "/some/rulesetFileForXmlRules.xml",
* "filesToScan": ["/full/path/to/xml_file1.xml", "/full/path/to/xml_file2.xml", ...]
* }
* }
Expand Down Expand Up @@ -123,19 +123,25 @@ private static List<String> getCustomRulesetsFromFile(String file) {
}

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

Gson gson = new Gson();

PmdRunInputData inputData;
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);
}

PmdRunner pmdRunner = new PmdRunner();
PmdRunResults results;
try {
results = pmdRunner.runRules(ruleSetInputFile, filesToScanInputFile);
results = pmdRunner.run(inputData);
} catch (Exception e) {
throw new RuntimeException("Error while attempting to invoke PmdRunner.run: " + e.getMessage(), e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.salesforce.sfca.shared;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

// This class helps us track the overall progress of all language runs
public class ProgressReporter {
private Map<String, Float> progressPerLanguage = new HashMap<>();
private float lastReportedProgress = 0.0f;

public void initialize(List<String> languages) {
progressPerLanguage = new HashMap<>();
languages.forEach(l -> this.updateProgressForLanguage(l, 0.0f));
}

public void updateProgressForLanguage(String language, float percComplete) {
progressPerLanguage.put(language, percComplete);
}

public void reportOverallProgress() {
float currentProgress = this.calculateOverallPercentage();
// The progress goes very fast, so we make sure to only report progress if there has been a significant enough increase (at least 1%)
if (currentProgress >= lastReportedProgress + 1) {
System.out.println("[Progress]" + currentProgress);
lastReportedProgress = currentProgress;
}
}

private float calculateOverallPercentage() {
float sum = 0.0f;
for (float progress : progressPerLanguage.values()) {
sum += progress;
}
return sum / progressPerLanguage.size();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ void whenCallingMainWithRunAndTooFewArgs_thenError() {
}

@Test
void whenCallingMainWithDescribeAndTooManyArgs_thenError() {
void whenCallingMainWithRunAndTooManyArgs_thenError() {
String[] args = {"run", "too", "many", "args"};
Exception thrown = assertThrows(Exception.class, () -> callCpdWrapper(args));
assertThat(thrown.getMessage(), is("Invalid number of arguments following the \"run\" command. Expected 2 but received: 3"));
Expand Down
Loading

0 comments on commit 70ba652

Please sign in to comment.