Skip to main content

mago_codex/
diff.rs

1use foldhash::HashMap;
2use foldhash::HashSet;
3
4use mago_database::file::FileId;
5
6use crate::differ::compute_file_diff;
7use crate::metadata::CodebaseMetadata;
8use crate::symbol::SymbolIdentifier;
9
10/// Represents a text diff hunk with position and offset information.
11///
12/// Format: `(old_start, old_length, line_offset, column_offset)`
13/// - `old_start`: Starting byte offset in the old version
14/// - `old_length`: Length of the changed region in bytes
15/// - `line_offset`: Line number change (`new_line` - `old_line`)
16/// - `column_offset`: Column number change (`new_column` - `old_column`)
17pub type DiffHunk = (usize, usize, isize, isize);
18
19/// Represents a range of deleted code.
20///
21/// Format: `(start_offset, end_offset)`
22/// - `start_offset`: Starting byte offset of deletion
23/// - `end_offset`: Ending byte offset of deletion
24pub type DeletionRange = (usize, usize);
25
26/// Represents the differences between two states of a codebase, typically used for incremental analysis.
27///
28/// This structure uses a single fingerprint hash per symbol to determine changes. Any change to a symbol
29/// (signature, body, modifiers, attributes) produces a different hash, triggering re-analysis.
30///
31/// Provides a comprehensive API for modification and querying following established conventions.
32#[derive(Default, Debug, Clone, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct CodebaseDiff {
35    /// Set of `(Symbol, Member)` pairs whose fingerprint hash is UNCHANGED.
36    /// These symbols can be safely skipped during re-analysis.
37    /// Member is empty for top-level symbols.
38    keep: HashSet<SymbolIdentifier>,
39
40    /// Set of `(Symbol, Member)` pairs that are new, deleted, or have a different fingerprint hash.
41    /// These symbols MUST be re-analyzed.
42    /// Member is empty for top-level symbols.
43    changed: HashSet<SymbolIdentifier>,
44
45    /// Map from source file identifier to a vector of text diff hunks.
46    /// Used for mapping issue positions between old and new code.
47    diff_map: HashMap<FileId, Vec<DiffHunk>>,
48
49    /// Map from source file identifier to a vector of deleted code ranges.
50    /// Used for filtering out issues in deleted code regions.
51    deletion_ranges_map: HashMap<FileId, Vec<DeletionRange>>,
52}
53
54impl CodebaseDiff {
55    #[inline]
56    #[must_use]
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Computes the `CodebaseDiff` between two `CodebaseMetadata` instances.
62    ///
63    /// This method compares the metadata of the old and new codebases to determine which symbols have changed,
64    /// which can be kept unchanged, and what text diffs exist for source files.
65    ///
66    /// It aggregates this information into a `CodebaseDiff` instance that can be used for incremental analysis.
67    #[must_use]
68    pub fn between(old_metadata: &CodebaseMetadata, new_metadata: &CodebaseMetadata) -> Self {
69        let mut aggregate_diff = CodebaseDiff::new();
70
71        let mut all_file_ids = old_metadata.get_all_file_ids();
72        all_file_ids.extend(new_metadata.get_all_file_ids());
73        all_file_ids.sort();
74        all_file_ids.dedup();
75
76        for file_id in all_file_ids {
77            let old_sig = old_metadata.get_file_signature(&file_id);
78            let new_sig = new_metadata.get_file_signature(&file_id);
79
80            let file_diff = compute_file_diff(file_id, old_sig, new_sig);
81
82            aggregate_diff.extend(file_diff);
83        }
84
85        aggregate_diff
86    }
87
88    /// Merges changes from another `CodebaseDiff` into this one.
89    #[inline]
90    pub fn extend(&mut self, other: Self) {
91        self.keep.extend(other.keep);
92        self.changed.extend(other.changed);
93        for (source, diffs) in other.diff_map {
94            self.diff_map.entry(source).or_default().extend(diffs);
95        }
96        for (source, ranges) in other.deletion_ranges_map {
97            self.deletion_ranges_map.entry(source).or_default().extend(ranges);
98        }
99    }
100
101    /// Returns a reference to the set of symbols/members to keep unchanged.
102    #[inline]
103    #[must_use]
104    pub fn get_keep(&self) -> &HashSet<SymbolIdentifier> {
105        &self.keep
106    }
107
108    /// Returns a reference to the set of changed symbols/members.
109    #[inline]
110    #[must_use]
111    pub fn get_changed(&self) -> &HashSet<SymbolIdentifier> {
112        &self.changed
113    }
114
115    /// Returns a reference to the map of source files to text diff hunks.
116    #[inline]
117    #[must_use]
118    pub fn get_diff_map(&self) -> &HashMap<FileId, Vec<DiffHunk>> {
119        &self.diff_map
120    }
121
122    /// Returns a reference to the map of source files to deletion ranges.
123    #[inline]
124    #[must_use]
125    pub fn get_deletion_ranges_map(&self) -> &HashMap<FileId, Vec<DeletionRange>> {
126        &self.deletion_ranges_map
127    }
128
129    /// Sets the 'keep' set, replacing the existing one.
130    #[inline]
131    pub fn set_keep(&mut self, keep_set: impl IntoIterator<Item = SymbolIdentifier>) {
132        self.keep = keep_set.into_iter().collect();
133    }
134
135    /// Returns a new instance with the 'keep' set replaced.
136    #[inline]
137    #[must_use]
138    pub fn with_keep(mut self, keep_set: impl IntoIterator<Item = SymbolIdentifier>) -> Self {
139        self.set_keep(keep_set);
140        self
141    }
142
143    /// Adds a single entry to the 'keep' set. Returns `true` if the entry was not already present.
144    #[inline]
145    pub fn add_keep_entry(&mut self, entry: SymbolIdentifier) -> bool {
146        self.keep.insert(entry)
147    }
148
149    /// Returns a new instance with the entry added to the 'keep' set.
150    #[inline]
151    #[must_use]
152    pub fn with_added_keep_entry(mut self, entry: SymbolIdentifier) -> Self {
153        self.add_keep_entry(entry);
154        self
155    }
156
157    /// Adds multiple entries to the 'keep' set.
158    #[inline]
159    pub fn add_keep_entries(&mut self, entries: impl IntoIterator<Item = SymbolIdentifier>) {
160        self.keep.extend(entries);
161    }
162
163    /// Returns a new instance with multiple entries added to the 'keep' set.
164    #[inline]
165    #[must_use]
166    pub fn with_added_keep_entries(mut self, entries: impl IntoIterator<Item = SymbolIdentifier>) -> Self {
167        self.add_keep_entries(entries);
168        self
169    }
170
171    /// Clears the 'keep' set.
172    #[inline]
173    pub fn unset_keep(&mut self) {
174        self.keep.clear();
175    }
176
177    /// Returns a new instance with an empty 'keep' set.
178    #[inline]
179    #[must_use]
180    pub fn without_keep(mut self) -> Self {
181        self.unset_keep();
182        self
183    }
184
185    /// Sets the 'changed' set, replacing the existing one.
186    #[inline]
187    pub fn set_changed(&mut self, change_set: impl IntoIterator<Item = SymbolIdentifier>) {
188        self.changed = change_set.into_iter().collect();
189    }
190
191    /// Returns a new instance with the 'changed' set replaced.
192    #[inline]
193    #[must_use]
194    pub fn with_changed(mut self, change_set: impl IntoIterator<Item = SymbolIdentifier>) -> Self {
195        self.set_changed(change_set);
196        self
197    }
198
199    /// Adds a single entry to the 'changed' set. Returns `true` if the entry was not already present.
200    #[inline]
201    pub fn add_changed_entry(&mut self, entry: SymbolIdentifier) -> bool {
202        self.changed.insert(entry)
203    }
204
205    /// Checks if the 'changed' set contains a specific entry.
206    #[inline]
207    #[must_use]
208    pub fn contains_changed_entry(&self, entry: &SymbolIdentifier) -> bool {
209        self.changed.contains(entry)
210    }
211
212    /// Returns a new instance with the entry added to the 'changed' set.
213    #[inline]
214    #[must_use]
215    pub fn with_added_changed_entry(mut self, entry: SymbolIdentifier) -> Self {
216        self.add_changed_entry(entry);
217        self
218    }
219
220    /// Adds multiple entries to the 'changed' set.
221    #[inline]
222    pub fn add_changed_entries(&mut self, entries: impl IntoIterator<Item = SymbolIdentifier>) {
223        self.changed.extend(entries);
224    }
225
226    /// Returns a new instance with multiple entries added to the 'changed' set.
227    #[inline]
228    #[must_use]
229    pub fn with_added_changed_entries(mut self, entries: impl IntoIterator<Item = SymbolIdentifier>) -> Self {
230        self.add_changed_entries(entries);
231        self
232    }
233
234    /// Clears the 'changed' set.
235    #[inline]
236    pub fn unset_changed(&mut self) {
237        self.changed.clear();
238    }
239
240    /// Returns a new instance with an empty 'changed' set.
241    #[inline]
242    #[must_use]
243    pub fn without_changed(mut self) -> Self {
244        self.unset_changed();
245        self
246    }
247
248    /// Sets the diff map, replacing the existing one.
249    #[inline]
250    pub fn set_diff_map(&mut self, map: HashMap<FileId, Vec<DiffHunk>>) {
251        self.diff_map = map;
252    }
253
254    /// Returns a new instance with the diff map replaced.
255    #[inline]
256    #[must_use]
257    pub fn with_diff_map(mut self, map: HashMap<FileId, Vec<DiffHunk>>) -> Self {
258        self.set_diff_map(map);
259        self
260    }
261
262    /// Adds or replaces the diff hunks for a specific source file. Returns previous hunks if any.
263    #[inline]
264    pub fn add_diff_map_entry(&mut self, source: FileId, diffs: Vec<DiffHunk>) -> Option<Vec<DiffHunk>> {
265        self.diff_map.insert(source, diffs)
266    }
267
268    /// Returns a new instance with the diff hunks for the source file added or updated.
269    #[inline]
270    #[must_use]
271    pub fn with_added_diff_map_entry(mut self, source: FileId, diffs: Vec<DiffHunk>) -> Self {
272        self.add_diff_map_entry(source, diffs);
273        self
274    }
275
276    /// Extends the diff hunks for a specific source file.
277    #[inline]
278    pub fn add_diffs_for_source(&mut self, source: FileId, diffs: impl IntoIterator<Item = DiffHunk>) {
279        self.diff_map.entry(source).or_default().extend(diffs);
280    }
281
282    /// Returns a new instance with the diff hunks for the source file extended.
283    #[inline]
284    #[must_use]
285    pub fn with_added_diffs_for_source(mut self, source: FileId, diffs: impl IntoIterator<Item = DiffHunk>) -> Self {
286        self.add_diffs_for_source(source, diffs);
287        self
288    }
289
290    /// Clears the diff map.
291    #[inline]
292    pub fn unset_diff_map(&mut self) {
293        self.diff_map.clear();
294    }
295
296    /// Returns a new instance with an empty diff map.
297    #[inline]
298    #[must_use]
299    pub fn without_diff_map(mut self) -> Self {
300        self.unset_diff_map();
301        self
302    }
303
304    /// Sets the deletion ranges map, replacing the existing one.
305    #[inline]
306    pub fn set_deletion_ranges_map(&mut self, map: HashMap<FileId, Vec<DeletionRange>>) {
307        self.deletion_ranges_map = map;
308    }
309
310    /// Returns a new instance with the deletion ranges map replaced.
311    #[inline]
312    #[must_use]
313    pub fn with_deletion_ranges_map(mut self, map: HashMap<FileId, Vec<DeletionRange>>) -> Self {
314        self.set_deletion_ranges_map(map);
315        self
316    }
317
318    /// Adds or replaces the deletion ranges for a specific source file. Returns previous ranges if any.
319    #[inline]
320    pub fn add_deletion_ranges_entry(
321        &mut self,
322        source: FileId,
323        ranges: Vec<DeletionRange>,
324    ) -> Option<Vec<DeletionRange>> {
325        self.deletion_ranges_map.insert(source, ranges)
326    }
327
328    /// Returns a new instance with the deletion ranges for the source file added or updated.
329    #[inline]
330    #[must_use]
331    pub fn with_added_deletion_ranges_entry(mut self, file: FileId, ranges: Vec<DeletionRange>) -> Self {
332        self.add_deletion_ranges_entry(file, ranges);
333        self
334    }
335
336    /// Extends the deletion ranges for a specific source file.
337    #[inline]
338    pub fn add_deletion_ranges_for_source(&mut self, file: FileId, ranges: impl IntoIterator<Item = (usize, usize)>) {
339        self.deletion_ranges_map.entry(file).or_default().extend(ranges);
340    }
341
342    /// Returns a new instance with the deletion ranges for the source file extended.
343    #[inline]
344    #[must_use]
345    pub fn with_added_deletion_ranges_for_source(
346        mut self,
347        file: FileId,
348        ranges: impl IntoIterator<Item = (usize, usize)>,
349    ) -> Self {
350        self.add_deletion_ranges_for_source(file, ranges);
351        self
352    }
353
354    /// Clears the deletion ranges map.
355    #[inline]
356    pub fn unset_deletion_ranges_map(&mut self) {
357        self.deletion_ranges_map.clear();
358    }
359
360    /// Returns a new instance with an empty deletion ranges map.
361    #[inline]
362    #[must_use]
363    pub fn without_deletion_ranges_map(mut self) -> Self {
364        self.unset_deletion_ranges_map();
365        self
366    }
367}