Skip to main content

wowlab_tidy/infra/
fix.rs

1use std::collections::BTreeMap;
2
3use wowlab_fs::{atomic, lock::Lock, path::Path};
4
5/// An auto-fix: replace a line range with new text.
6#[derive(Clone, Debug)]
7pub(crate) struct Fix {
8    pub(crate) start_line: usize,
9    pub(crate) end_line: usize,
10    pub(crate) replacement: String,
11}
12
13/// One whole-file replacement produced by a rust-analyzer syntax-tree edit.
14#[derive(Clone, Debug)]
15pub(crate) struct TreeFix {
16    pub(crate) rel: String,
17    pub(crate) rule: &'static str,
18    pub(crate) replacement: String,
19}
20
21impl Fix {
22    pub(crate) fn replace_line(line: usize, new: impl Into<String>) -> Self {
23        Fix {
24            start_line: line,
25            end_line: line,
26            replacement: new.into(),
27        }
28    }
29
30    pub(crate) fn replace_lines(
31        start_line: usize,
32        end_line: usize,
33        replacement: impl Into<String>,
34    ) -> Self {
35        Fix {
36            start_line,
37            end_line,
38            replacement: replacement.into(),
39        }
40    }
41
42    pub(crate) fn delete(start_line: usize, end_line: usize) -> Self {
43        Fix {
44            start_line,
45            end_line,
46            replacement: String::new(),
47        }
48    }
49}
50
51/// Apply `(rel_path, fix)` auto-fixes under a `.tidy.lock`, returning the count applied.
52pub(crate) fn apply_fixes(fixes: &[(String, Fix)], root: &Path) -> usize {
53    let mut by_file: BTreeMap<&str, Vec<&Fix>> = BTreeMap::new();
54
55    for (rel, fix) in fixes {
56        by_file.entry(rel).or_default().push(fix);
57    }
58
59    let lock_path = root.join(".tidy.lock");
60    let _lock = match Lock::try_acquire(&lock_path) {
61        Ok(guard) => guard,
62        Err(error) => {
63            // #t(rust_println) binary-only fix writer, not library output
64            eprintln!("tidy: {error}");
65
66            return 0;
67        }
68    };
69
70    let mut total = 0;
71
72    for (rel, mut file_fixes) in by_file {
73        let path = root.join(rel);
74        let Ok(contents) = wowlab_fs::file::read_text(&path) else {
75            // #t(rust_println) binary-only fix writer, not library output
76            eprintln!("tidy: failed to read {}", path.display());
77            continue;
78        };
79        let mut lines: Vec<String> = contents.lines().map(String::from).collect();
80
81        file_fixes.sort_by_key(|a| std::cmp::Reverse(a.start_line));
82
83        // Bottom-to-top single pass: fixes at higher lines can't shift lower ones.
84        let mut lowest_touched = usize::MAX;
85
86        for fix in file_fixes {
87            let start = fix.start_line.saturating_sub(1);
88            let end = fix.end_line.min(lines.len());
89
90            if start >= lines.len() || start >= end {
91                continue;
92            }
93
94            if end > lowest_touched {
95                continue;
96            }
97
98            if fix.replacement.is_empty() {
99                lines.drain(start..end);
100            } else {
101                let new_lines: Vec<String> = fix.replacement.lines().map(String::from).collect();
102
103                lines.splice(start..end, new_lines);
104            }
105
106            lowest_touched = start;
107            total += 1;
108        }
109
110        let mut output = lines.join("\n");
111
112        if contents.ends_with('\n') {
113            output.push('\n');
114        }
115
116        if let Err(error) = atomic::replace(&path, output.as_bytes()) {
117            // #t(rust_println) binary-only fix writer, not library output
118            eprintln!("tidy: failed to write {}: {error}", path.display());
119        }
120    }
121
122    total
123}
124
125/// Apply independent whole-file tree edits for one rule under the tidy lock.
126pub(crate) fn apply_tree_fixes(fixes: &[TreeFix], root: &Path) -> usize {
127    let lock_path = root.join(".tidy.lock");
128    let _lock = match Lock::try_acquire(&lock_path) {
129        Ok(guard) => guard,
130        Err(error) => {
131            // #t(rust_println) binary-only fix writer, not library output
132            eprintln!("tidy: {error}");
133
134            return 0;
135        }
136    };
137
138    let mut total = 0;
139
140    for fix in fixes {
141        let path = root.join(&fix.rel);
142
143        if atomic::replace(&path, fix.replacement.as_bytes()).is_ok() {
144            total += 1;
145        } else {
146            // #t(rust_println) binary-only fix writer, not library output
147            eprintln!("tidy: failed to write {}", path.display());
148        }
149    }
150
151    total
152}