Skip to main content

cargo_quality/differ/
types.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use crate::analyzer::Suggestion;
5
6/// Before/after text of a proposed change for display.
7///
8/// Groups the human-facing strings of a diff entry: the affected line as it
9/// is, the line after the fix, and a short description of the change.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ChangePreview {
12    pub original:    String,
13    pub modified:    String,
14    pub description: String
15}
16
17/// Represents a single code change.
18///
19/// Stores the location and preview text of a proposed modification for
20/// display, and the underlying [`Suggestion`] so the same change can be
21/// applied through the shared fix engine.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct DiffEntry {
24    pub line:       usize,
25    pub analyzer:   String,
26    pub preview:    ChangePreview,
27    pub suggestion: Suggestion
28}
29
30/// Diff results for a single file.
31///
32/// Contains all proposed changes grouped by analyzer.
33#[derive(Debug, Clone)]
34pub struct FileDiff {
35    pub path:    String,
36    pub entries: Vec<DiffEntry>
37}
38
39impl FileDiff {
40    /// Creates a new file diff result.
41    ///
42    /// # Arguments
43    ///
44    /// * `path` - File path
45    ///
46    /// # Returns
47    ///
48    /// Empty `FileDiff` structure
49    #[inline]
50    pub fn new(path: String) -> Self {
51        Self {
52            path,
53            entries: Vec::new()
54        }
55    }
56
57    /// Adds a diff entry to the file.
58    ///
59    /// # Arguments
60    ///
61    /// * `entry` - Diff entry to add
62    #[inline]
63    pub fn add_entry(&mut self, entry: DiffEntry) {
64        self.entries.push(entry);
65    }
66
67    /// Returns total number of changes.
68    ///
69    /// # Returns
70    ///
71    /// Number of diff entries
72    #[inline]
73    pub fn total_changes(&self) -> usize {
74        self.entries.len()
75    }
76}
77
78/// Complete diff results for all files.
79///
80/// Aggregates changes across multiple files.
81#[derive(Debug, Clone)]
82pub struct DiffResult {
83    pub files: Vec<FileDiff>
84}
85
86impl DiffResult {
87    /// Creates a new empty diff result.
88    ///
89    /// # Returns
90    ///
91    /// Empty `DiffResult` structure
92    #[inline]
93    pub fn new() -> Self {
94        Self {
95            files: Vec::new()
96        }
97    }
98
99    /// Adds file diff to results.
100    ///
101    /// # Arguments
102    ///
103    /// * `file_diff` - File diff to add
104    #[inline]
105    pub fn add_file(&mut self, file_diff: FileDiff) {
106        if file_diff.total_changes() > 0 {
107            self.files.push(file_diff);
108        }
109    }
110
111    /// Returns total number of changes across all files.
112    ///
113    /// # Returns
114    ///
115    /// Total change count
116    #[inline]
117    pub fn total_changes(&self) -> usize {
118        self.files.iter().map(|f| f.total_changes()).sum()
119    }
120
121    /// Returns number of files with changes.
122    ///
123    /// # Returns
124    ///
125    /// File count
126    #[inline]
127    pub fn total_files(&self) -> usize {
128        self.files.len()
129    }
130}
131
132impl Default for DiffResult {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::analyzer::TextEdit;
142
143    #[test]
144    fn test_diff_entry_creation() {
145        let entry = DiffEntry {
146            line:       10,
147            analyzer:   "test".to_string(),
148            preview:    ChangePreview {
149                original:    "old".to_string(),
150                modified:    "new".to_string(),
151                description: "desc".to_string()
152            },
153            suggestion: Suggestion {
154                edit:   TextEdit::default(),
155                import: None
156            }
157        };
158
159        assert_eq!(entry.line, 10);
160        assert_eq!(entry.analyzer, "test");
161    }
162
163    #[test]
164    fn test_file_diff_new() {
165        let diff = FileDiff::new("test.rs".to_string());
166        assert_eq!(diff.path, "test.rs");
167        assert_eq!(diff.total_changes(), 0);
168    }
169
170    #[test]
171    fn test_file_diff_add_entry() {
172        let mut diff = FileDiff::new("test.rs".to_string());
173        let entry = DiffEntry {
174            line:       1,
175            analyzer:   "test".to_string(),
176            preview:    ChangePreview {
177                original:    "old".to_string(),
178                modified:    "new".to_string(),
179                description: "desc".to_string()
180            },
181            suggestion: Suggestion {
182                edit:   TextEdit::default(),
183                import: None
184            }
185        };
186
187        diff.add_entry(entry);
188        assert_eq!(diff.total_changes(), 1);
189    }
190
191    #[test]
192    fn test_diff_result_new() {
193        let result = DiffResult::new();
194        assert_eq!(result.total_changes(), 0);
195        assert_eq!(result.total_files(), 0);
196    }
197
198    #[test]
199    fn test_diff_result_add_file() {
200        let mut result = DiffResult::new();
201        let mut file_diff = FileDiff::new("test.rs".to_string());
202
203        let entry = DiffEntry {
204            line:       1,
205            analyzer:   "test".to_string(),
206            preview:    ChangePreview {
207                original:    "old".to_string(),
208                modified:    "new".to_string(),
209                description: "desc".to_string()
210            },
211            suggestion: Suggestion {
212                edit:   TextEdit::default(),
213                import: None
214            }
215        };
216
217        file_diff.add_entry(entry);
218        result.add_file(file_diff);
219
220        assert_eq!(result.total_files(), 1);
221        assert_eq!(result.total_changes(), 1);
222    }
223
224    #[test]
225    fn test_diff_result_skip_empty_files() {
226        let mut result = DiffResult::new();
227        let file_diff = FileDiff::new("test.rs".to_string());
228        result.add_file(file_diff);
229
230        assert_eq!(result.total_files(), 0);
231    }
232
233    #[test]
234    fn test_diff_result_multiple_files() {
235        let mut result = DiffResult::new();
236
237        let mut file1 = FileDiff::new("file1.rs".to_string());
238        file1.add_entry(DiffEntry {
239            line:       1,
240            analyzer:   "test".to_string(),
241            preview:    ChangePreview {
242                original:    "old".to_string(),
243                modified:    "new".to_string(),
244                description: "desc".to_string()
245            },
246            suggestion: Suggestion {
247                edit:   TextEdit::default(),
248                import: None
249            }
250        });
251
252        let mut file2 = FileDiff::new("file2.rs".to_string());
253        file2.add_entry(DiffEntry {
254            line:       1,
255            analyzer:   "test".to_string(),
256            preview:    ChangePreview {
257                original:    "old".to_string(),
258                modified:    "new".to_string(),
259                description: "desc".to_string()
260            },
261            suggestion: Suggestion {
262                edit:   TextEdit::default(),
263                import: None
264            }
265        });
266
267        result.add_file(file1);
268        result.add_file(file2);
269
270        assert_eq!(result.total_files(), 2);
271        assert_eq!(result.total_changes(), 2);
272    }
273}