diff --git a/tools/hako_check/cli.hako b/tools/hako_check/cli.hako index bfe9ca5a..1f0165a6 100644 --- a/tools/hako_check/cli.hako +++ b/tools/hako_check/cli.hako @@ -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 replacement(MVP) + 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 {