Skip to main content

the_code_graph_domain/analysis/
change_detection.rs

1use crate::model::{DiffHunk, SymbolNode};
2
3/// Given a set of diff hunks and known symbols, find which symbols are affected.
4/// Uses post-diff (new) line numbers for normal hunks.
5/// For pure deletions (new_count = 0), uses old line numbers.
6pub fn find_affected_symbols(hunks: &[DiffHunk], symbols: &[SymbolNode]) -> Vec<SymbolNode> {
7    let mut affected = Vec::new();
8    for symbol in symbols {
9        let sym_file = symbol.location.file.to_string_lossy();
10        let sym_start = symbol.location.line_start;
11        let sym_end = symbol.location.line_end;
12
13        for hunk in hunks {
14            let hunk_file = hunk.file.to_string_lossy();
15            if sym_file != hunk_file {
16                continue;
17            }
18
19            // Determine the hunk's effective line range
20            let (hunk_start, hunk_end) = if hunk.new_count == 0 {
21                // Pure deletion — use old line range
22                (
23                    hunk.old_start,
24                    hunk.old_start + hunk.old_count.saturating_sub(1),
25                )
26            } else {
27                // Normal change — use new line range
28                (
29                    hunk.new_start,
30                    hunk.new_start + hunk.new_count.saturating_sub(1),
31                )
32            };
33
34            // Check line range overlap
35            if sym_start <= hunk_end && hunk_start <= sym_end {
36                affected.push(symbol.clone());
37                break; // Don't add same symbol twice
38            }
39        }
40    }
41    affected
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use crate::model::*;
48
49    fn sym(name: &str, file: &str, start: usize, end: usize) -> SymbolNode {
50        SymbolNode {
51            name: name.into(),
52            qualified_name: format!("{file}::{name}"),
53            kind: SymbolKind::Function,
54            location: Location {
55                file: file.into(),
56                line_start: start,
57                line_end: end,
58                col_start: 0,
59                col_end: 0,
60            },
61            visibility: Visibility::Public,
62            is_exported: false,
63            is_async: false,
64            is_test: false,
65            decorators: vec![],
66            signature: None,
67        }
68    }
69
70    #[test]
71    fn overlapping_hunk_matches_symbol() {
72        let symbols = vec![sym("foo", "src/a.rs", 10, 20)];
73        let hunks = vec![DiffHunk {
74            file: "src/a.rs".into(),
75            old_start: 15,
76            old_count: 3,
77            new_start: 15,
78            new_count: 5,
79        }];
80        let affected = find_affected_symbols(&hunks, &symbols);
81        assert_eq!(affected.len(), 1);
82        assert_eq!(affected[0].name, "foo");
83    }
84
85    #[test]
86    fn non_overlapping_hunk_no_match() {
87        let symbols = vec![sym("foo", "src/a.rs", 10, 20)];
88        let hunks = vec![DiffHunk {
89            file: "src/a.rs".into(),
90            old_start: 25,
91            old_count: 3,
92            new_start: 25,
93            new_count: 3,
94        }];
95        let affected = find_affected_symbols(&hunks, &symbols);
96        assert!(affected.is_empty());
97    }
98
99    #[test]
100    fn different_file_no_match() {
101        let symbols = vec![sym("foo", "src/a.rs", 10, 20)];
102        let hunks = vec![DiffHunk {
103            file: "src/b.rs".into(),
104            old_start: 15,
105            old_count: 3,
106            new_start: 15,
107            new_count: 3,
108        }];
109        let affected = find_affected_symbols(&hunks, &symbols);
110        assert!(affected.is_empty());
111    }
112
113    #[test]
114    fn pure_deletion_hunk_matches_symbol() {
115        let symbols = vec![sym("foo", "src/a.rs", 10, 20)];
116        let hunks = vec![DiffHunk {
117            file: "src/a.rs".into(),
118            old_start: 12,
119            old_count: 3,
120            new_start: 12,
121            new_count: 0,
122        }];
123        let affected = find_affected_symbols(&hunks, &symbols);
124        assert_eq!(affected.len(), 1);
125    }
126}