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:
nyash-codex
2025-11-08 23:58:42 +09:00
parent ec12094ff7
commit 2bbd4b60f7

View File

@ -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 {