001package com.streamconverter.tools;
002
003import java.io.IOException;
004import java.nio.file.Files;
005import java.nio.file.Path;
006import java.nio.file.Paths;
007import java.nio.file.StandardCopyOption;
008import java.time.LocalDateTime;
009import java.time.format.DateTimeFormatter;
010import java.util.*;
011import java.util.regex.Matcher;
012import java.util.regex.Pattern;
013import java.util.stream.Collectors;
014import javax.xml.parsers.DocumentBuilder;
015import javax.xml.parsers.DocumentBuilderFactory;
016import org.w3c.dom.Document;
017import org.w3c.dom.Element;
018import org.w3c.dom.NodeList;
019
020public class PmdAutoFixer {
021
022  private static final String PMD_REPORT_PATH = "build/reports/pmd/main.xml";
023  private static final String SOURCE_PATH = "src/main/java";
024
025  private final Map<String, Integer> fixCounts = new HashMap<>();
026  private final Set<String> processedFiles = new HashSet<>();
027
028  public static void main(String[] args) {
029    new PmdAutoFixer().run();
030  }
031
032  public void run() {
033    System.out.println("๐Ÿ”ง PMD Auto-Fixer Starting...");
034
035    try {
036      // Create backup
037      createBackup();
038
039      // Parse PMD report
040      List<PmdViolation> violations = parsePmdReport();
041      System.out.printf("๐Ÿ“Š Found %d violations to process%n", violations.size());
042
043      // Group violations by rule
044      Map<String, List<PmdViolation>> violationsByRule =
045          violations.stream().collect(Collectors.groupingBy(v -> v.rule));
046
047      // Apply fixes
048      applyFixes(violationsByRule);
049
050      // Print summary
051      printSummary();
052
053    } catch (Exception e) {
054      System.err.println("โŒ Error during auto-fix: " + e.getMessage());
055      e.printStackTrace();
056    }
057  }
058
059  private void createBackup() throws IOException {
060    Path sourcePath = Paths.get(SOURCE_PATH);
061    if (!Files.exists(sourcePath)) {
062      throw new IOException("Source path not found: " + sourcePath);
063    }
064
065    String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
066    Path backupPath = Paths.get("pmd-fixes-backup-" + timestamp);
067
068    System.out.println("๐Ÿ’พ Creating backup at: " + backupPath.toAbsolutePath());
069
070    copyDirectory(sourcePath, backupPath);
071  }
072
073  private void copyDirectory(Path source, Path target) throws IOException {
074    Files.walk(source)
075        .forEach(
076            sourcePath -> {
077              try {
078                Path targetPath = target.resolve(source.relativize(sourcePath));
079                if (Files.isDirectory(sourcePath)) {
080                  Files.createDirectories(targetPath);
081                } else {
082                  Files.createDirectories(targetPath.getParent());
083                  Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
084                }
085              } catch (IOException e) {
086                throw new RuntimeException(e);
087              }
088            });
089  }
090
091  private List<PmdViolation> parsePmdReport() throws Exception {
092    Path reportPath = Paths.get(PMD_REPORT_PATH);
093    if (!Files.exists(reportPath)) {
094      throw new IOException("PMD report not found: " + reportPath);
095    }
096
097    List<PmdViolation> violations = new ArrayList<>();
098
099    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
100    DocumentBuilder builder = factory.newDocumentBuilder();
101    Document doc = builder.parse(reportPath.toFile());
102
103    NodeList fileNodes = doc.getElementsByTagName("file");
104    for (int i = 0; i < fileNodes.getLength(); i++) {
105      Element fileElement = (Element) fileNodes.item(i);
106      String fileName = fileElement.getAttribute("name");
107
108      NodeList violationNodes = fileElement.getElementsByTagName("violation");
109      for (int j = 0; j < violationNodes.getLength(); j++) {
110        Element violationElement = (Element) violationNodes.item(j);
111        violations.add(
112            new PmdViolation(
113                fileName,
114                violationElement.getAttribute("rule"),
115                Integer.parseInt(violationElement.getAttribute("beginline")),
116                Integer.parseInt(violationElement.getAttribute("endline")),
117                violationElement.getTextContent().trim()));
118      }
119    }
120
121    return violations;
122  }
123
124  private void applyFixes(Map<String, List<PmdViolation>> violationsByRule) {
125    System.out.println("\n๐ŸŽฏ Applying automatic fixes...");
126
127    // Phase 1: Safe Final Keyword Additions
128    fixMethodArgumentCouldBeFinal(violationsByRule.get("MethodArgumentCouldBeFinal"));
129    fixLocalVariableCouldBeFinal(violationsByRule.get("LocalVariableCouldBeFinal"));
130
131    // Phase 2: Performance Optimizations
132    fixInefficientEmptyStringCheck(violationsByRule.get("InefficientEmptyStringCheck"));
133    fixAppendCharacterWithChar(violationsByRule.get("AppendCharacterWithChar"));
134    fixUseIndexOfChar(violationsByRule.get("UseIndexOfChar"));
135
136    // Phase 3: Code Style Improvements
137    fixRedundantFieldInitializer(violationsByRule.get("RedundantFieldInitializer"));
138    fixUseUnderscoresInNumericLiterals(violationsByRule.get("UseUnderscoresInNumericLiterals"));
139  }
140
141  private void fixMethodArgumentCouldBeFinal(List<PmdViolation> violations) {
142    if (violations == null || violations.isEmpty()) return;
143
144    System.out.println("๐Ÿ”„ Fixing MethodArgumentCouldBeFinal violations...");
145
146    Map<String, List<PmdViolation>> byFile =
147        violations.stream().collect(Collectors.groupingBy(v -> v.fileName));
148
149    int totalFixed = 0;
150
151    for (Map.Entry<String, List<PmdViolation>> entry : byFile.entrySet()) {
152      try {
153        String content = Files.readString(Paths.get(entry.getKey()));
154        // Simple pattern-based fixing for method parameters
155        // This is a basic implementation - could be enhanced with proper Java parsing
156        Pattern methodParamPattern = Pattern.compile("(\\([^)]*?)\\b(\\w+\\s+)(\\w+)\\s*([,)])");
157
158        Matcher matcher = methodParamPattern.matcher(content);
159        StringBuffer sb = new StringBuffer();
160        int fixCount = 0;
161
162        while (matcher.find()) {
163          String paramType = matcher.group(2).trim();
164          if (!paramType.startsWith("final ")) {
165            matcher.appendReplacement(
166                sb,
167                matcher.group(1)
168                    + "final "
169                    + paramType
170                    + " "
171                    + matcher.group(3)
172                    + matcher.group(4));
173            fixCount++;
174          }
175        }
176        matcher.appendTail(sb);
177
178        if (fixCount > 0) {
179          Files.writeString(Paths.get(entry.getKey()), sb.toString());
180          totalFixed += fixCount;
181          processedFiles.add(entry.getKey());
182        }
183
184      } catch (IOException e) {
185        System.err.println("โš ๏ธ Error processing file: " + entry.getKey() + " - " + e.getMessage());
186      }
187    }
188
189    fixCounts.put("MethodArgumentCouldBeFinal", totalFixed);
190  }
191
192  private void fixLocalVariableCouldBeFinal(List<PmdViolation> violations) {
193    if (violations == null || violations.isEmpty()) return;
194
195    System.out.println("๐Ÿ”„ Fixing LocalVariableCouldBeFinal violations...");
196
197    Map<String, List<PmdViolation>> byFile =
198        violations.stream().collect(Collectors.groupingBy(v -> v.fileName));
199
200    int totalFixed = 0;
201
202    for (Map.Entry<String, List<PmdViolation>> entry : byFile.entrySet()) {
203      try {
204        String content = Files.readString(Paths.get(entry.getKey()));
205
206        // Pattern for local variable declarations
207        Pattern localVarPattern =
208            Pattern.compile(
209                "(^\\s+)((?:String|int|long|double|boolean|List|Map|Set|\\w+)(?:<[^>]+>)?)\\s+(\\w+)\\s*=",
210                Pattern.MULTILINE);
211
212        Matcher matcher = localVarPattern.matcher(content);
213        StringBuffer sb = new StringBuffer();
214        int fixCount = 0;
215
216        while (matcher.find()) {
217          String indent = matcher.group(1);
218          String type = matcher.group(2);
219          String varName = matcher.group(3);
220
221          if (!type.startsWith("final ")) {
222            matcher.appendReplacement(sb, indent + "final " + type + " " + varName + " =");
223            fixCount++;
224          }
225        }
226        matcher.appendTail(sb);
227
228        if (fixCount > 0) {
229          Files.writeString(Paths.get(entry.getKey()), sb.toString());
230          totalFixed += fixCount;
231          processedFiles.add(entry.getKey());
232        }
233
234      } catch (IOException e) {
235        System.err.println("โš ๏ธ Error processing file: " + entry.getKey() + " - " + e.getMessage());
236      }
237    }
238
239    fixCounts.put("LocalVariableCouldBeFinal", totalFixed);
240  }
241
242  private void fixInefficientEmptyStringCheck(List<PmdViolation> violations) {
243    if (violations == null || violations.isEmpty()) return;
244
245    System.out.println("๐Ÿ”„ Fixing InefficientEmptyStringCheck violations...");
246
247    int totalFixed =
248        applySimpleReplacement(
249            violations,
250            "\\.length\\(\\)\\s*==\\s*0",
251            ".isEmpty()",
252            "\\.length\\(\\)\\s*!=\\s*0",
253            "!$1.isEmpty()");
254
255    fixCounts.put("InefficientEmptyStringCheck", totalFixed);
256  }
257
258  private void fixAppendCharacterWithChar(List<PmdViolation> violations) {
259    if (violations == null || violations.isEmpty()) return;
260
261    System.out.println("๐Ÿ”„ Fixing AppendCharacterWithChar violations...");
262
263    int totalFixed = applySimpleReplacement(violations, "\\.append\\(\"(.)\"\\)", ".append('$1')");
264
265    fixCounts.put("AppendCharacterWithChar", totalFixed);
266  }
267
268  private void fixUseIndexOfChar(List<PmdViolation> violations) {
269    if (violations == null || violations.isEmpty()) return;
270
271    System.out.println("๐Ÿ”„ Fixing UseIndexOfChar violations...");
272
273    int totalFixed =
274        applySimpleReplacement(violations, "\\.indexOf\\(\"(.)\"\\)", ".indexOf('$1')");
275
276    fixCounts.put("UseIndexOfChar", totalFixed);
277  }
278
279  private void fixRedundantFieldInitializer(List<PmdViolation> violations) {
280    if (violations == null || violations.isEmpty()) return;
281
282    System.out.println("๐Ÿ”„ Fixing RedundantFieldInitializer violations...");
283
284    int totalFixed =
285        applySimpleReplacement(
286            violations, "\\s*=\\s*null;", ";", "\\s*=\\s*false;", ";", "\\s*=\\s*0;", ";");
287
288    fixCounts.put("RedundantFieldInitializer", totalFixed);
289  }
290
291  private void fixUseUnderscoresInNumericLiterals(List<PmdViolation> violations) {
292    if (violations == null || violations.isEmpty()) return;
293
294    System.out.println("๐Ÿ”„ Fixing UseUnderscoresInNumericLiterals violations...");
295
296    // This would need more sophisticated number parsing
297    // Placeholder implementation
298    fixCounts.put("UseUnderscoresInNumericLiterals", 0);
299  }
300
301  private int applySimpleReplacement(List<PmdViolation> violations, String... replacements) {
302    if (violations == null || violations.isEmpty()) return 0;
303
304    Map<String, List<PmdViolation>> byFile =
305        violations.stream().collect(Collectors.groupingBy(v -> v.fileName));
306
307    int totalFixed = 0;
308
309    for (String fileName : byFile.keySet()) {
310      try {
311        String content = Files.readString(Paths.get(fileName));
312        String originalContent = content;
313        int fileFixCount = 0;
314
315        for (int i = 0; i < replacements.length; i += 2) {
316          String pattern = replacements[i];
317          String replacement = replacements[i + 1];
318          String beforeReplacement = content;
319          content = content.replaceAll(pattern, replacement);
320
321          // Count actual replacements by checking how many times the pattern matched
322          if (!content.equals(beforeReplacement)) {
323            int matches = beforeReplacement.split(pattern, -1).length - 1;
324            fileFixCount += matches;
325          }
326        }
327
328        if (!content.equals(originalContent)) {
329          Files.writeString(Paths.get(fileName), content);
330          totalFixed += fileFixCount;
331          processedFiles.add(fileName);
332        }
333
334      } catch (IOException e) {
335        System.err.println("โš ๏ธ Error processing file: " + fileName + " - " + e.getMessage());
336      }
337    }
338
339    return totalFixed;
340  }
341
342  private void printSummary() {
343    System.out.println("\n๐Ÿ“Š Auto-Fix Summary:");
344    System.out.println("โ”".repeat(50));
345
346    int totalFixes = 0;
347    for (Map.Entry<String, Integer> entry : fixCounts.entrySet()) {
348      System.out.printf("%-35s: %3d fixes%n", entry.getKey(), entry.getValue());
349      totalFixes += entry.getValue();
350    }
351
352    System.out.println("โ”".repeat(50));
353    System.out.printf("%-35s: %3d fixes%n", "TOTAL", totalFixes);
354    System.out.printf("Files processed: %d%n", processedFiles.size());
355
356    System.out.println("\n๐Ÿงช Next steps:");
357    System.out.println("   1. Review changes: git diff");
358    System.out.println("   2. Run tests: ./gradlew test");
359    System.out.println("   3. Run PMD again: ./gradlew pmdMain");
360    System.out.println("   4. If satisfied, commit changes");
361
362    System.out.println("\n๐Ÿ PMD Auto-Fix Complete!");
363  }
364
365  private static class PmdViolation {
366    final String fileName;
367    final String rule;
368
369    PmdViolation(String fileName, String rule, int beginLine, int endLine, String message) {
370      this.fileName = fileName;
371      this.rule = rule;
372    }
373  }
374}