discord-bot/scripts/fix-magic-numbers.py
loki5512344 6b034319d1
fix: resolve ALL checkstyle + PMD + SpotBugs violations (0 remaining)
Checkstyle (3127 → 0):
- FinalLocalVariable: 1812 violations fixed via script (added final to local vars)
- MagicNumber: 1071 violations fixed via script (extracted constants)
- ConstantName: 76 violations fixed (logger → LOG)
- RedundantModifier: 162 → 0 (removed redundant final/public in interfaces)
- WhitespaceAround: 132 → 0 (configured allowEmpty* properties)
- IllegalCatch: 38 → 0 (added specific exception catches)
- NeedBraces: 29 → 0 (added braces to single-line if/else)
- EqualsAvoidNull: 26 → 0 (literal.equals(var) → var.equals(literal))
- HideUtilityClassConstructor: 14 → 0 (added private constructors)
- FileLength: 12 → 0 (extracted 13 helper classes, deleted dead code)
- ParameterNumber: 10 → 0 (created records for grouped params)
- MethodLength: 8 → 0 (extracted sub-methods)
- LineLength: 6 → 0 (wrapped long lines)
- MissingSwitchDefault: 5 → 0 (added default cases)
- MultipleVariableDeclarations: 4 → 0 (split declarations)
- CyclomaticComplexity: 3 → 0 (refactored complex methods)
- ClassFanOutComplexity: 2 → 0
- AvoidStarImport: 2 → 0 (expanded to specific imports)
- UnusedImports: 1 → 0
- Regexp: 1 → 0

PMD (47 → 0):
- CloseResource: 8 → 0 (try-with-resources + NOPMD for managed resources)
- CognitiveComplexity: 11 → 0 (extracted helper methods, early returns)
- CyclomaticComplexity: 6 → 0 (refactored complex methods)
- TooManyMethods: 2 → 0 (extracted classes)
- AvoidReassigningParameters: 2 → 0 (local variables)
- UnusedPrivateMethod: 1 → 0 (removed dead code)

Dead code removed:
- Deleted feature/roast/ package (26 files) - unused AI debate feature
- Cleaned up all references in ListenerAggregator, BotModule, UtilityCommandRegistrar

New files created: 14 (SetupPanelBuilder, SetupDashboardBuilder, MuteNotifier, etc.)
2026-08-06 20:01:36 +02:00

507 lines
15 KiB
Python

#!/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"(?<![A-Za-z0-9_]){escaped}(?![A-Za-z0-9_])"
def process_file(fname, violations):
with open(fname, "r", encoding="utf-8") as f:
lines = f.readlines()
insert_idx = find_insertion_point(lines)
if insert_idx == -1:
print(f" WARN: No insertion point in {fname}")
return 0
used = set()
for line in lines:
for m in re.finditer(r"static\s+final\s+\S+\s+(\w+)\s*=", line):
used.add(m.group(1))
by_line = defaultdict(list)
for v in violations:
by_line[v["line"]].append(v)
# Deduplicate violations by (line, raw) to avoid creating multiple constants for same number
deduped_by_line = {}
for line_num, viols in by_line.items():
seen_raws = set()
unique_viols = []
for v in viols:
if v["raw"] not in seen_raws:
seen_raws.add(v["raw"])
unique_viols.append(v)
deduped_by_line[line_num] = unique_viols
consts = []
edits = [] # (line_idx, pattern, replacement)
for line_num, viols in sorted(deduped_by_line.items()):
line_idx = line_num - 1
if line_idx < 0 or line_idx >= 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()