Skip to main content

mago_codex/
diff.rs

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