hako_check: AST-scope rename (single file) via --rename-box / --rename-method; --fix-plan outputs refactor_plan.json + apply script skeleton; integrates with --fix-dry-run unified diff
This commit is contained in:
@ -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<Map{old,new}>
|
||||
local ren_methods = new ArrayBox() // Array<Map{box,old,new}>
|
||||
// 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 <Old> <New>"); 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 <Box> <Old> <New>"); 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<lines.size() { buf = buf + lines.get(j); if j<lines.size()-1 { buf = buf + "\n" } j=j+1 }
|
||||
return buf
|
||||
}
|
||||
_rename_method_text(text, ast, box, old, neu) {
|
||||
if text == null || box == null || old == null || neu == null { return text }
|
||||
local lines = me._split_lines(text)
|
||||
local changed = 0
|
||||
// Use AST spans to find definition line
|
||||
local mline = -1
|
||||
if ast != null {
|
||||
local boxes = ast.get("boxes")
|
||||
if boxes != null {
|
||||
local bi=0; while bi<boxes.size() {
|
||||
local b = boxes.get(bi)
|
||||
if b != null && b.get("name") == box {
|
||||
local ms = b.get("methods")
|
||||
if ms != null {
|
||||
local mi=0; while mi<ms.size() {
|
||||
local m = ms.get(mi)
|
||||
if m.get("name") == old { mline = m.get("span"); break }
|
||||
mi = mi + 1
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
bi = bi + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if mline > 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<lines.size() {
|
||||
local ln2 = lines.get(ci)
|
||||
local patt = box + "." + old + "("
|
||||
if ln2.indexOf(patt) >= 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<lines.size() { buf = buf + lines.get(j); if j<lines.size()-1 { buf = buf + "\n" } j=j+1 }
|
||||
return buf
|
||||
}
|
||||
_print_refactor_plan_json(path, ren_boxes, ren_methods) {
|
||||
// Print a small JSON describing intended refactors and a shell apply skeleton
|
||||
print("{")
|
||||
print(" \"file\": " + me._json_quote(path) + ",")
|
||||
// boxes
|
||||
print(" \"rename_boxes\": [")
|
||||
if ren_boxes != null {
|
||||
local i=0; while i<ren_boxes.size() {
|
||||
local rb = ren_boxes.get(i)
|
||||
print(" {\"old\": " + me._json_quote(rb.get("old")) + ", \"new\": " + me._json_quote(rb.get("new")) + "}" + (i<ren_boxes.size()-1?",":""))
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
print(" ],")
|
||||
// methods
|
||||
print(" \"rename_methods\": [")
|
||||
if ren_methods != null {
|
||||
local j=0; while j<ren_methods.size() {
|
||||
local rm = ren_methods.get(j)
|
||||
print(" {\"box\": " + me._json_quote(rm.get("box")) + ", \"old\": " + me._json_quote(rm.get("old")) + ", \"new\": " + me._json_quote(rm.get("new")) + "}" + (j<ren_methods.size()-1?",":""))
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
print(" ],")
|
||||
// apply script (sed-based skeleton; manual review required)
|
||||
print(" \"apply_script\": " + me._json_quote("#!/bin/sh\n# Review before apply\nFILE=\"" + path + "\"\n# Box renames\n"))
|
||||
if ren_boxes != null {
|
||||
local i2=0; while i2<ren_boxes.size() {
|
||||
local rb2=ren_boxes.get(i2)
|
||||
print(me._json_quote("sed -i -E 's/\\b" + rb2.get("old") + "\\./" + rb2.get("new") + "./g' \"$FILE\"\n") + "+")
|
||||
i2 = i2 + 1
|
||||
}
|
||||
}
|
||||
print(me._json_quote("# Method renames\n"))
|
||||
if ren_methods != null {
|
||||
local j2=0; while j2<ren_methods.size() {
|
||||
local rm2=ren_methods.get(j2)
|
||||
print(me._json_quote("sed -i -E 's/" + rm2.get("box") + "\\." + rm2.get("old") + "\\(/" + rm2.get("box") + "." + rm2.get("new") + "(/g' \"$FILE\"\n") + "+")
|
||||
j2 = j2 + 1
|
||||
}
|
||||
}
|
||||
print(me._json_quote("# Definition lines may require manual adjustment if formatting differs\n"))
|
||||
print("}")
|
||||
return 0
|
||||
}
|
||||
_needs_ast(only, skip) {
|
||||
// Parse AST when duplicate_method is explicitly requested (needs method spans/definitions precision).
|
||||
if only != null {
|
||||
|
||||
Reference in New Issue
Block a user