diff --git a/tools/hako_check/cli.hako b/tools/hako_check/cli.hako index 9b9d5672..7802f88c 100644 --- a/tools/hako_check/cli.hako +++ b/tools/hako_check/cli.hako @@ -31,6 +31,10 @@ static box HakoAnalyzerBox { // QuickFix options (Phase 21.8) local fix_dry_run = 0 local fix_scope = "text" // reserved for future + local fix_plan = 0 + // AST rename options + local ren_boxes = new ArrayBox() // Array + local ren_methods = new ArrayBox() // Array // single-pass parse: handle options in-place and collect sources local i = 0 local fail = 0 @@ -55,6 +59,15 @@ static box HakoAnalyzerBox { if i + 1 >= args.size() { print("[lint/error] --fix-scope requires value"); return 2 } fix_scope = args.get(i+1); i = i + 2; continue } + if p == "--fix-plan" { fix_plan = 1; i = i + 1; continue } + if p == "--rename-box" { + if i + 2 >= args.size() { print("[lint/error] --rename-box requires "); return 2 } + local rec = new MapBox(); rec.set("old", args.get(i+1)); rec.set("new", args.get(i+2)); ren_boxes.push(rec); i = i + 3; continue + } + if p == "--rename-method" { + if i + 3 >= args.size() { print("[lint/error] --rename-method requires "); return 2 } + local recm = new MapBox(); recm.set("box", args.get(i+1)); recm.set("old", args.get(i+2)); recm.set("new", args.get(i+3)); ren_methods.push(recm); i = i + 4; continue + } if p == "--rules" { if i + 1 >= args.size() { print("[lint/error] --rules requires CSV"); return 2 } rules_only = me._parse_csv(args.get(i+1)); i = i + 2; continue @@ -78,7 +91,7 @@ static box HakoAnalyzerBox { text = me._sanitize(text) // Phase 21.8: QuickFix (--fix-dry-run) - if fix_dry_run == 1 { + if fix_dry_run == 1 && (ren_boxes.size() == 0 && ren_methods.size() == 0) { // Apply text-scope quick fixes in a safe subset and print unified diff local patched = me._fix_text_quick(text) if patched != null && patched != text { @@ -87,6 +100,19 @@ static box HakoAnalyzerBox { // In dry-run mode, we do not run analyzers further continue } + + // AST-scope rename (dry-run or plan) + if (ren_boxes.size() > 0 || ren_methods.size() > 0) { + if fix_plan == 1 { + me._print_refactor_plan_json(p, ren_boxes, ren_methods) + continue + } + if fix_dry_run == 1 { + local patched_ast = me._fix_ast_rename(text, p, ren_boxes, ren_methods) + if patched_ast != null && patched_ast != text { me._print_unified_diff(p, text, patched_ast) } + continue + } + } // analysis - only build IR if needed by active rules local ir = null if me._needs_ir(rules_only, rules_skip) == 1 { @@ -419,6 +445,156 @@ static box HakoAnalyzerBox { } return 0 } + + // ---- AST rename (Box/Method) within a single file ---- + _fix_ast_rename(text, path, ren_boxes, ren_methods) { + if text == null { return null } + local out = text + // Box rename first (affects definitions and calls) + if ren_boxes != null { + local i = 0 + while i < ren_boxes.size() { + local rb = ren_boxes.get(i) + out = me._rename_box_text(out, rb.get("old"), rb.get("new")) + i = i + 1 + } + } + // Method renames scoped by box, use AST spans for definitions + if ren_methods != null && ren_methods.size() > 0 { + // parse AST once to get method span lines + local ast = HakoParserCoreBox.parse(out) + local i2 = 0 + while i2 < ren_methods.size() { + local rm = ren_methods.get(i2) + out = me._rename_method_text(out, ast, rm.get("box"), rm.get("old"), rm.get("new")) + i2 = i2 + 1 + } + } + return out + } + _rename_box_text(text, old, neu) { + if text == null || old == null || neu == null { return text } + local lines = me._split_lines(text) + local changed = 0 + // Def line: static box Old { → replace Old only after the prefix + local i = 0 + while i < lines.size() { + local ln = lines.get(i) + local pref = "static box " + old + " " + if me._ltrim(ln).indexOf(pref) == 0 { + lines.set(i, ln.replace(pref, "static box " + neu + " ")) + changed = 1 + } + // Calls: occurrences of "Old." → "New." when "Old." is found + if ln.indexOf(old + ".") >= 0 { + // simplistic boundary: avoid replacing inside quoted strings is out of scope (MVP) + lines.set(i, ln.replace(old + ".", neu + ".")) + if lines.get(i) != ln { changed = 1 } + } + i = i + 1 + } + if changed == 0 { return text } + // join + local j=0; local buf=""; while j 0 { + local idx = mline - 1 + if idx >= 0 && idx < lines.size() { + local ln = lines.get(idx) + // Replace 'method old(' with 'method new(' + local marker = "method " + old + "(" + if ln.indexOf(marker) >= 0 { + lines.set(idx, ln.replace(marker, "method " + neu + "(")) + changed = 1 + } + } + } + // Update calls: box.old( → box.new( + local ci=0; while ci= 0 { + lines.set(ci, ln2.replace(patt, box + "." + neu + "(")) + if lines.get(ci) != ln2 { changed = 1 } + } + ci = ci + 1 + } + if changed == 0 { return text } + local j=0; local buf=""; while j