hako_check: add --fix-dry-run (MVP text scope) for HC003 using→quoted; emit minimal unified diff

This commit is contained in:
nyash-codex
2025-11-08 23:50:31 +09:00
parent fa3091061d
commit 1dcc944361

View File

@ -28,6 +28,9 @@ static box HakoAnalyzerBox {
local debug = 0
// Default: AST path OFF (enable with --force-ast)
local no_ast = 1
// QuickFix options (Phase 21.8)
local fix_dry_run = 0
local fix_scope = "text" // reserved for future
// single-pass parse: handle options in-place and collect sources
local i = 0
local fail = 0
@ -47,6 +50,11 @@ static box HakoAnalyzerBox {
if i + 1 >= args.size() { print("[lint/error] --format requires value"); return 2 }
fmt = args.get(i+1); i = i + 2; continue
}
if p == "--fix-dry-run" { fix_dry_run = 1; i = i + 1; continue }
if p == "--fix-scope" {
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 == "--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
@ -68,6 +76,17 @@ static box HakoAnalyzerBox {
local text_raw = text
// pre-sanitize (ASCII quotes, normalize newlines) - minimal & reversible
text = me._sanitize(text)
// Phase 21.8: QuickFix (--fix-dry-run)
if fix_dry_run == 1 {
// 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 {
me._print_unified_diff(p, text, patched)
}
// In dry-run mode, we do not run analyzers further
continue
}
// analysis - only build IR if needed by active rules
local ir = null
if me._needs_ir(rules_only, rules_skip) == 1 {
@ -230,6 +249,74 @@ static box HakoAnalyzerBox {
print("]}")
return 0
}
// ---- QuickFix (text scope, MVP: HC003 using quoted) ----
_fix_text_quick(text) {
if text == null { return null }
// Only HC003: using 非引用→引用化
local lines = me._split_lines(text)
local changed = 0
local out = new ArrayBox()
local i = 0
while i < lines.size() {
local ln = lines.get(i)
local ltrim = me._ltrim(ln)
if ltrim.indexOf("using ") == 0 && ltrim.indexOf('using "') != 0 {
// rewrite: using X ... -> using "X" ...X は最初の空白/セミコロン/"as" まで)
local prefix_len = ln.indexOf("using ")
if prefix_len < 0 { prefix_len = 0 }
local head = ln.substring(0, prefix_len)
local rest = ln.substring(prefix_len)
// rest starts with 'using '
local after = rest.substring(6)
// Find boundary
local b = 0; local n = after.length()
while b < n {
local ch = after.substring(b,b+1)
// stop at space, semicolon or before ' as '
if ch == " " || ch == ";" { break }
b = b + 1
}
local name = after.substring(0,b)
local tail = after.substring(b)
// If tail starts with ' as ' but name had trailing spaces, we already stopped at space
// Wrap name with quotes if not already
if name.length() > 0 {
local newline = head + "using \"" + name + "\"" + tail
if newline != ln { changed = 1; out.push(newline) } else { out.push(ln) }
} else { out.push(ln) }
} else {
out.push(ln)
}
i = i + 1
}
if changed == 0 { return text }
// Join with \n
local j = 0; local buf = ""; while j < out.size() { buf = buf + out.get(j); if j < out.size()-1 { buf = buf + "\n" } j = j + 1 }
return buf
}
_print_unified_diff(path, before, after) {
if before == null || after == null || before == after { return 0 }
// Minimal unified diff for whole file replacementMVP
print("--- " + path)
print("+++ " + path)
print("@@")
// Print lines with +/-; this MVP prints only changed lines as + and original as - when they differ line-wise
local bl = me._split_lines(before); local al = me._split_lines(after)
local max = bl.size(); if al.size() > max { max = al.size() }
local i = 0
while i < max {
local b = i < bl.size() ? bl.get(i) : null
local a = i < al.size() ? al.get(i) : null
if b == a {
print(" " + (a==null?"":a))
} else {
if b != null { print("-" + b) }
if a != null { print("+" + a) }
}
i = i + 1
}
return 0
}
_needs_ast(only, skip) {
// Parse AST when duplicate_method is explicitly requested (needs method spans/definitions precision).
if only != null {