the_code_graph_domain/analysis/
change_detection.rs1use crate::model::{DiffHunk, SymbolNode};
2
3pub 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 let (hunk_start, hunk_end) = if hunk.new_count == 0 {
21 (
23 hunk.old_start,
24 hunk.old_start + hunk.old_count.saturating_sub(1),
25 )
26 } else {
27 (
29 hunk.new_start,
30 hunk.new_start + hunk.new_count.saturating_sub(1),
31 )
32 };
33
34 if sym_start <= hunk_end && hunk_start <= sym_end {
36 affected.push(symbol.clone());
37 break; }
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}