1use crate::types::SemanticAnalysis;
2use std::collections::HashMap;
3use std::path::PathBuf;
4use tracing::instrument;
5
6#[derive(Debug, Clone)]
9pub struct DataflowGraph {
10 pub assignments: HashMap<String, Vec<(PathBuf, usize, String)>>,
12 pub field_accesses: HashMap<String, Vec<(PathBuf, usize, String)>>,
14}
15
16impl DataflowGraph {
17 pub fn new() -> Self {
19 Self {
20 assignments: HashMap::new(),
21 field_accesses: HashMap::new(),
22 }
23 }
24
25 #[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 pub fn find_assignments(&self, symbol: &str) -> Vec<(PathBuf, usize, String)> {
51 self.assignments.get(symbol).cloned().unwrap_or_default()
52 }
53
54 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}