Skip to main content

code_analyze_mcp/
dataflow.rs

1use crate::types::SemanticAnalysis;
2use std::collections::HashMap;
3use std::path::PathBuf;
4use tracing::instrument;
5
6/// DataflowGraph tracks variable assignments and field accesses.
7/// Internal struct (no Serialize/Deserialize); provides query interface for focused analysis.
8#[derive(Debug, Clone)]
9pub struct DataflowGraph {
10    /// Map: variable_name -> vec of (file_path, line, scope)
11    pub assignments: HashMap<String, Vec<(PathBuf, usize, String)>>,
12    /// Map: object.field -> vec of (file_path, line, scope)
13    pub field_accesses: HashMap<String, Vec<(PathBuf, usize, String)>>,
14}
15
16impl DataflowGraph {
17    /// Create a new empty DataflowGraph.
18    pub fn new() -> Self {
19        Self {
20            assignments: HashMap::new(),
21            field_accesses: HashMap::new(),
22        }
23    }
24
25    /// Build a DataflowGraph from analysis results across multiple files.
26    #[instrument(skip(results))]
27    pub fn build_from_results(results: &[(PathBuf, SemanticAnalysis)]) -> Self {
28        let mut graph = Self::new();
29        for (path, analysis) in results {
30            for assignment in &analysis.assignments {
31                graph
32                    .assignments
33                    .entry(assignment.variable.clone())
34                    .or_default()
35                    .push((path.clone(), assignment.line, assignment.scope.clone()));
36            }
37            for field_access in &analysis.field_accesses {
38                let key = format!("{}.{}", field_access.object, field_access.field);
39                graph.field_accesses.entry(key).or_default().push((
40                    path.clone(),
41                    field_access.line,
42                    field_access.scope.clone(),
43                ));
44            }
45        }
46        graph
47    }
48
49    /// Find all assignments to a variable by name.
50    pub fn find_assignments(&self, symbol: &str) -> Vec<(PathBuf, usize, String)> {
51        self.assignments.get(symbol).cloned().unwrap_or_default()
52    }
53
54    /// Find all field accesses where the object matches the given symbol.
55    /// Searches for keys prefixed with `symbol.` (e.g., symbol "user" matches "user.name", "user.age").
56    pub fn find_field_accesses(&self, symbol: &str) -> Vec<(PathBuf, usize, String)> {
57        let prefix = format!("{}.", symbol);
58        self.field_accesses
59            .iter()
60            .filter(|(key, _)| key.starts_with(&prefix))
61            .flat_map(|(_, entries)| entries.clone())
62            .collect()
63    }
64}
65
66impl Default for DataflowGraph {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::types::{AssignmentInfo, FieldAccessInfo};
76
77    #[test]
78    fn test_dataflow_graph_construction() {
79        let mut analysis1 = SemanticAnalysis {
80            functions: Vec::new(),
81            classes: Vec::new(),
82            imports: Vec::new(),
83            references: Vec::new(),
84            call_frequency: Default::default(),
85            calls: Vec::new(),
86            assignments: vec![
87                AssignmentInfo {
88                    variable: "x".to_string(),
89                    value: "42".to_string(),
90                    line: 5,
91                    scope: "main".to_string(),
92                },
93                AssignmentInfo {
94                    variable: "y".to_string(),
95                    value: "x + 1".to_string(),
96                    line: 6,
97                    scope: "main".to_string(),
98                },
99            ],
100            field_accesses: vec![FieldAccessInfo {
101                object: "obj".to_string(),
102                field: "name".to_string(),
103                line: 7,
104                scope: "main".to_string(),
105            }],
106        };
107
108        let path1 = PathBuf::from("test.rs");
109        let graph = DataflowGraph::build_from_results(&[(path1.clone(), analysis1)]);
110
111        let x_assignments = graph.find_assignments("x");
112        assert_eq!(x_assignments.len(), 1);
113        assert_eq!(x_assignments[0].1, 5);
114        assert_eq!(x_assignments[0].2, "main");
115
116        let y_assignments = graph.find_assignments("y");
117        assert_eq!(y_assignments.len(), 1);
118        assert_eq!(y_assignments[0].1, 6);
119
120        let field_accesses = graph.find_field_accesses("obj");
121        assert_eq!(field_accesses.len(), 1);
122        assert_eq!(field_accesses[0].1, 7);
123    }
124
125    #[test]
126    fn test_dataflow_graph_shadowed_variables() {
127        let analysis1 = SemanticAnalysis {
128            functions: Vec::new(),
129            classes: Vec::new(),
130            imports: Vec::new(),
131            references: Vec::new(),
132            call_frequency: Default::default(),
133            calls: Vec::new(),
134            assignments: vec![
135                AssignmentInfo {
136                    variable: "x".to_string(),
137                    value: "10".to_string(),
138                    line: 3,
139                    scope: "outer".to_string(),
140                },
141                AssignmentInfo {
142                    variable: "x".to_string(),
143                    value: "20".to_string(),
144                    line: 8,
145                    scope: "inner".to_string(),
146                },
147            ],
148            field_accesses: Vec::new(),
149        };
150
151        let path1 = PathBuf::from("test.rs");
152        let graph = DataflowGraph::build_from_results(&[(path1, analysis1)]);
153
154        let x_assignments = graph.find_assignments("x");
155        assert_eq!(
156            x_assignments.len(),
157            2,
158            "Both shadowed assignments should be tracked"
159        );
160        assert_eq!(x_assignments[0].2, "outer");
161        assert_eq!(x_assignments[1].2, "inner");
162    }
163
164    #[test]
165    fn test_find_field_accesses_no_false_prefix_match() {
166        let analysis = SemanticAnalysis {
167            functions: Vec::new(),
168            classes: Vec::new(),
169            imports: Vec::new(),
170            references: Vec::new(),
171            call_frequency: Default::default(),
172            calls: Vec::new(),
173            assignments: Vec::new(),
174            field_accesses: vec![
175                FieldAccessInfo {
176                    object: "objective".to_string(),
177                    field: "status".to_string(),
178                    line: 5,
179                    scope: "run".to_string(),
180                },
181                FieldAccessInfo {
182                    object: "obj".to_string(),
183                    field: "name".to_string(),
184                    line: 10,
185                    scope: "run".to_string(),
186                },
187            ],
188        };
189
190        let path = PathBuf::from("test.rs");
191        let graph = DataflowGraph::build_from_results(&[(path, analysis)]);
192
193        let matches = graph.find_field_accesses("obj");
194        assert_eq!(
195            matches.len(),
196            1,
197            "must not match 'objective.status' for symbol 'obj'"
198        );
199        assert_eq!(matches[0].1, 10);
200    }
201}