agentstatedeveloper_core/
dataflow.rs1use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum DataFlowKind {
21 ArgToParam,
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct DataFlowEdge {
29 pub kind: DataFlowKind,
30 pub from_symbol_id: String,
31 pub from_qname: String,
32 pub to_symbol_id: String,
33 pub to_qname: String,
34 pub param: String,
36 pub arg: String,
38 pub file: String,
39 pub line: u32,
40 pub confidence: f64,
41}
42
43#[derive(Debug, Clone, PartialEq)]
48pub struct DetectedDataFlow {
49 pub caller_qname: String,
50 pub callee_qname: String,
51 pub arg_index: usize,
53 pub arg: String,
55 pub file: String,
56 pub line: u32,
57 pub confidence: f64,
58}
59
60pub fn parse_params(signature: &str) -> Vec<String> {
65 let Some(open) = signature.find('(') else {
66 return Vec::new();
67 };
68 let after = &signature[open + 1..];
69 let Some(close) = matching_paren(after) else {
70 return Vec::new();
71 };
72 let inner = &after[..close];
73 split_top_level(inner)
74 .into_iter()
75 .filter_map(|p| {
76 let p = p.trim();
77 if p.starts_with('*') {
79 return None;
80 }
81 let name = p.split([':', '=']).next().unwrap_or("").trim();
83 if name.is_empty() || name == "self" || name == "cls" {
84 None
85 } else {
86 Some(name.to_string())
87 }
88 })
89 .collect()
90}
91
92fn split_top_level(s: &str) -> Vec<String> {
95 let mut out = Vec::new();
96 let mut depth = 0i32;
97 let mut start = 0usize;
98 for (i, c) in s.char_indices() {
99 match c {
100 '(' | '[' | '{' => depth += 1,
101 ')' | ']' | '}' => depth -= 1,
102 ',' if depth == 0 => {
103 out.push(s[start..i].to_string());
104 start = i + 1;
105 }
106 _ => {}
107 }
108 }
109 out.push(s[start..].to_string());
110 out
111}
112
113fn matching_paren(s: &str) -> Option<usize> {
116 let mut depth = 0i32;
117 for (i, c) in s.char_indices() {
118 match c {
119 '(' | '[' | '{' => depth += 1,
120 ')' if depth == 0 => return Some(i),
121 ')' | ']' | '}' => depth -= 1,
122 _ => {}
123 }
124 }
125 None
126}
127
128pub fn resolve_edge(
133 det: &DetectedDataFlow,
134 sym_id_of: impl Fn(&str) -> Option<String>,
135 params_of: impl Fn(&str) -> Option<Vec<String>>,
136) -> Option<DataFlowEdge> {
137 let from_symbol_id = sym_id_of(&det.caller_qname)?;
138 let to_symbol_id = sym_id_of(&det.callee_qname)?;
139 let params = params_of(&det.callee_qname)?;
140 let param = params.get(det.arg_index)?.clone();
141 Some(DataFlowEdge {
142 kind: DataFlowKind::ArgToParam,
143 from_symbol_id,
144 from_qname: det.caller_qname.clone(),
145 to_symbol_id,
146 to_qname: det.callee_qname.clone(),
147 param,
148 arg: det.arg.clone(),
149 file: det.file.clone(),
150 line: det.line,
151 confidence: det.confidence,
152 })
153}
154
155pub fn edges_from_tree(tree: &serde_json::Value) -> Vec<DataFlowEdge> {
158 let mut out = Vec::new();
159 let Some(by_from) = tree.as_object() else {
160 return out;
161 };
162 for arr in by_from.values() {
163 let Some(arr) = arr.as_array() else {
164 continue;
165 };
166 for e in arr {
167 if let Ok(edge) = serde_json::from_value::<DataFlowEdge>(e.clone()) {
168 out.push(edge);
169 }
170 }
171 }
172 out
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn parse_params_basic_and_annotated() {
181 assert_eq!(parse_params("def f(a, b, c)"), vec!["a", "b", "c"]);
182 assert_eq!(
183 parse_params("def get_user(id: int, name: str = \"x\")"),
184 vec!["id", "name"]
185 );
186 }
187
188 #[test]
189 fn parse_params_skips_receiver_and_varargs() {
190 assert_eq!(parse_params("def m(self, x, *args, **kwargs)"), vec!["x"]);
191 assert_eq!(parse_params("def c(cls, y)"), vec!["y"]);
192 }
193
194 #[test]
195 fn parse_params_handles_nested_parens_in_defaults() {
196 assert_eq!(
198 parse_params("def f(a, b=foo(1, 2), c)"),
199 vec!["a", "b", "c"]
200 );
201 }
202
203 #[test]
204 fn parse_params_empty_and_malformed() {
205 assert!(parse_params("def f()").is_empty());
206 assert!(parse_params("no parens here").is_empty());
207 }
208
209 #[test]
210 fn resolve_edge_maps_position_to_param() {
211 let det = DetectedDataFlow {
212 caller_qname: "m.caller".into(),
213 callee_qname: "m.get_user".into(),
214 arg_index: 0,
215 arg: "uid".into(),
216 file: "m.py".into(),
217 line: 4,
218 confidence: 0.8,
219 };
220 let edge = resolve_edge(
221 &det,
222 |q| Some(format!("sym_{q}")),
223 |q| (q == "m.get_user").then(|| vec!["id".to_string(), "name".to_string()]),
224 )
225 .expect("edge resolves");
226 assert_eq!(edge.from_symbol_id, "sym_m.caller");
227 assert_eq!(edge.to_symbol_id, "sym_m.get_user");
228 assert_eq!(edge.param, "id");
229 assert_eq!(edge.arg, "uid");
230 }
231
232 #[test]
233 fn resolve_edge_none_when_arg_position_has_no_param() {
234 let det = DetectedDataFlow {
235 caller_qname: "m.caller".into(),
236 callee_qname: "m.f".into(),
237 arg_index: 5, arg: "x".into(),
239 file: "m.py".into(),
240 line: 1,
241 confidence: 0.8,
242 };
243 assert!(
244 resolve_edge(
245 &det,
246 |q| Some(q.to_string()),
247 |_| Some(vec!["only".to_string()])
248 )
249 .is_none()
250 );
251 }
252
253 #[test]
254 fn resolve_edge_none_when_callee_unresolved() {
255 let det = DetectedDataFlow {
256 caller_qname: "m.caller".into(),
257 callee_qname: "external.thing".into(),
258 arg_index: 0,
259 arg: "x".into(),
260 file: "m.py".into(),
261 line: 1,
262 confidence: 0.8,
263 };
264 assert!(resolve_edge(&det, |q| Some(q.to_string()), |_| None).is_none());
266 }
267}