1use std::collections::BTreeMap;
2
3use wowlab_fs::{atomic, lock::Lock, path::Path};
4
5#[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#[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
51pub(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 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 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 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 eprintln!("tidy: failed to write {}: {error}", path.display());
119 }
120 }
121
122 total
123}
124
125pub(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 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 eprintln!("tidy: failed to write {}", path.display());
148 }
149 }
150
151 total
152}