cargo_quality/differ/
apply.rs1use std::fs;
13
14use masterror::AppResult;
15
16use super::types::{DiffResult, FileDiff};
17use crate::{analyzer::Suggestion, error::IoError, fixer::apply_suggestions};
18
19pub fn apply_diff(result: &DiffResult) -> AppResult<usize> {
37 let mut applied = 0;
38
39 for file in &result.files {
40 applied += apply_file(file)?;
41 }
42
43 Ok(applied)
44}
45
46fn apply_file(file: &FileDiff) -> AppResult<usize> {
56 if file.entries.is_empty() {
57 return Ok(0);
58 }
59
60 let content = fs::read_to_string(&file.path).map_err(IoError::from)?;
61 let lines: Vec<&str> = content.lines().collect();
62
63 let mut suggestions = Vec::new();
64 for entry in &file.entries {
65 let idx = entry.line.saturating_sub(1);
66 if lines.get(idx).is_some_and(|line| *line == entry.original) {
67 suggestions.push(Suggestion {
68 edit: entry.edit.clone(),
69 import: entry.import.clone()
70 });
71 }
72 }
73
74 if suggestions.is_empty() {
75 return Ok(0);
76 }
77
78 let updated = apply_suggestions(&content, &suggestions);
79 fs::write(&file.path, updated).map_err(IoError::from)?;
80
81 Ok(suggestions.len())
82}
83
84#[cfg(test)]
85mod tests {
86 use std::path::Path;
87
88 use tempfile::TempDir;
89
90 use super::*;
91 use crate::analyzers::get_analyzers;
92
93 fn diff_for(path: &Path) -> DiffResult {
94 let file = super::super::generate_diff(path.to_str().unwrap(), &get_analyzers()).unwrap();
95 let mut result = DiffResult::new();
96 result.add_file(file);
97 result
98 }
99
100 #[test]
101 fn test_apply_rewrites_and_preserves_comments() {
102 let temp = TempDir::new().unwrap();
103 let path = temp.path().join("a.rs");
104 fs::write(
105 &path,
106 "//! Module doc\n\nfn main() {\n // note\n let x = std::fs::read_to_string(\"f\");\n}\n"
107 )
108 .unwrap();
109
110 let applied = apply_diff(&diff_for(&path)).unwrap();
111 assert_eq!(applied, 1);
112
113 let output = fs::read_to_string(&path).unwrap();
114 assert!(output.contains("use std::fs::read_to_string;"));
115 assert!(output.contains("let x = read_to_string(\"f\");"));
116 assert!(!output.contains("std::fs::read_to_string("));
117 assert!(output.contains("// note"), "comment preserved");
118 assert!(output.starts_with("//! Module doc"));
119 assert!(output.ends_with('\n'));
120 }
121
122 #[test]
123 fn test_apply_dedups_imports() {
124 let temp = TempDir::new().unwrap();
125 let path = temp.path().join("c.rs");
126 fs::write(
127 &path,
128 "fn main() {\n let a = std::fs::read_to_string(\"a\");\n let b = std::fs::read_to_string(\"b\");\n}\n"
129 )
130 .unwrap();
131
132 let applied = apply_diff(&diff_for(&path)).unwrap();
133 assert_eq!(applied, 2);
134
135 let output = fs::read_to_string(&path).unwrap();
136 assert_eq!(output.matches("use std::fs::read_to_string;").count(), 1);
137 }
138
139 #[test]
140 fn test_apply_is_collision_safe() {
141 let temp = TempDir::new().unwrap();
142 let path = temp.path().join("d.rs");
143 fs::write(
144 &path,
145 "fn main() {\n let a = std::fs::read(\"x\");\n let b = other::helpers::read(\"y\");\n}\n"
146 )
147 .unwrap();
148
149 let result = diff_for(&path);
150 assert_eq!(
151 result.total_changes(),
152 0,
153 "colliding reads produce no changes"
154 );
155
156 let applied = apply_diff(&result).unwrap();
157 assert_eq!(applied, 0);
158 assert!(
159 fs::read_to_string(&path)
160 .unwrap()
161 .contains("std::fs::read(\"x\")")
162 );
163 }
164
165 #[test]
166 fn test_apply_skips_when_file_changed() {
167 let temp = TempDir::new().unwrap();
168 let path = temp.path().join("e.rs");
169 fs::write(
170 &path,
171 "fn main() {\n let x = std::fs::read_to_string(\"f\");\n}\n"
172 )
173 .unwrap();
174
175 let result = diff_for(&path);
176 fs::write(&path, "fn main() {\n let y = 1;\n}\n").unwrap();
177
178 let applied = apply_diff(&result).unwrap();
179 assert_eq!(applied, 0, "stale entry is skipped");
180 assert!(fs::read_to_string(&path).unwrap().contains("let y = 1;"));
181 }
182
183 #[test]
184 fn test_apply_empty_result() {
185 let applied = apply_diff(&DiffResult::new()).unwrap();
186 assert_eq!(applied, 0);
187 }
188}