cargo_quality/differ/
apply.rs1use std::fs;
12
13use masterror::AppResult;
14
15use super::types::{DiffResult, FileDiff};
16use crate::error::IoError;
17
18pub fn apply_diff(result: &DiffResult) -> AppResult<usize> {
35 let mut applied = 0;
36
37 for file in &result.files {
38 applied += apply_file(file)?;
39 }
40
41 Ok(applied)
42}
43
44fn apply_file(file: &FileDiff) -> AppResult<usize> {
54 if file.entries.is_empty() {
55 return Ok(0);
56 }
57
58 let content = fs::read_to_string(&file.path).map_err(IoError::from)?;
59 let mut lines: Vec<String> = content.lines().map(str::to_string).collect();
60 let mut imports: Vec<String> = Vec::new();
61 let mut applied = 0;
62
63 for entry in &file.entries {
64 let idx = entry.line.saturating_sub(1);
65
66 let Some(line) = lines.get_mut(idx) else {
67 continue;
68 };
69
70 if *line != entry.original {
71 continue;
72 }
73
74 *line = entry.modified.clone();
75
76 if let Some(import) = &entry.import
77 && !imports.contains(import)
78 {
79 imports.push(import.clone());
80 }
81
82 applied += 1;
83 }
84
85 if applied == 0 {
86 return Ok(0);
87 }
88
89 insert_imports(&mut lines, imports);
90
91 let mut text = lines.join("\n");
92 if content.ends_with('\n') {
93 text.push('\n');
94 }
95
96 fs::write(&file.path, text).map_err(IoError::from)?;
97
98 Ok(applied)
99}
100
101fn insert_imports(lines: &mut Vec<String>, imports: Vec<String>) {
111 if imports.is_empty() {
112 return;
113 }
114
115 let mut insert_at = 0;
116 for (index, line) in lines.iter().enumerate() {
117 let trimmed = line.trim_start();
118 if trimmed.is_empty()
119 || trimmed.starts_with("//!")
120 || trimmed.starts_with("#!")
121 || (trimmed.starts_with("//") && !trimmed.starts_with("///"))
122 {
123 insert_at = index + 1;
124 } else {
125 break;
126 }
127 }
128
129 for (offset, import) in imports.into_iter().enumerate() {
130 lines.insert(insert_at + offset, import);
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use tempfile::TempDir;
137
138 use super::*;
139 use crate::differ::types::DiffEntry;
140
141 fn entry(line: usize, original: &str, modified: &str, import: Option<&str>) -> DiffEntry {
142 DiffEntry {
143 line,
144 analyzer: "path_import".to_string(),
145 original: original.to_string(),
146 modified: modified.to_string(),
147 description: "desc".to_string(),
148 import: import.map(str::to_string)
149 }
150 }
151
152 #[test]
153 fn test_apply_rewrites_line_and_inserts_import() {
154 let temp = TempDir::new().unwrap();
155 let path = temp.path().join("a.rs");
156 fs::write(
157 &path,
158 "//! Module doc\n\nfn main() {\n let x = std::fs::read_to_string(\"f\");\n}\n"
159 )
160 .unwrap();
161
162 let mut result = DiffResult::new();
163 let mut file = FileDiff::new(path.to_str().unwrap().to_string());
164 file.add_entry(entry(
165 4,
166 " let x = std::fs::read_to_string(\"f\");",
167 " let x = read_to_string(\"f\");",
168 Some("use std::fs::read_to_string;")
169 ));
170 result.add_file(file);
171
172 let applied = apply_diff(&result).unwrap();
173 assert_eq!(applied, 1);
174
175 let output = fs::read_to_string(&path).unwrap();
176 assert!(output.contains("use std::fs::read_to_string;"));
177 assert!(output.contains("let x = read_to_string(\"f\");"));
178 assert!(!output.contains("std::fs::read_to_string("));
179 assert!(output.starts_with("//! Module doc"));
180 assert!(output.ends_with('\n'));
181 }
182
183 #[test]
184 fn test_apply_skips_mismatched_original() {
185 let temp = TempDir::new().unwrap();
186 let path = temp.path().join("b.rs");
187 fs::write(&path, "fn main() {\n let x = 1;\n}\n").unwrap();
188
189 let mut result = DiffResult::new();
190 let mut file = FileDiff::new(path.to_str().unwrap().to_string());
191 file.add_entry(entry(2, " let x = 2;", " let x = changed;", None));
192 result.add_file(file);
193
194 let applied = apply_diff(&result).unwrap();
195 assert_eq!(applied, 0);
196
197 let output = fs::read_to_string(&path).unwrap();
198 assert!(output.contains("let x = 1;"));
199 }
200
201 #[test]
202 fn test_apply_dedups_imports() {
203 let temp = TempDir::new().unwrap();
204 let path = temp.path().join("c.rs");
205 fs::write(
206 &path,
207 "fn main() {\n let a = std::fs::read_to_string(\"a\");\n let b = std::fs::read_to_string(\"b\");\n}\n"
208 )
209 .unwrap();
210
211 let mut result = DiffResult::new();
212 let mut file = FileDiff::new(path.to_str().unwrap().to_string());
213 file.add_entry(entry(
214 2,
215 " let a = std::fs::read_to_string(\"a\");",
216 " let a = read_to_string(\"a\");",
217 Some("use std::fs::read_to_string;")
218 ));
219 file.add_entry(entry(
220 3,
221 " let b = std::fs::read_to_string(\"b\");",
222 " let b = read_to_string(\"b\");",
223 Some("use std::fs::read_to_string;")
224 ));
225 result.add_file(file);
226
227 let applied = apply_diff(&result).unwrap();
228 assert_eq!(applied, 2);
229
230 let output = fs::read_to_string(&path).unwrap();
231 assert_eq!(output.matches("use std::fs::read_to_string;").count(), 1);
232 }
233
234 #[test]
235 fn test_apply_empty_result() {
236 let result = DiffResult::new();
237 let applied = apply_diff(&result).unwrap();
238 assert_eq!(applied, 0);
239 }
240}