the_code_graph_domain/use_cases/
impact.rs1use crate::analysis::blast_radius::compute_blast_radius;
2use crate::analysis::change_detection::find_affected_symbols;
3use crate::error::Result;
4use crate::model::*;
5use crate::ports::GraphStore;
6use crate::traversal::InMemoryGraph;
7
8pub struct ImpactUseCase<S> {
9 store: S,
10}
11
12impl<S: GraphStore> ImpactUseCase<S> {
13 pub fn new(store: S) -> Self {
14 Self { store }
15 }
16
17 pub fn blast_radius(
18 &self,
19 targets: &[ImpactTarget],
20 max_depth: usize,
21 min_confidence: Confidence,
22 ) -> Result<ImpactReport> {
23 let mut graph = InMemoryGraph::new();
24 self.store.edges_streaming(&mut |edge| {
25 graph.add_edge(edge);
26 Ok(())
27 })?;
28 Ok(compute_blast_radius(
29 &graph,
30 targets,
31 max_depth,
32 min_confidence,
33 ))
34 }
35
36 pub fn diff_impact(
37 &self,
38 hunks: &[DiffHunk],
39 max_depth: usize,
40 min_confidence: Confidence,
41 ) -> Result<DiffImpactReport> {
42 let mut graph = InMemoryGraph::new();
43 self.store.edges_streaming(&mut |edge| {
44 graph.add_edge(edge);
45 Ok(())
46 })?;
47 let hunk_files: Vec<&std::path::Path> = hunks.iter().map(|h| h.file.as_path()).collect();
48 let symbols = self.store.symbols_for_files(&hunk_files)?;
49 let changed = find_affected_symbols(hunks, &symbols);
50 let targets: Vec<ImpactTarget> = changed
51 .iter()
52 .map(|s| ImpactTarget::Symbol(s.qualified_name.clone()))
53 .collect();
54 let impact = compute_blast_radius(&graph, &targets, max_depth, min_confidence);
55 Ok(DiffImpactReport {
56 changed_symbols: changed,
57 impact,
58 })
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::test_support::InMemoryGraphStore;
66
67 #[test]
68 fn blast_radius_with_mock_returns_transitive_closure() {
69 let mut store = InMemoryGraphStore::new();
70 store.insert_file(FileNode {
71 path: "a.rs".into(),
72 language: Language::Rust,
73 hash: "h1".into(),
74 });
75 store.insert_file(FileNode {
76 path: "b.rs".into(),
77 language: Language::Rust,
78 hash: "h2".into(),
79 });
80 store.insert_symbol(SymbolNode {
81 name: "foo".into(),
82 qualified_name: "a.rs::foo".into(),
83 kind: SymbolKind::Function,
84 location: Location {
85 file: "a.rs".into(),
86 line_start: 1,
87 line_end: 10,
88 col_start: 0,
89 col_end: 0,
90 },
91 visibility: Visibility::Public,
92 is_exported: true,
93 is_async: false,
94 is_test: false,
95 decorators: vec![],
96 signature: None,
97 });
98 store.insert_symbol(SymbolNode {
99 name: "bar".into(),
100 qualified_name: "b.rs::bar".into(),
101 kind: SymbolKind::Function,
102 location: Location {
103 file: "b.rs".into(),
104 line_start: 1,
105 line_end: 10,
106 col_start: 0,
107 col_end: 0,
108 },
109 visibility: Visibility::Public,
110 is_exported: true,
111 is_async: false,
112 is_test: false,
113 decorators: vec![],
114 signature: None,
115 });
116 store.insert_edge(Edge {
117 kind: EdgeKind::Calls,
118 source: "a.rs::foo".into(),
119 target: "b.rs::bar".into(),
120 metadata: None,
121 });
122
123 let uc = ImpactUseCase::new(store);
124 let report = uc
125 .blast_radius(
126 &[ImpactTarget::Symbol("a.rs::foo".into())],
127 3,
128 Confidence::Structural,
129 )
130 .unwrap();
131 assert!(report
132 .affected
133 .iter()
134 .any(|n| n.qualified_name == "b.rs::bar"));
135 }
136
137 #[test]
138 fn diff_impact_non_overlapping_returns_empty_report() {
139 let store = InMemoryGraphStore::new();
140 let uc = ImpactUseCase::new(store);
141 let hunks = vec![DiffHunk {
142 file: "nonexistent.rs".into(),
143 old_start: 1,
144 old_count: 1,
145 new_start: 1,
146 new_count: 1,
147 }];
148 let report = uc.diff_impact(&hunks, 3, Confidence::Structural).unwrap();
149 assert!(report.changed_symbols.is_empty());
150 }
151
152 #[test]
153 fn diff_impact_overlapping_returns_affected_symbols() {
154 let mut store = InMemoryGraphStore::new();
155 store.insert_symbol(SymbolNode {
156 name: "foo".into(),
157 qualified_name: "a.rs::foo".into(),
158 kind: SymbolKind::Function,
159 location: Location {
160 file: "a.rs".into(),
161 line_start: 10,
162 line_end: 20,
163 col_start: 0,
164 col_end: 0,
165 },
166 visibility: Visibility::Public,
167 is_exported: false,
168 is_async: false,
169 is_test: false,
170 decorators: vec![],
171 signature: None,
172 });
173 store.insert_edge(Edge {
174 kind: EdgeKind::Calls,
175 source: "a.rs::foo".into(),
176 target: "b.rs::bar".into(),
177 metadata: None,
178 });
179
180 let uc = ImpactUseCase::new(store);
181 let hunks = vec![DiffHunk {
182 file: "a.rs".into(),
183 old_start: 15,
184 old_count: 3,
185 new_start: 15,
186 new_count: 3,
187 }];
188 let report = uc.diff_impact(&hunks, 3, Confidence::Structural).unwrap();
189 assert_eq!(report.changed_symbols.len(), 1);
190 assert_eq!(report.changed_symbols[0].name, "foo");
191 assert!(report
192 .impact
193 .affected
194 .iter()
195 .any(|n| n.qualified_name == "b.rs::bar"));
196 }
197}