001package com.streamconverter.analysis; 002 003import java.io.IOException; 004import java.nio.file.Files; 005import java.nio.file.Path; 006import java.nio.file.Paths; 007import java.util.*; 008import java.util.stream.Collectors; 009import javax.xml.parsers.DocumentBuilder; 010import javax.xml.parsers.DocumentBuilderFactory; 011import org.slf4j.Logger; 012import org.slf4j.LoggerFactory; 013import org.w3c.dom.Document; 014import org.w3c.dom.Element; 015import org.w3c.dom.NodeList; 016 017/** 018 * PMD XMLレポートをAI可読形式に変換するユーティリティクラス 019 * 020 * <p>PMDの生XMLレポートは構造化されているが冗長で、AI解析には適していない。 このクラスは以下の形式に変換する: 021 * 022 * <ul> 023 * <li>Markdown要約レポート - AI可読性の高いサマリー 024 * <li>CSVレポート - スプレッドシート分析用 025 * <li>JSON構造化レポート - プログラム解析用 026 * </ul> 027 */ 028public class PmdReportConverter { 029 private static final Logger LOG = LoggerFactory.getLogger(PmdReportConverter.class); 030 031 /** Creates a new converter. */ 032 public PmdReportConverter() {} 033 034 /** 035 * CLI entry point for converting a PMD XML report. 036 * 037 * @param args arguments where args[0] is the XML file and args[1] is the optional output 038 * directory 039 * @throws Exception if conversion fails 040 */ 041 public static void main(String[] args) throws Exception { 042 if (args.length < 1) { 043 if (LOG.isErrorEnabled()) { 044 LOG.error("Usage: PmdReportConverter <pmd-xml-file> [output-dir]"); 045 } 046 System.exit(1); 047 } 048 049 String xmlFile = args[0]; 050 String outputDir = args.length > 1 ? args[1] : "build/reports/pmd/converted"; 051 052 PmdReportConverter converter = new PmdReportConverter(); 053 converter.convertReport(xmlFile, outputDir); 054 } 055 056 /** 057 * Converts the given PMD XML report into multiple formats. 058 * 059 * @param xmlFilePath path to the PMD XML file 060 * @param outputDir directory where converted reports will be written 061 * @throws Exception if processing fails 062 */ 063 public void convertReport(String xmlFilePath, String outputDir) throws Exception { 064 Path xmlPath = Paths.get(xmlFilePath); 065 Path outputPath = Paths.get(outputDir); 066 Files.createDirectories(outputPath); 067 068 // XML解析 069 List<PmdViolation> violations = parseXmlReport(xmlPath); 070 071 // 各形式で出力 072 generateMarkdownReport(violations, outputPath.resolve("pmd-summary.md")); 073 generateCsvReport(violations, outputPath.resolve("pmd-violations.csv")); 074 generateJsonReport(violations, outputPath.resolve("pmd-report.json")); 075 076 if (LOG.isInfoEnabled()) { 077 LOG.info("✅ PMD報告書変換完了:"); 078 } 079 if (LOG.isInfoEnabled()) { 080 LOG.info(" 📄 Markdown: {}", outputPath.resolve("pmd-summary.md")); 081 } 082 if (LOG.isInfoEnabled()) { 083 LOG.info(" 📊 CSV: {}", outputPath.resolve("pmd-violations.csv")); 084 } 085 if (LOG.isInfoEnabled()) { 086 LOG.info(" 🔗 JSON: {}", outputPath.resolve("pmd-report.json")); 087 } 088 } 089 090 private List<PmdViolation> parseXmlReport(Path xmlPath) throws Exception { 091 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 092 Document doc = builder.parse(xmlPath.toFile()); 093 094 NodeList fileNodes = doc.getElementsByTagName("file"); 095 List<PmdViolation> violations = new ArrayList<>(); 096 097 for (int i = 0; i < fileNodes.getLength(); i++) { 098 Element fileElement = (Element) fileNodes.item(i); 099 String fileName = fileElement.getAttribute("name"); 100 101 NodeList violationNodes = fileElement.getElementsByTagName("violation"); 102 for (int j = 0; j < violationNodes.getLength(); j++) { 103 Element violationElement = (Element) violationNodes.item(j); 104 105 PmdViolation violation = 106 new PmdViolation( 107 extractRelativePath(fileName), 108 Integer.parseInt(violationElement.getAttribute("beginline")), 109 violationElement.getAttribute("rule"), 110 violationElement.getAttribute("ruleset"), 111 Integer.parseInt(violationElement.getAttribute("priority")), 112 violationElement.getTextContent().trim(), 113 violationElement.getAttribute("class"), 114 violationElement.getAttribute("method"), 115 violationElement.getAttribute("variable")); 116 violations.add(violation); 117 } 118 } 119 120 return violations; 121 } 122 123 private String extractRelativePath(String fullPath) { 124 // StreamConverterプロジェクト内の相対パスを抽出 125 int index = fullPath.indexOf("streamconverter-"); 126 return index != -1 ? fullPath.substring(index) : fullPath; 127 } 128 129 private void generateMarkdownReport(List<PmdViolation> violations, Path outputPath) 130 throws IOException { 131 StringBuilder md = new StringBuilder(); 132 133 md.append("# PMD Code Quality Analysis Report\n\n"); 134 md.append("**Generated**: ").append(new Date()).append("\n"); 135 md.append("**Total Violations**: ").append(violations.size()).append("\n\n"); 136 137 // 違反数上位のルール 138 Map<String, Long> ruleStats = 139 violations.stream() 140 .collect(Collectors.groupingBy(PmdViolation::rule, Collectors.counting())); 141 142 md.append("## 🎯 Top Code Smell Rules\n\n"); 143 md.append("| Rank | Rule | Count | Category |\n"); 144 md.append("|------|------|-------|----------|\n"); 145 146 ruleStats.entrySet().stream() 147 .sorted(Map.Entry.<String, Long>comparingByValue().reversed()) 148 .limit(20) 149 .forEach( 150 entry -> { 151 String category = 152 violations.stream() 153 .filter(v -> v.rule().equals(entry.getKey())) 154 .findFirst() 155 .map(PmdViolation::ruleset) 156 .orElse("Unknown"); 157 md.append( 158 String.format( 159 "| | %s | %d | %s |\n", entry.getKey(), entry.getValue(), category)); 160 }); 161 162 // ファイル別統計 163 Map<String, Long> fileStats = 164 violations.stream() 165 .collect(Collectors.groupingBy(PmdViolation::file, Collectors.counting())); 166 167 md.append("\n## 📁 Files with Most Issues\n\n"); 168 md.append("| File | Violations |\n"); 169 md.append("|------|------------|\n"); 170 171 fileStats.entrySet().stream() 172 .sorted(Map.Entry.<String, Long>comparingByValue().reversed()) 173 .limit(15) 174 .forEach( 175 entry -> { 176 md.append( 177 String.format( 178 "| %s | %d |\n", 179 entry.getKey().replaceAll(".*/(\\w+\\.java)", "$1"), entry.getValue())); 180 }); 181 182 // 優先度別統計 183 Map<Integer, Long> priorityStats = 184 violations.stream() 185 .collect(Collectors.groupingBy(PmdViolation::priority, Collectors.counting())); 186 187 md.append("\n## ⚡ Priority Distribution\n\n"); 188 md.append("| Priority | Count | Description |\n"); 189 md.append("|----------|-------|-------------|\n"); 190 priorityStats.entrySet().stream() 191 .sorted(Map.Entry.comparingByKey()) 192 .forEach( 193 entry -> { 194 String desc = 195 switch (entry.getKey()) { 196 case 1 -> "🔴 High - Critical issues"; 197 case 2 -> "🟡 Medium - Important issues"; 198 case 3 -> "🟢 Low - Minor issues"; 199 case 4 -> "ℹ️ Info - Informational"; 200 default -> "❓ Unknown"; 201 }; 202 md.append( 203 String.format("| %d | %d | %s |\n", entry.getKey(), entry.getValue(), desc)); 204 }); 205 206 Files.writeString(outputPath, md.toString()); 207 } 208 209 private void generateCsvReport(List<PmdViolation> violations, Path outputPath) 210 throws IOException { 211 StringBuilder csv = new StringBuilder(); 212 csv.append("File,Line,Rule,Category,Priority,Description,Class,Method,Variable\n"); 213 214 violations.forEach( 215 v -> { 216 csv.append( 217 String.format( 218 "\"%s\",%d,\"%s\",\"%s\",%d,\"%s\",\"%s\",\"%s\",\"%s\"\n", 219 v.file(), 220 v.line(), 221 v.rule(), 222 v.ruleset(), 223 v.priority(), 224 v.description().replace("\"", "\"\""), 225 v.className(), 226 v.method(), 227 v.variable())); 228 }); 229 230 Files.writeString(outputPath, csv.toString()); 231 } 232 233 private void generateJsonReport(List<PmdViolation> violations, Path outputPath) 234 throws IOException { 235 StringBuilder json = new StringBuilder(); 236 json.append("{\n"); 237 json.append(" \"summary\": {\n"); 238 json.append(" \"totalViolations\": ").append(violations.size()).append(",\n"); 239 json.append(" \"generatedAt\": \"").append(new Date()).append("\"\n"); 240 json.append(" },\n"); 241 json.append(" \"violations\": [\n"); 242 243 for (int i = 0; i < violations.size(); i++) { 244 PmdViolation v = violations.get(i); 245 json.append(" {\n"); 246 json.append(" \"file\": \"").append(v.file()).append("\",\n"); 247 json.append(" \"line\": ").append(v.line()).append(",\n"); 248 json.append(" \"rule\": \"").append(v.rule()).append("\",\n"); 249 json.append(" \"category\": \"").append(v.ruleset()).append("\",\n"); 250 json.append(" \"priority\": ").append(v.priority()).append(",\n"); 251 json.append(" \"description\": \"") 252 .append(v.description().replace("\"", "\\\"")) 253 .append("\",\n"); 254 json.append(" \"class\": \"").append(v.className()).append("\",\n"); 255 json.append(" \"method\": \"").append(v.method()).append("\",\n"); 256 json.append(" \"variable\": \"").append(v.variable()).append("\"\n"); 257 json.append(" }").append(i < violations.size() - 1 ? "," : "").append("\n"); 258 } 259 260 json.append(" ]\n"); 261 json.append("}\n"); 262 263 Files.writeString(outputPath, json.toString()); 264 } 265 266 /** 267 * PMD違反情報を表すレコードクラス 268 * 269 * @param file ファイルパス 270 * @param line 行番号 271 * @param rule 違反ルール 272 * @param ruleset ルールセット 273 * @param priority 優先度 274 * @param description 説明 275 * @param className クラス名 276 * @param method メソッド名 277 * @param variable 変数名 278 */ 279 public record PmdViolation( 280 String file, 281 int line, 282 String rule, 283 String ruleset, 284 int priority, 285 String description, 286 String className, 287 String method, 288 String variable) {} 289}