1use aiproof_config::Config;
3use aiproof_core::rule::Ctx;
4use std::path::PathBuf;
5
6pub struct FixOutcome {
7 pub fixes_applied: usize,
8 pub files_changed: usize,
9}
10
11pub fn run_fix(
12 paths: &[PathBuf],
13 config: &Config,
14 unsafe_fixes: bool,
15) -> anyhow::Result<FixOutcome> {
16 let files = crate::discovery::discover(paths, config)?;
17 let rules = crate::run::filtered_rules(config);
18 let ctx = Ctx {
19 target_models: config.target_models.as_slice(),
20 max_tokens_budget: config.max_tokens_budget,
21 };
22
23 let mut fixes_applied = 0;
24 let mut files_changed = 0;
25
26 for f in files {
27 let Ok(mut source) = std::fs::read_to_string(&f.path) else {
28 continue;
29 };
30 let original = source.clone();
31
32 for _ in 0..10 {
34 let docs = match aiproof_parse::parse_file(&f.path, &source) {
35 Ok(d) => d,
36 Err(_) => break,
37 };
38 let mut edits_for_pass = Vec::new();
39 for doc in &docs {
40 for rule in &rules {
41 for diag in rule.check(doc, &ctx) {
42 if let Some(fix) = rule.autofix(&diag, doc) {
43 if !unsafe_fixes && !fix.safe {
44 continue;
45 }
46 edits_for_pass.extend(fix.edits);
47 }
48 }
49 }
50 }
51 if edits_for_pass.is_empty() {
52 break;
53 }
54
55 edits_for_pass.sort_by_key(|e| std::cmp::Reverse(e.span.byte_range.start));
57 for e in &edits_for_pass {
58 let range = e.span.byte_range.clone();
59 if range.end > source.len() {
60 continue;
61 }
62 source.replace_range(range, &e.replacement);
63 fixes_applied += 1;
64 }
65 }
66
67 if source != original {
68 let tmp = f.path.with_extension("aiproof-tmp");
70 std::fs::write(&tmp, &source)?;
71 std::fs::rename(&tmp, &f.path)?;
72 files_changed += 1;
73 }
74 }
75
76 Ok(FixOutcome {
77 fixes_applied,
78 files_changed,
79 })
80}