11package com .salesforce .sfca .cpdwrapper ;
22
33import net .sourceforge .pmd .cpd .CPDConfiguration ;
4+ import net .sourceforge .pmd .cpd .CPDListener ;
45import net .sourceforge .pmd .cpd .CpdAnalysis ;
56import net .sourceforge .pmd .cpd .Mark ;
67import net .sourceforge .pmd .cpd .Match ;
2425 * Class to help us invoke CPD - once for each language that should be processed
2526 */
2627class CpdRunner {
28+ private final ProgressReporter progressReporter = new ProgressReporter ();
29+
2730 public Map <String , CpdLanguageRunResults > run (CpdRunInputData runInputData ) throws IOException {
2831 validateRunInputData (runInputData );
2932
30- Map <String , CpdLanguageRunResults > results = new HashMap <>();
33+ List <String > languagesToProcess = runInputData .filesToScanPerLanguage .entrySet ().stream ()
34+ .filter (entry -> !entry .getValue ().isEmpty ()) // Keep only non-empty lists
35+ .map (Map .Entry ::getKey )
36+ .collect (Collectors .toList ());
3137
32- for (Map .Entry <String , List <String >> entry : runInputData .filesToScanPerLanguage .entrySet ()) {
33- String language = entry .getKey ();
34- List <String > filesToScan = entry .getValue ();
35- if (filesToScan .isEmpty ()) {
36- continue ;
37- }
38+ progressReporter .initialize (languagesToProcess );
39+
40+ Map <String , CpdLanguageRunResults > results = new HashMap <>();
41+ for (String language : languagesToProcess ) {
42+ List <String > filesToScan = runInputData .filesToScanPerLanguage .get (language );
3843 List <Path > pathsToScan = filesToScan .stream ().map (Paths ::get ).collect (Collectors .toList ());
3944 CpdLanguageRunResults languageRunResults = runLanguage (language , pathsToScan , runInputData .minimumTokens , runInputData .skipDuplicateFiles );
40-
4145 if (!languageRunResults .matches .isEmpty () || !languageRunResults .processingErrors .isEmpty ()) {
4246 results .put (language , languageRunResults );
4347 }
4448 }
45-
4649 return results ;
4750 }
4851
@@ -64,8 +67,15 @@ private CpdLanguageRunResults runLanguage(String language, List<Path> pathsToSca
6467 CpdLanguageRunResults languageRunResults = new CpdLanguageRunResults ();
6568
6669 try (CpdAnalysis cpd = CpdAnalysis .create (config )) {
67- cpd .performAnalysis (report -> {
6870
71+ // Note that we could use cpd.files().getCollectedFiles().size() to get the true totalNumFiles but
72+ // unfortunately getCollectedFiles doesn't cache and does a sort operation which is expensive.
73+ // So instead we use pathsToScan.size() since we send in the list of files instead of folders and so
74+ // these numbers should be the same.
75+ int totalNumFiles = pathsToScan .size ();
76+ cpd .setCpdListener (new CpdLanguageRunListener (progressReporter , language , totalNumFiles ));
77+
78+ cpd .performAnalysis (report -> {
6979 for (Report .ProcessingError reportProcessingError : report .getProcessingErrors ()) {
7080 CpdLanguageRunResults .ProcessingError processingErr = new CpdLanguageRunResults .ProcessingError ();
7181 processingErr .file = reportProcessingError .getFileId ().getAbsolutePath ();
@@ -134,4 +144,77 @@ public boolean isLoggable(Level level) {
134144 public int numErrors () {
135145 return 0 ;
136146 }
147+ }
148+
149+ // This class helps us track the overall progress of all language runs
150+ class ProgressReporter {
151+ private Map <String , Float > progressPerLanguage = new HashMap <>();
152+ private float lastReportedProgress = 0.0f ;
153+
154+ public void initialize (List <String > languages ) {
155+ progressPerLanguage = new HashMap <>();
156+ languages .forEach (l -> this .updateProgressForLanguage (l , 0.0f ));
157+ }
158+
159+ public void updateProgressForLanguage (String language , float percComplete ) {
160+ progressPerLanguage .put (language , percComplete );
161+ }
162+
163+ public void reportOverallProgress () {
164+ float currentProgress = this .calculateOverallPercentage ();
165+ // The progress goes very fast, so we make sure to only report progress if there has been a significant enough increase (at least 1%)
166+ if (currentProgress >= lastReportedProgress + 1 ) {
167+ System .out .println ("[Progress]" + currentProgress );
168+ lastReportedProgress = currentProgress ;
169+ }
170+ }
171+
172+ private float calculateOverallPercentage () {
173+ float sum = 0.0f ;
174+ for (float progress : progressPerLanguage .values ()) {
175+ sum += progress ;
176+ }
177+ return sum / progressPerLanguage .size ();
178+ }
179+ }
180+
181+ // This class is a specific listener for a run of cpd for a single language.
182+ class CpdLanguageRunListener implements CPDListener {
183+ private final ProgressReporter progressReporter ;
184+ private final String language ;
185+ private final int totalNumFiles ;
186+ private int numFilesAdded = 0 ;
187+ private int currentPhase = CPDListener .INIT ;
188+
189+ public CpdLanguageRunListener (ProgressReporter progressReporter , String language , int totalNumFiles ) {
190+ this .progressReporter = progressReporter ;
191+ this .language = language ;
192+ this .totalNumFiles = totalNumFiles ;
193+ }
194+
195+ @ Override
196+ public void addedFile (int i ) {
197+ // All files are added while we still are on phase 0 - INIT, i.e. before the phase is updated to phase 1 - HASH.
198+ this .numFilesAdded += i ;
199+ updateAndReportCompletePercentage ();
200+ }
201+
202+ @ Override
203+ public void phaseUpdate (int i ) {
204+ this .currentPhase = i ;
205+ updateAndReportCompletePercentage ();
206+ }
207+
208+ private void updateAndReportCompletePercentage () {
209+ this .progressReporter .updateProgressForLanguage (this .language , calculateCompletePercentage ());
210+ this .progressReporter .reportOverallProgress ();
211+ }
212+
213+ private float calculateCompletePercentage () {
214+ if (this .currentPhase == CPDListener .INIT ) {
215+ // Using Math.min just in case the totalNumFiles is inaccurate - although it shouldn't be.
216+ return 25 *(Math .min ((float ) this .numFilesAdded / this .totalNumFiles , 1.0f ));
217+ }
218+ return 100 * ((float ) this .currentPhase / CPDListener .DONE );
219+ }
137220}
0 commit comments