Skip to main content

the_code_graph_domain/use_cases/
impact.rs

1use 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}