cargo_quality/differ/
apply.rs1use std::fs;
13
14use masterror::AppResult;
15
16use super::types::{DiffResult, FileDiff};
17use crate::{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
67 .get(idx)
68 .is_some_and(|line| *line == entry.preview.original)
69 {
70 suggestions.push(entry.suggestion.clone());
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::default_analyzers;
92
93 fn diff_for(path: &Path) -> DiffResult {
94 let file =
95 super::super::generate_diff(path.to_str().unwrap(), &default_analyzers()).unwrap();
96 let mut result = DiffResult::new();
97 result.add_file(file);
98 result
99 }
100
101 #[test]
102 fn test_apply_rewrites_and_preserves_comments() {
103 let temp = TempDir::new().unwrap();
104 let path = temp.path().join("a.rs");
105 fs::write(
106 &path,
107 "//! Module doc\n\nfn main() {\n // note\n let x = std::fs::read_to_string(\"f\");\n}\n"
108 )
109 .unwrap();
110
111 let applied = apply_diff(&diff_for(&path)).unwrap();
112 assert_eq!(applied, 3, "path rewrite, comment removal, notes insertion");
113
114 let output = fs::read_to_string(&path).unwrap();
115 assert!(output.contains("use std::fs::read_to_string;"));
116 assert!(output.contains("let x = read_to_string(\"f\");"));
117 assert!(!output.contains("std::fs::read_to_string("));
118 assert!(output.contains("/// - note"), "comment moved to doc block");
119 assert!(!output.contains(" // note"), "inline comment removed");
120 assert!(output.starts_with("//! Module doc"));
121 assert!(output.ends_with('\n'));
122 }
123
124 #[test]
125 fn test_apply_dedups_imports() {
126 let temp = TempDir::new().unwrap();
127 let path = temp.path().join("c.rs");
128 fs::write(
129 &path,
130 "fn main() {\n let a = std::fs::read_to_string(\"a\");\n let b = std::fs::read_to_string(\"b\");\n}\n"
131 )
132 .unwrap();
133
134 let applied = apply_diff(&diff_for(&path)).unwrap();
135 assert_eq!(applied, 2);
136
137 let output = fs::read_to_string(&path).unwrap();
138 assert_eq!(output.matches("use std::fs::read_to_string;").count(), 1);
139 }
140
141 #[test]
142 fn test_apply_is_collision_safe() {
143 let temp = TempDir::new().unwrap();
144 let path = temp.path().join("d.rs");
145 fs::write(
146 &path,
147 "fn main() {\n let a = std::fs::read(\"x\");\n let b = crate::helpers::read(\"y\");\n}\n"
148 )
149 .unwrap();
150
151 let result = diff_for(&path);
152 assert_eq!(
153 result.total_changes(),
154 0,
155 "colliding reads produce no changes"
156 );
157
158 let applied = apply_diff(&result).unwrap();
159 assert_eq!(applied, 0);
160 assert!(
161 fs::read_to_string(&path)
162 .unwrap()
163 .contains("std::fs::read(\"x\")")
164 );
165 }
166
167 #[test]
168 fn test_apply_skips_when_file_changed() {
169 let temp = TempDir::new().unwrap();
170 let path = temp.path().join("e.rs");
171 fs::write(
172 &path,
173 "fn main() {\n let x = std::fs::read_to_string(\"f\");\n}\n"
174 )
175 .unwrap();
176
177 let result = diff_for(&path);
178 fs::write(&path, "fn main() {\n let y = 1;\n}\n").unwrap();
179
180 let applied = apply_diff(&result).unwrap();
181 assert_eq!(applied, 0, "stale entry is skipped");
182 assert!(fs::read_to_string(&path).unwrap().contains("let y = 1;"));
183 }
184
185 #[test]
186 fn test_apply_empty_result() {
187 let applied = apply_diff(&DiffResult::new()).unwrap();
188 assert_eq!(applied, 0);
189 }
190}