Skip to main content

aiproof_cli/
fix.rs

1/// Apply safe autofixes idempotently.
2use 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        // Iterate until no more fixes. Cap at 10 iterations.
33        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            // Apply edits in reverse-byte-position order to avoid shifting offsets.
56            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            // Atomic rewrite: write to a tempfile in the same dir, then rename.
69            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}