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.)
507 lines
15 KiB
Python
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()
|