Skip to content

Commit d0fa32a

Browse files
authored
Add autofix engine for code issue resolution
1 parent 2211b33 commit d0fa32a

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

tools/fixers/autofix_engine.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from __future__ import annotations
4+
import argparse, importlib, json
5+
from dataclasses import dataclass, asdict
6+
from pathlib import Path
7+
from typing import Any, Dict, List
8+
9+
SCOPE_PREFIXES = ("src/", "include/", "docs/", "examples/", "tests/", "benchmarks/")
10+
EXCLUDE_PARTS = ("third_party/", "vcpkg", "build", "releases/")
11+
12+
@dataclass
13+
class Finding:
14+
file: str
15+
line: int
16+
type: str
17+
severity: str
18+
message: str
19+
20+
@dataclass
21+
class FixResult:
22+
status: str # fixed|skipped|unsafe
23+
reason: str
24+
file: str
25+
line: int
26+
type: str
27+
before: str = ""
28+
after: str = ""
29+
30+
FIXER_MAP = {
31+
"docs_broken_markdown_link": "tools.fixers.markdown_link_fixer",
32+
"string_concat_loop": "tools.fixers.string_concat_loop_fixer",
33+
"hardcoded_output": "tools.fixers.logging_fixer",
34+
"unstructured_log": "tools.fixers.logging_fixer",
35+
"missing_doxygen_comment": "tools.fixers.doxygen_fixer_adapter",
36+
"missing_doxygen_brief": "tools.fixers.doxygen_fixer_adapter",
37+
"missing_doxygen_param": "tools.fixers.doxygen_fixer_adapter",
38+
"missing_doxygen_return": "tools.fixers.doxygen_fixer_adapter",
39+
}
40+
41+
def in_scope(path: str) -> bool:
42+
if any(x in path for x in EXCLUDE_PARTS):
43+
return False
44+
return path.startswith(SCOPE_PREFIXES)
45+
46+
def load_findings(p: Path) -> List[Finding]:
47+
data = json.loads(p.read_text(encoding="utf-8"))
48+
out: List[Finding] = []
49+
for x in data:
50+
out.append(Finding(
51+
file=x.get("file", ""),
52+
line=int(x.get("line", 1)),
53+
type=x.get("type", ""),
54+
severity=x.get("severity", "MEDIUM"),
55+
message=x.get("message", "")
56+
))
57+
return out
58+
59+
def run():
60+
ap = argparse.ArgumentParser()
61+
ap.add_argument("--root", default=".")
62+
ap.add_argument("--findings", default="ai_working/gap_scan_results.json")
63+
ap.add_argument("--types", default="", help="comma-separated whitelist")
64+
ap.add_argument("--check-only", action="store_true")
65+
ap.add_argument("--apply", action="store_true")
66+
ap.add_argument("--report", default="ai_working/autofix_report.json")
67+
ap.add_argument("--max-risk", default="medium", choices=["low", "medium", "high"])
68+
args = ap.parse_args()
69+
70+
if not args.check_only and not args.apply:
71+
raise SystemExit("Use --check-only or --apply")
72+
73+
root = Path(args.root).resolve()
74+
findings_path = (root / args.findings).resolve()
75+
findings = load_findings(findings_path)
76+
77+
allow = set(x.strip() for x in args.types.split(",") if x.strip())
78+
results: List[FixResult] = []
79+
doxygen_batch: List[Finding] = []
80+
81+
for f in findings:
82+
if allow and f.type not in allow:
83+
continue
84+
if not in_scope(f.file):
85+
continue
86+
87+
mod_name = FIXER_MAP.get(f.type)
88+
if not mod_name:
89+
results.append(FixResult("skipped", "no_fixer_registered", f.file, f.line, f.type))
90+
continue
91+
92+
if mod_name.endswith("doxygen_fixer_adapter"):
93+
doxygen_batch.append(f)
94+
continue
95+
96+
mod = importlib.import_module(mod_name)
97+
r = mod.fix(root=root, finding=asdict(f), check_only=args.check_only)
98+
results.append(FixResult(**r))
99+
100+
# one-shot doxygen adapter call
101+
if doxygen_batch:
102+
mod = importlib.import_module("tools.fixers.doxygen_fixer_adapter")
103+
batch_res = mod.fix_batch(root=root, findings=[asdict(x) for x in doxygen_batch], check_only=args.check_only)
104+
for r in batch_res:
105+
results.append(FixResult(**r))
106+
107+
summary = {"fixed": 0, "skipped": 0, "unsafe": 0}
108+
by_type: Dict[str, Dict[str, int]] = {}
109+
for r in results:
110+
summary[r.status] = summary.get(r.status, 0) + 1
111+
by_type.setdefault(r.type, {"fixed": 0, "skipped": 0, "unsafe": 0})
112+
by_type[r.type][r.status] += 1
113+
114+
out = {
115+
"summary": summary,
116+
"by_type": by_type,
117+
"results": [asdict(x) for x in results],
118+
}
119+
rp = (root / args.report).resolve()
120+
rp.parent.mkdir(parents=True, exist_ok=True)
121+
rp.write_text(json.dumps(out, indent=2, ensure_ascii=False), encoding="utf-8")
122+
123+
print(json.dumps(summary, indent=2))
124+
if args.check_only and summary["fixed"] > 0:
125+
raise SystemExit(1)
126+
127+
if __name__ == "__main__":
128+
run()

0 commit comments

Comments
 (0)