cargo_quality/differ/
generator.rs1use std::fs;
5
6use masterror::AppResult;
7
8use super::types::{ChangePreview, DiffEntry, FileDiff};
9use crate::{
10 analyzer::{Analyzer, Suggestion},
11 error::{IoError, ParseError}
12};
13
14pub fn generate_diff(file_path: &str, analyzers: &[Box<dyn Analyzer>]) -> AppResult<FileDiff> {
34 let content = fs::read_to_string(file_path).map_err(IoError::from)?;
35 let ast = syn::parse_file(&content).map_err(ParseError::from)?;
36
37 let mut file_diff = FileDiff::new(file_path.to_string());
38
39 for analyzer in analyzers {
40 for suggestion in analyzer.suggestions(&ast, &content)? {
41 file_diff.add_entry(entry_from_suggestion(analyzer.name(), &content, suggestion));
42 }
43 }
44
45 Ok(file_diff)
46}
47
48fn entry_from_suggestion(analyzer: &str, content: &str, suggestion: Suggestion) -> DiffEntry {
63 let start = suggestion.edit.range.start;
64 let end = suggestion.edit.range.end;
65
66 let line = content[..start]
67 .bytes()
68 .filter(|&byte| byte == b'\n')
69 .count()
70 + 1;
71 let line_start = content[..start].rfind('\n').map_or(0, |index| index + 1);
72 let line_end = content[start..]
73 .find('\n')
74 .map_or(content.len(), |index| start + index);
75
76 let original = content[line_start..line_end].to_string();
77 let rel_start = start - line_start;
78 let rel_end = (end - line_start).min(original.len());
79 let modified = format!(
80 "{}{}{}",
81 &original[..rel_start],
82 suggestion.edit.replacement,
83 &original[rel_end..]
84 );
85
86 DiffEntry {
87 line,
88 analyzer: analyzer.to_string(),
89 preview: ChangePreview {
90 original,
91 modified,
92 description: format!("{} fix", analyzer)
93 },
94 suggestion
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use tempfile::TempDir;
101
102 use super::*;
103 use crate::analyzers::default_analyzers;
104
105 #[test]
106 fn test_generate_diff_integration() {
107 let temp_dir = TempDir::new().unwrap();
108 let file_path = temp_dir.path().join("test.rs");
109 std::fs::write(
110 &file_path,
111 "fn main() { let x = std::fs::read_to_string(\"f\"); }"
112 )
113 .unwrap();
114
115 let analyzers = default_analyzers();
116 let result = generate_diff(file_path.to_str().unwrap(), &analyzers);
117
118 assert!(result.is_ok());
119 }
120
121 #[test]
122 fn test_generate_diff_no_issues() {
123 let temp_dir = TempDir::new().unwrap();
124 let file_path = temp_dir.path().join("test.rs");
125 std::fs::write(&file_path, "fn main() {}").unwrap();
126
127 let analyzers = default_analyzers();
128 let result = generate_diff(file_path.to_str().unwrap(), &analyzers);
129
130 assert!(result.is_ok());
131 }
132
133 #[test]
134 fn test_generate_diff_invalid_syntax() {
135 let temp_dir = TempDir::new().unwrap();
136 let file_path = temp_dir.path().join("test.rs");
137 std::fs::write(&file_path, "fn main() { invalid syntax +++").unwrap();
138
139 let analyzers = default_analyzers();
140 let result = generate_diff(file_path.to_str().unwrap(), &analyzers);
141
142 assert!(result.is_err());
143 }
144
145 #[test]
146 fn test_path_import_included_in_diff() {
147 let temp_dir = TempDir::new().unwrap();
148 let file_path = temp_dir.path().join("test.rs");
149 std::fs::write(
150 &file_path,
151 "fn main() { let x = std::fs::read_to_string(\"f\"); }"
152 )
153 .unwrap();
154
155 let analyzers = default_analyzers();
156 let result = generate_diff(file_path.to_str().unwrap(), &analyzers).unwrap();
157
158 assert!(
159 result.entries.iter().any(|e| e.analyzer == "path_import"),
160 "path_import should be included in diff with suggestions"
161 );
162 }
163
164 #[test]
165 fn test_format_args_excluded_from_diff_without_suggestion() {
166 let temp_dir = TempDir::new().unwrap();
167 let file_path = temp_dir.path().join("test.rs");
168 std::fs::write(
169 &file_path,
170 "fn main() { println!(\"Hello {}\", \"world\"); }"
171 )
172 .unwrap();
173
174 let analyzers = default_analyzers();
175 let result = generate_diff(file_path.to_str().unwrap(), &analyzers).unwrap();
176
177 for entry in &result.entries {
178 assert_ne!(entry.analyzer, "format_args");
179 }
180 }
181}