#!/usr/bin/env python3 """Fix all MagicNumber checkstyle violations by extracting magic numbers to named constants.""" import xml.etree.ElementTree as ET import html import re import os import sys from collections import defaultdict from pathlib import Path SKIP_NUMBERS = {-2, -1, 0, 1, 2} def parse_checkstyle_report(report_path): tree = ET.parse(report_path) root = tree.getroot() files = defaultdict(list) for f in root.findall("file"): fname = f.get("name") for e in f.findall("error"): if "MagicNumber" not in e.get("source", ""): continue line = int(e.get("line")) col = int(e.get("column")) msg = html.unescape(e.get("message")) m = re.search(r"'([^']+)'", msg) if not m: continue raw = m.group(1) try: if raw.startswith("0x") or raw.startswith("0X"): num = int(raw, 16) elif raw.endswith("L") or raw.endswith("l"): num = int(raw[:-1]) elif raw.endswith("f") or raw.endswith("F"): num = float(raw[:-1]) elif "_" in raw: num = int(raw.replace("_", "")) elif "." in raw: num = float(raw) else: num = int(raw) except ValueError: num = raw if isinstance(num, (int, float)) and num in SKIP_NUMBERS: continue files[fname].append({"line": line, "col": col, "raw": raw, "num": num}) return files def is_field_declaration(line): stripped = line.strip() if not stripped: return False if not re.match( r"(private|public|protected|static|final|volatile|transient)\s", stripped ): return False semi = stripped.find(";") paren = stripped.find("(") brace = stripped.find("{") if brace != -1 and (semi == -1 or brace < semi) and (paren == -1 or brace < paren): return False if paren != -1 and (semi == -1 or paren < semi): return False return True def find_insertion_point(lines): class_idx = -1 for i, line in enumerate(lines): if re.search(r"\b(class|interface|enum)\b", line) and "{" in line: class_idx = i break if class_idx == -1: return -1 brace_depth = 0 body_start = -1 for i in range(class_idx, len(lines)): for ch in lines[i]: if ch == "{": brace_depth += 1 elif ch == "}": brace_depth -= 1 if brace_depth > 0: body_start = i + 1 break if body_start == -1: return -1 depth = 1 last_field_end = body_start i = body_start while i < len(lines): stripped = lines[i].strip() line_opens = stripped.count("{") line_closes = stripped.count("}") depth_before = depth for ch in stripped: if ch == "{": depth += 1 elif ch == "}": depth -= 1 if depth < 1: break if depth_before == 1: if ( not stripped or stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("*") or stripped.startswith("@") ): i += 1 continue if is_field_declaration(lines[i]): j = i while j < len(lines) and ";" not in lines[j]: j += 1 last_field_end = j + 1 i = j + 1 continue break else: i += 1 continue i += 1 return last_field_end def is_in_special_method(lines, line_idx): depth = 0 for i in range(line_idx, -1, -1): stripped = lines[i].strip() depth += stripped.count("}") - stripped.count("{") if depth < 0: if re.search(r"\b(hashCode|toString|equals|compareTo)\s*\(", lines[i]): return True break return False def is_in_string_literal(lines, line_idx, col): line = lines[line_idx] in_str = False in_chr = False i = 0 while i < col - 1 and i < len(line): ch = line[i] if ch == "\\" and (in_str or in_chr): i += 2 continue if ch == '"': in_str = not in_str elif ch == "'": in_chr = not in_chr i += 1 return in_str or in_chr def extract_colors_from_line(line): results = [] for m in re.finditer( r"new\s+Color\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)", line ): results.append( { "r": int(m.group(1)), "g": int(m.group(2)), "b": int(m.group(3)), "start": m.start(), "end": m.end(), "raw": m.group(0), } ) return results COLOR_MAP = { (255, 215, 0): "COLOR_GOLD", (139, 69, 19): "COLOR_SADDLE_BROWN", (160, 82, 45): "COLOR_SIENNA", (205, 133, 63): "COLOR_PERU", (210, 180, 140): "COLOR_TAN", (128, 128, 128): "COLOR_GRAY", (200, 200, 200): "COLOR_LIGHT_GRAY", (100, 255, 150): "COLOR_LIGHT_GREEN", (52, 152, 219): "COLOR_PETER_RIVER", (255, 165, 0): "COLOR_ORANGE", (255, 0, 0): "COLOR_RED", (0, 200, 0): "COLOR_GREEN", (100, 200, 255): "COLOR_LIGHT_BLUE", (150, 150, 150): "COLOR_DARK_GRAY", (231, 76, 60): "COLOR_ALIZARIN", (255, 105, 180): "COLOR_DEEP_PINK", (255, 182, 193): "COLOR_LIGHT_PINK", (255, 192, 203): "COLOR_PINK", (255, 218, 185): "COLOR_PEACH_PUFF", (176, 196, 222): "COLOR_LIGHT_STEEL_BLUE", (100, 0, 0): "COLOR_DARK_RED", (88, 101, 242): "COLOR_BLURPLE", (46, 204, 113): "COLOR_EMERALD", (52, 73, 94): "COLOR_MIDNIGHT_BLUE", (113, 54, 138): "COLOR_INDIGO", (155, 89, 182): "COLOR_AMETHYST", (233, 30, 99): "COLOR_HOT_PINK", (180, 0, 0): "COLOR_DARK_RED_2", (255, 159, 67): "COLOR_CARROT", (241, 196, 15): "COLOR_SUN_FLOWER", (149, 165, 166): "COLOR_CONCRETE", (0, 150, 255): "COLOR_SKY_BLUE", (255, 200, 100): "COLOR_WARM_GOLD", (200, 200, 255): "COLOR_LAVENDER", (150, 100, 50): "COLOR_BRONZE", (0, 0, 0): "COLOR_BLACK", (255, 255, 255): "COLOR_WHITE", (145, 89, 182): "COLOR_WISTERIA", (0, 128, 0): "COLOR_FOREST_GREEN", (128, 0, 0): "COLOR_MAROON", (0, 0, 128): "COLOR_NAVY", } NUM_MAP = { 3: "THREE", 4: "FOUR", 5: "FIVE", 6: "SIX", 7: "SEVEN", 8: "EIGHT", 9: "NINE", 10: "TEN", 12: "TWELVE", 14: "FOURTEEN", 15: "FIFTEEN", 20: "TWENTY", 25: "TWENTY_FIVE", 30: "THIRTY", 50: "FIFTY", 60: "SIXTY", 70: "SEVENTY", 80: "EIGHTY", 90: "NINETY", 100: "HUNDRED", 200: "TWO_HUNDRED", 300: "THREE_HUNDRED", 500: "FIVE_HUNDRED", 800: "EIGHT_HUNDRED", 1000: "THOUSAND", 3000: "THREE_THOUSAND", 86400: "SECONDS_PER_DAY", 3600: "SECONDS_PER_HOUR", 60000: "MILLIS_PER_MINUTE", 600000: "SIX_HUNDRED_THOUSAND", 10000: "TEN_THOUSAND", 100000: "ONE_HUNDRED_THOUSAND", 101: "HUNDRED_PLUS_ONE", 255: "MAX_UNSIGNED_BYTE", 128: "HALF_MAX_UNSIGNED_BYTE", 65535: "MAX_UNSIGNED_SHORT", } def unique_name(base, used): name = base c = 2 while name in used: name = f"{base}_{c}" c += 1 return name def get_color_name(r, g, b, used): base = COLOR_MAP.get((r, g, b), f"COLOR_R{r}_G{g}_B{b}") return unique_name(base, used) def get_num_name(num, raw, used): if isinstance(num, (int, float)) and num in NUM_MAP: base = NUM_MAP[num] elif isinstance(num, (int, float)) and num < 0: base = f"NEGATIVE_{abs(int(num))}" elif isinstance(num, float): base = "VAL_" + str(num).replace(".", "_").replace("-", "NEG_") elif isinstance(num, int): base = f"VAL_{num}" elif isinstance(raw, str) and (raw.startswith("0x") or raw.startswith("0X")): base = "HEX_" + raw[2:].upper() else: base = ( "VAL_" + raw.replace("0x", "HEX_") .replace("0X", "HEX_") .replace("L", "") .replace("l", "") .replace("f", "") .replace("F", "") .replace(".", "_") .replace("-", "NEG_") .upper() ) base = base.replace(".", "_") if base and base[0].isdigit(): base = "VAL_" + base return unique_name(base, used) def build_word_boundary_pattern(raw): """Build a regex pattern that matches the raw number as a standalone token, not as part of a larger identifier.""" # Escape special regex characters escaped = re.escape(raw) # Use word boundary assertions - but we need to be careful because # word boundaries don't work well with all number formats. # Instead, ensure the match is surrounded by non-alphanumeric chars or line boundaries. return f"(?= len(lines): continue line = lines[line_idx] if is_in_special_method(lines, line_idx): continue # Process Color constructors colors = extract_colors_from_line(line) for c in colors: r, g, b = c["r"], c["g"], c["b"] name = get_color_name(r, g, b, used) used.add(name) consts.append((name, "Color", c["raw"])) pattern = build_word_boundary_pattern(c["raw"]) edits.append((line_idx, pattern, name)) # Process individual numbers for v in viols: col0 = v["col"] - 1 raw = v["raw"] num = v["num"] if isinstance(num, (int, float)) and num in SKIP_NUMBERS: continue # Skip if inside a Color constructor if any(c["start"] <= col0 <= c["end"] for c in colors): continue if is_in_string_literal(lines, line_idx, v["col"]): continue # Determine type and value if raw.startswith("0x") or raw.startswith("0X"): jtype = "int" value = raw elif raw.endswith("L") or raw.endswith("l"): jtype = "long" value = raw elif raw.endswith("f") or raw.endswith("F"): jtype = "float" value = raw elif isinstance(num, float): jtype = "double" value = raw elif isinstance(num, int) and (num > 2147483647 or num < -214783648): jtype = "long" value = raw + ("L" if not raw.endswith("L") else "") else: jtype = "int" value = raw name = get_num_name(num, raw, used) used.add(name) consts.append((name, jtype, value)) pattern = build_word_boundary_pattern(raw) edits.append((line_idx, pattern, name)) if not consts: return 0 # Deduplicate constants seen = set() unique = [] for name, typ, val in consts: if name not in seen: seen.add(name) unique.append((name, typ, val)) # Apply edits per line (rightmost first to preserve positions) by_line_edits = defaultdict(list) for line_idx, pattern, repl in edits: by_line_edits[line_idx].append((pattern, repl)) for line_idx, line_edits in by_line_edits.items(): line = lines[line_idx] # Find ALL matches across ALL edit patterns all_matches = [] for pattern, repl in line_edits: for m in re.finditer(pattern, line): all_matches.append((m.start(), m.end(), repl)) # Sort by start position, then by length (longer first) for same position all_matches.sort(key=lambda x: (x[0], -(x[1] - x[0]))) # Remove overlapping matches (keep longer match) filtered = [] last_end = -1 for start, end, repl in all_matches: if start >= last_end: filtered.append((start, end, repl)) last_end = end # Apply from right to left to preserve positions for start, end, repl in reversed(filtered): line = line[:start] + repl + line[end:] lines[line_idx] = line # Insert constants AFTER replacements to avoid matching inside constant names const_lines = [ f" private static final {typ} {name} = {val};\n" for name, typ, val in unique ] for i, cl in enumerate(const_lines): lines.insert(insert_idx + i, cl) with open(fname, "w", encoding="utf-8") as f: f.writelines(lines) return len(unique) def main(): project_root = Path("/storage/project/jvm/discord-bot") report_path = project_root / "build" / "reports" / "checkstyle" / "main.xml" if not report_path.exists(): print(f"ERROR: Report not found at {report_path}") sys.exit(1) files = parse_checkstyle_report(report_path) total_violations = sum(len(v) for v in files.values()) print(f"Found {total_violations} violations across {len(files)} files") total_fixed = 0 files_fixed = 0 for fname, violations in sorted(files.items()): if not os.path.exists(fname): print(f" SKIP (not found): {fname}") continue count = process_file(fname, violations) if count > 0: total_fixed += count files_fixed += 1 print( f" Fixed {count} constants in {os.path.relpath(fname, project_root)}" ) print(f"\nTotal: {total_fixed} constants added across {files_fixed} files") if __name__ == "__main__": main()