Skip to main content

cargo_quality/differ/
generator.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use std::fs;
5
6use masterror::AppResult;
7
8use super::types::{DiffEntry, FileDiff};
9use crate::{
10    analyzer::Analyzer,
11    error::{IoError, ParseError}
12};
13
14/// Generates diff showing proposed changes.
15///
16/// Analyzes files and compares current state with proposed fixes.
17///
18/// # Arguments
19///
20/// * `file_path` - Path to analyze
21/// * `analyzers` - List of analyzers to apply
22///
23/// # Returns
24///
25/// `AppResult<FileDiff>` - Diff results or error
26///
27/// # Examples
28///
29/// ```no_run
30/// use cargo_quality::{analyzers::get_analyzers, differ::generate_diff};
31/// let diff = generate_diff("src/main.rs", &get_analyzers()).unwrap();
32/// ```
33pub 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        let result = analyzer.analyze(&ast, &content)?;
41
42        for issue in result.issues {
43            if issue.line == 0 || !issue.fix.is_available() {
44                continue;
45            }
46
47            let original_content = content
48                .lines()
49                .nth(issue.line.saturating_sub(1))
50                .unwrap_or("");
51
52            let (modified_line, import) =
53                if let Some((import, pattern, replacement)) = issue.fix.as_import() {
54                    let modified = original_content.replace(pattern, replacement);
55                    (modified, Some(import.to_string()))
56                } else if let Some(simple) = issue.fix.as_simple() {
57                    (simple.to_string(), None)
58                } else {
59                    continue;
60                };
61
62            let entry = DiffEntry {
63                line: issue.line,
64                analyzer: analyzer.name().to_string(),
65                original: original_content.to_string(),
66                modified: modified_line,
67                description: issue.message,
68                import
69            };
70
71            file_diff.add_entry(entry);
72        }
73    }
74
75    Ok(file_diff)
76}
77
78#[cfg(test)]
79mod tests {
80    use tempfile::TempDir;
81
82    use super::*;
83    use crate::analyzers::get_analyzers;
84
85    #[test]
86    fn test_generate_diff_integration() {
87        let temp_dir = TempDir::new().unwrap();
88        let file_path = temp_dir.path().join("test.rs");
89        std::fs::write(
90            &file_path,
91            "fn main() { let x = std::fs::read_to_string(\"f\"); }"
92        )
93        .unwrap();
94
95        let analyzers = get_analyzers();
96        let result = generate_diff(file_path.to_str().unwrap(), &analyzers);
97
98        assert!(result.is_ok());
99    }
100
101    #[test]
102    fn test_generate_diff_no_issues() {
103        let temp_dir = TempDir::new().unwrap();
104        let file_path = temp_dir.path().join("test.rs");
105        std::fs::write(&file_path, "fn main() {}").unwrap();
106
107        let analyzers = get_analyzers();
108        let result = generate_diff(file_path.to_str().unwrap(), &analyzers);
109
110        assert!(result.is_ok());
111    }
112
113    #[test]
114    fn test_generate_diff_invalid_syntax() {
115        let temp_dir = TempDir::new().unwrap();
116        let file_path = temp_dir.path().join("test.rs");
117        std::fs::write(&file_path, "fn main() { invalid syntax +++").unwrap();
118
119        let analyzers = get_analyzers();
120        let result = generate_diff(file_path.to_str().unwrap(), &analyzers);
121
122        assert!(result.is_err());
123    }
124
125    #[test]
126    fn test_path_import_included_in_diff() {
127        let temp_dir = TempDir::new().unwrap();
128        let file_path = temp_dir.path().join("test.rs");
129        std::fs::write(
130            &file_path,
131            "fn main() { let x = std::fs::read_to_string(\"f\"); }"
132        )
133        .unwrap();
134
135        let analyzers = get_analyzers();
136        let result = generate_diff(file_path.to_str().unwrap(), &analyzers).unwrap();
137
138        assert!(
139            result.entries.iter().any(|e| e.analyzer == "path_import"),
140            "path_import should be included in diff with suggestions"
141        );
142    }
143
144    #[test]
145    fn test_format_args_excluded_from_diff_without_suggestion() {
146        let temp_dir = TempDir::new().unwrap();
147        let file_path = temp_dir.path().join("test.rs");
148        std::fs::write(
149            &file_path,
150            "fn main() { println!(\"Hello {}\", \"world\"); }"
151        )
152        .unwrap();
153
154        let analyzers = get_analyzers();
155        let result = generate_diff(file_path.to_str().unwrap(), &analyzers).unwrap();
156
157        for entry in &result.entries {
158            assert_ne!(entry.analyzer, "format_args");
159        }
160    }
161}