#!/usr/bin/env python3 """Auto-fix checkstyle violations from XML report - improved version.""" import re import sys import xml.etree.ElementTree as ET from collections import defaultdict from pathlib import Path def parse_report(xml_path): tree = ET.parse(xml_path) root = tree.getroot() violations = defaultdict(list) for file_elem in root.findall("file"): fname = file_elem.get("name") for v in file_elem.findall("error"): sev = v.get("severity", "warning") if sev == "ignore": continue violations[fname].append( { "line": int(v.get("line", 0)), "column": int(v.get("column", 0)), "source": v.get("source", ""), "message": v.get("message", ""), } ) return violations def fix_file(filepath, violations): try: content = Path(filepath).read_text(encoding="utf-8") lines = content.splitlines(keepends=True) except Exception as e: print(f" SKIP {filepath}: {e}") return 0 fixed = 0 is_interface = re.search(r"\binterface\s+\w+", content) is not None by_line = defaultdict(list) for v in violations: by_line[v["line"]].append(v) for line_num in sorted(by_line.keys(), reverse=True): if line_num < 1 or line_num > len(lines): continue line = lines[line_num - 1] original = line for v in by_line[line_num]: source = v["source"] # RedundantModifier in interfaces if "RedundantModifierCheck" in source and is_interface: # Remove 'final' from interface fields (implicitly public static final) new_line = re.sub(r"\bfinal\b\s+", "", line) # Remove 'public' from interface methods/fields new_line = re.sub(r"\bpublic\b\s+", "", new_line) # Remove 'static' from interface fields new_line = re.sub(r"\bstatic\b\s+final\b", "final", new_line) if new_line != line: lines[line_num - 1] = new_line line = new_line fixed += 1 break # RedundantModifier in general (e.g. 'final' on enum fields) if ( "RedundantModifierCheck" in source and "interface" not in content[line_num - 10 : line_num + 10] ): new_line = re.sub(r"\bpublic\b\s+(?=static\s+final\b)", "", line) if new_line != line: lines[line_num - 1] = new_line line = new_line fixed += 1 break # WhitespaceAround: add space around { and } if "WhitespaceAroundCheck" in source: msg = v["message"] new_line = line if "{" in msg or "}" in msg: if not line.strip().startswith( ("import ", "package ", "@", "//", "/*", "*", '"') ): # Add space before { new_line = re.sub(r"(\S)\{", r"\1 {", new_line) # Add space after { new_line = re.sub(r"\{(\S)", r"{ \1", new_line) # Don't add space after { in empty blocks {} new_line = re.sub(r"\{ \}", "{}", new_line) # Add space before } new_line = re.sub(r"(\S)\}", r"\1 }", new_line) # Add space after } new_line = re.sub(r"\}(\S)", r"} \1", new_line) if new_line != line: lines[line_num - 1] = new_line line = new_line fixed += 1 break # NeedBraces: add braces to single-line if/else/for/while if "NeedBracesCheck" in source: stripped = line.strip() indent = line[: len(line) - len(line.lstrip())] # if (...) singleStatement; m = re.match(r"^(if\s*\(.+\))\s+(.+;)\s*$", stripped) if m: lines[line_num - 1] = ( f"{indent}{m.group(1)} {{\n{indent} {m.group(2)}\n{indent}}}\n" ) fixed += 1 break # else singleStatement m = re.match(r"^else\s+(.+;)\s*$", stripped) if m: lines[line_num - 1] = ( f"{indent}else {{\n{indent} {m.group(1)}\n{indent}}}\n" ) fixed += 1 break # else { ... } but missing braces on else itself m = re.match(r"^else\s*\{", stripped) if not m and "else" in stripped: m2 = re.match(r"^else\s+(\w.+)", stripped) if m2: lines[line_num - 1] = ( f"{indent}else {{\n{indent} {m2.group(1)}\n{indent}}}\n" ) fixed += 1 break # for/while single line m = re.match(r"^(for\s*\(.+\)|while\s*\(.+\))\s+(.+;)\s*$", stripped) if m: lines[line_num - 1] = ( f"{indent}{m.group(1)} {{\n{indent} {m.group(2)}\n{indent}}}\n" ) fixed += 1 break # EqualsAvoidNull: "literal".equals(var) -> var.equals("literal") if "EqualsAvoidNullCheck" in source: m = re.match(r'^(\s*)"([^"]+)"\.equals\((.+)\)\s*;', line) if m: indent, literal, var = m.groups() lines[line_num - 1] = f'{indent}{var}.equals("{literal}");\n' fixed += 1 break # MultipleVariableDeclarations: split on one line if "MultipleVariableDeclarationsCheck" in source: m = re.match(r"^(\s*)(\w+(?:<[^>]+>)?)\s+(.+);", line) if m and "," in m.group(3): indent = m.group(1) type_ = m.group(2) decls = m.group(3) parts = [d.strip() for d in decls.split(",")] new_lines = "".join(f"{indent}{type_} {p};\n" for p in parts) lines[line_num - 1] = new_lines fixed += 1 break # MissingSwitchDefault if "MissingSwitchDefaultCheck" in source: # Find the end of the switch block and add default # Look for the closing } of the switch depth = 0 for i in range(line_num - 1, min(line_num + 50, len(lines))): l = lines[i] depth += l.count("{") - l.count("}") if depth == 0 and i > line_num - 1: # Found the end of the switch, add default before closing } indent = line[: len(line) - len(line.lstrip())] case_indent = indent + " " lines[i] = ( f"{case_indent}default:\n{case_indent} break;\n{lines[i]}" ) fixed += 1 break if fixed > 0: Path(filepath).write_text("".join(lines), encoding="utf-8") return fixed def main(): xml_path = sys.argv[1] if len(sys.argv) > 1 else "build/reports/checkstyle/main.xml" violations = parse_report(xml_path) total = sum(len(v) for v in violations.values()) print(f"Found {total} violations in {len(violations)} files") fixed_files = 0 fixed_count = 0 for filepath, viols in sorted(violations.items()): n = fix_file(filepath, viols) if n > 0: fixed_files += 1 fixed_count += n print( f" Fixed {n} in {filepath.replace('/storage/project/jvm/discord-bot/', '')}" ) print(f"\nTotal: fixed {fixed_count} violations in {fixed_files} files") if __name__ == "__main__": main()