#!/usr/bin/env python3 """Add 'final' to local variables that are not reassigned.""" 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"): if "FinalLocalVariable" in v.get("source", ""): violations[fname].append(int(v.get("line", 0))) return violations def is_reassigned(var_name, lines, start_line, end_line): """Check if variable is reassigned after declaration.""" for i in range(start_line, min(end_line, len(lines))): line = lines[i].strip() # Skip comments if line.startswith("//") or line.startswith("/*") or line.startswith("*"): continue # Check for reassignment: varName = ..., varName +=, varName++, etc. if re.match(rf"\b{re.escape(var_name)}\s*[\+\-\*\/]?=(?!=)", line): return True if re.match(rf"\b{re.escape(var_name)}\+\+", line) or re.match( rf"\b{re.escape(var_name)}\-\-", line ): return True return False def find_method_end(lines, start_line): """Find the end of the method containing start_line.""" depth = 0 for i in range(start_line - 1, len(lines)): for ch in lines[i]: if ch == "{": depth += 1 elif ch == "}": depth -= 1 if depth == 0: return i + 1 return len(lines) def fix_file(filepath, line_numbers): try: lines = Path(filepath).read_text(encoding="utf-8").splitlines(keepends=True) except Exception as e: print(f" SKIP {filepath}: {e}") return 0 fixed = 0 for line_num in sorted(set(line_numbers)): if line_num < 1 or line_num > len(lines): continue line = lines[line_num - 1] # Skip if already has final if re.search(r"\bfinal\s+", line): continue # Match variable declaration patterns # Pattern: type varName = ...; m = re.match(r"^(\s*)((?:final\s+)?(?:[\w<>\[\], ?]+?))\s+(\w+)\s*=", line) if m: indent = m.group(1) type_part = m.group(2).strip() var_name = m.group(3) # Skip if it's a for-loop variable if "for" in line: continue # Skip if already final if type_part.startswith("final "): continue # Check if variable is reassigned method_end = find_method_end(lines, line_num) if not is_reassigned(var_name, lines, line_num + 1, method_end): new_line = f"{indent}final {type_part} {var_name}" # Preserve the rest of the line after varName rest = line[m.end() :] lines[line_num - 1] = f"{new_line}{rest}\n" fixed += 1 continue # Pattern: type varName; (no initializer) m = re.match(r"^(\s*)((?:final\s+)?(?:[\w<>\[\], ?]+?))\s+(\w+)\s*;", line) if m: indent = m.group(1) type_part = m.group(2).strip() var_name = m.group(3) if "for" in line or type_part.startswith("final "): continue method_end = find_method_end(lines, line_num) if not is_reassigned(var_name, lines, line_num + 1, method_end): lines[line_num - 1] = f"{indent}final {type_part} {var_name};\n" fixed += 1 continue # Pattern: for (type varName : ...) or for (type varName = ...) if "for" in line and "(" in line: m = re.search(r"for\s*\(\s*(?:final\s+)?([\w<>\[\], ?]+?)\s+(\w+)", line) if m: type_part = m.group(1).strip() var_name = m.group(2) if not re.search(r"\bfinal\s+" + re.escape(type_part), line): new_line = re.sub( rf"(for\s*\(\s*)({re.escape(type_part)})", rf"\1final \2", line ) if new_line != line: lines[line_num - 1] = new_line fixed += 1 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} FinalLocalVariable violations in {len(violations)} files") fixed_files = 0 fixed_count = 0 for filepath, line_nums in sorted(violations.items()): n = fix_file(filepath, line_nums) 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()