1use arbor_core::{CodeNode, NodeKind};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum UncertainEdgeKind {
13 Callback,
15 DynamicDispatch,
17 WidgetTree,
19 EventHandler,
21 DependencyInjection,
23 Reflection,
25}
26
27impl std::fmt::Display for UncertainEdgeKind {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 UncertainEdgeKind::Callback => write!(f, "callback"),
31 UncertainEdgeKind::DynamicDispatch => write!(f, "dynamic dispatch"),
32 UncertainEdgeKind::WidgetTree => write!(f, "widget tree"),
33 UncertainEdgeKind::EventHandler => write!(f, "event handler"),
34 UncertainEdgeKind::DependencyInjection => write!(f, "dependency injection"),
35 UncertainEdgeKind::Reflection => write!(f, "reflection"),
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct UncertainEdge {
43 pub from: String,
44 pub to: String,
45 pub kind: UncertainEdgeKind,
46 pub confidence: f32, pub reason: String,
48}
49
50pub struct HeuristicsMatcher;
52
53impl HeuristicsMatcher {
54 pub fn is_flutter_widget(node: &CodeNode) -> bool {
56 node.kind == NodeKind::Class
58 && (node.name.ends_with("Widget")
59 || node.name.ends_with("State")
60 || node.name.ends_with("Page")
61 || node.name.ends_with("Screen")
62 || node.name.ends_with("View"))
63 }
64
65 pub fn is_react_component(node: &CodeNode) -> bool {
67 (node.kind == NodeKind::Function || node.kind == NodeKind::Class)
68 && node.file.ends_with(".tsx")
69 && node.name.chars().next().is_some_and(|c| c.is_uppercase())
70 }
71
72 pub fn is_event_handler(node: &CodeNode) -> bool {
74 let name_lower = node.name.to_lowercase();
75 (node.kind == NodeKind::Function || node.kind == NodeKind::Method)
76 && (name_lower.starts_with("on")
77 || name_lower.starts_with("handle")
78 || name_lower.ends_with("handler")
79 || name_lower.ends_with("callback")
80 || name_lower.ends_with("listener"))
81 }
82
83 pub fn is_callback_style(node: &CodeNode) -> bool {
85 let name_lower = node.name.to_lowercase();
86 name_lower.ends_with("fn")
87 || name_lower.ends_with("callback")
88 || name_lower.ends_with("handler")
89 || name_lower.starts_with("on_")
90 }
91
92 pub fn is_dependency_injection(node: &CodeNode) -> bool {
94 let name_lower = node.name.to_lowercase();
95 name_lower.ends_with("factory")
96 || name_lower.ends_with("provider")
97 || name_lower.ends_with("injector")
98 || name_lower.ends_with("container")
99 || name_lower.contains("singleton")
100 }
101
102 pub fn is_likely_entry_point(node: &CodeNode) -> bool {
109 if !matches!(node.kind, NodeKind::Function | NodeKind::Method) {
110 return false;
111 }
112 let name = node.name.to_lowercase();
113 let file = node.file.to_lowercase();
114
115 if node.name == "main" || node.name == "__main__" {
117 return true;
118 }
119
120 if name.ends_with("_view")
122 || name.ends_with("_handler")
123 || name.ends_with("_controller")
124 || name.ends_with("_endpoint")
125 || name.starts_with("handle_")
126 || name.starts_with("get_")
127 || name.starts_with("post_")
128 || name.starts_with("put_")
129 || name.starts_with("delete_")
130 || name.starts_with("patch_")
131 {
132 if file.contains("route")
134 || file.contains("view")
135 || file.contains("handler")
136 || file.contains("controller")
137 || file.contains("endpoint")
138 || file.contains("api")
139 {
140 return true;
141 }
142 }
143
144 if name.contains("webhook")
146 || name.contains("receive")
147 || name.contains("subscribe")
148 || name.starts_with("on_")
149 || (name.starts_with("handle") && !file.contains("test"))
150 {
151 return true;
152 }
153
154 if (name.contains("job")
156 || name.contains("task")
157 || name.contains("worker")
158 || name.contains("cron")
159 || name.ends_with("_run")
160 || name == "run"
161 || name == "execute"
162 || name == "process")
163 && (file.contains("job")
164 || file.contains("task")
165 || file.contains("worker")
166 || file.contains("cron")
167 || file.contains("celery")
168 || file.contains("sidekiq")
169 || file.contains("background"))
170 {
171 return true;
172 }
173
174 if name.contains("command") || name.ends_with("_cmd") || name == "cli" || name == "invoke" {
176 return true;
177 }
178
179 false
180 }
181
182 pub fn infer_uncertain_edges(nodes: &[&CodeNode]) -> Vec<UncertainEdge> {
184 let mut edges = Vec::new();
185
186 for node in nodes {
187 if Self::is_event_handler(node) {
189 edges.push(UncertainEdge {
190 from: "event_source".to_string(),
191 to: node.id.clone(),
192 kind: UncertainEdgeKind::EventHandler,
193 confidence: 0.7,
194 reason: format!("'{}' looks like an event handler", node.name),
195 });
196 }
197
198 if Self::is_callback_style(node) {
200 edges.push(UncertainEdge {
201 from: "caller".to_string(),
202 to: node.id.clone(),
203 kind: UncertainEdgeKind::Callback,
204 confidence: 0.6,
205 reason: format!("'{}' is likely passed as a callback", node.name),
206 });
207 }
208
209 if Self::is_flutter_widget(node) {
211 edges.push(UncertainEdge {
212 from: "parent_widget".to_string(),
213 to: node.id.clone(),
214 kind: UncertainEdgeKind::WidgetTree,
215 confidence: 0.8,
216 reason: format!("'{}' is a Flutter widget in the widget tree", node.name),
217 });
218 }
219 }
220
221 edges
222 }
223}
224
225#[derive(Debug, Clone)]
227pub struct AnalysisWarning {
228 pub message: String,
229 pub suggestion: String,
230}
231
232impl AnalysisWarning {
233 pub fn new(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
234 Self {
235 message: message.into(),
236 suggestion: suggestion.into(),
237 }
238 }
239}
240
241pub fn detect_analysis_limitations(nodes: &[&CodeNode]) -> Vec<AnalysisWarning> {
243 let mut warnings = Vec::new();
244
245 let callback_count = nodes
246 .iter()
247 .filter(|n| HeuristicsMatcher::is_callback_style(n))
248 .count();
249 if callback_count > 5 {
250 warnings.push(AnalysisWarning::new(
251 format!("Found {} callback-style nodes", callback_count),
252 "Callbacks may be invoked dynamically. Verify runtime behavior.",
253 ));
254 }
255
256 let event_handler_count = nodes
257 .iter()
258 .filter(|n| HeuristicsMatcher::is_event_handler(n))
259 .count();
260 if event_handler_count > 3 {
261 warnings.push(AnalysisWarning::new(
262 format!("Found {} event handlers", event_handler_count),
263 "Event handlers are connected at runtime. Check event sources.",
264 ));
265 }
266
267 let widget_count = nodes
268 .iter()
269 .filter(|n| HeuristicsMatcher::is_flutter_widget(n))
270 .count();
271 if widget_count > 0 {
272 warnings.push(AnalysisWarning::new(
273 format!("Detected {} Flutter widgets", widget_count),
274 "Widget tree hierarchy is determined at runtime.",
275 ));
276 }
277
278 warnings
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_flutter_widget_detection() {
287 let widget = CodeNode::new("HomeWidget", "HomeWidget", NodeKind::Class, "home.dart");
288 assert!(HeuristicsMatcher::is_flutter_widget(&widget));
289
290 let state = CodeNode::new("HomeState", "HomeState", NodeKind::Class, "home.dart");
291 assert!(HeuristicsMatcher::is_flutter_widget(&state));
292
293 let non_widget = CodeNode::new(
294 "UserService",
295 "UserService",
296 NodeKind::Class,
297 "service.dart",
298 );
299 assert!(!HeuristicsMatcher::is_flutter_widget(&non_widget));
300 }
301
302 #[test]
303 fn test_event_handler_detection() {
304 let handler = CodeNode::new("onClick", "onClick", NodeKind::Function, "button.ts");
305 assert!(HeuristicsMatcher::is_event_handler(&handler));
306
307 let handler2 = CodeNode::new(
308 "handleSubmit",
309 "handleSubmit",
310 NodeKind::Function,
311 "form.ts",
312 );
313 assert!(HeuristicsMatcher::is_event_handler(&handler2));
314
315 let non_handler = CodeNode::new("calculate", "calculate", NodeKind::Function, "math.ts");
316 assert!(!HeuristicsMatcher::is_event_handler(&non_handler));
317 }
318
319 #[test]
320 fn test_react_component_detection() {
321 let component = CodeNode::new(
322 "UserProfile",
323 "UserProfile",
324 NodeKind::Function,
325 "profile.tsx",
326 );
327 assert!(HeuristicsMatcher::is_react_component(&component));
328
329 let non_component = CodeNode::new("helper", "helper", NodeKind::Function, "utils.tsx");
330 assert!(!HeuristicsMatcher::is_react_component(&non_component));
331
332 let wrong_ext = CodeNode::new(
334 "UserProfile",
335 "UserProfile",
336 NodeKind::Function,
337 "profile.rs",
338 );
339 assert!(!HeuristicsMatcher::is_react_component(&wrong_ext));
340
341 let class_comp = CodeNode::new("AppContainer", "AppContainer", NodeKind::Class, "app.tsx");
343 assert!(HeuristicsMatcher::is_react_component(&class_comp));
344 }
345
346 #[test]
347 fn test_callback_style_detection() {
348 let callback = CodeNode::new(
349 "on_click_handler",
350 "on_click_handler",
351 NodeKind::Function,
352 "a.rs",
353 );
354 assert!(HeuristicsMatcher::is_callback_style(&callback));
355
356 let callback_fn = CodeNode::new("sortFn", "sortFn", NodeKind::Function, "a.ts");
357 assert!(HeuristicsMatcher::is_callback_style(&callback_fn));
358
359 let regular = CodeNode::new("process_data", "process_data", NodeKind::Function, "a.rs");
360 assert!(!HeuristicsMatcher::is_callback_style(®ular));
361 }
362
363 #[test]
364 fn test_dependency_injection_detection() {
365 let factory = CodeNode::new("UserFactory", "UserFactory", NodeKind::Class, "factory.ts");
366 assert!(HeuristicsMatcher::is_dependency_injection(&factory));
367
368 let provider = CodeNode::new("AuthProvider", "AuthProvider", NodeKind::Class, "auth.ts");
369 assert!(HeuristicsMatcher::is_dependency_injection(&provider));
370
371 let regular = CodeNode::new("UserService", "UserService", NodeKind::Class, "service.ts");
372 assert!(!HeuristicsMatcher::is_dependency_injection(®ular));
373 }
374
375 #[test]
376 fn test_infer_uncertain_edges_from_patterns() {
377 let handler = CodeNode::new("onClick", "onClick", NodeKind::Function, "button.ts");
378 let widget = CodeNode::new("HomeWidget", "HomeWidget", NodeKind::Class, "home.dart");
379 let regular = CodeNode::new("calculate", "calculate", NodeKind::Function, "math.ts");
380
381 let nodes: Vec<&CodeNode> = vec![&handler, &widget, ®ular];
382 let edges = HeuristicsMatcher::infer_uncertain_edges(&nodes);
383
384 assert!(edges
386 .iter()
387 .any(|e| matches!(e.kind, UncertainEdgeKind::EventHandler)));
388 assert!(edges
389 .iter()
390 .any(|e| matches!(e.kind, UncertainEdgeKind::WidgetTree)));
391 assert!(!edges.iter().any(|e| e.to == regular.id));
393 }
394
395 #[test]
396 fn test_detect_analysis_limitations_callbacks() {
397 let nodes: Vec<CodeNode> = (0..7)
399 .map(|i| {
400 CodeNode::new(
401 format!("on_event_{}", i),
402 format!("on_event_{}", i),
403 NodeKind::Function,
404 "events.ts",
405 )
406 })
407 .collect();
408 let node_refs: Vec<&CodeNode> = nodes.iter().collect();
409
410 let warnings = detect_analysis_limitations(&node_refs);
411 assert!(!warnings.is_empty());
412 assert!(warnings.iter().any(|w| w.message.contains("callback")));
413 }
414
415 #[test]
416 fn test_detect_analysis_limitations_flutter_widgets() {
417 let widgets: Vec<CodeNode> = vec![CodeNode::new(
418 "HomeWidget",
419 "HomeWidget",
420 NodeKind::Class,
421 "home.dart",
422 )];
423 let node_refs: Vec<&CodeNode> = widgets.iter().collect();
424
425 let warnings = detect_analysis_limitations(&node_refs);
426 assert!(warnings.iter().any(|w| w.message.contains("Flutter")));
427 }
428
429 #[test]
430 fn test_uncertain_edge_kind_display() {
431 assert_eq!(UncertainEdgeKind::Callback.to_string(), "callback");
432 assert_eq!(
433 UncertainEdgeKind::DynamicDispatch.to_string(),
434 "dynamic dispatch"
435 );
436 assert_eq!(UncertainEdgeKind::WidgetTree.to_string(), "widget tree");
437 assert_eq!(UncertainEdgeKind::EventHandler.to_string(), "event handler");
438 assert_eq!(
439 UncertainEdgeKind::DependencyInjection.to_string(),
440 "dependency injection"
441 );
442 assert_eq!(UncertainEdgeKind::Reflection.to_string(), "reflection");
443 }
444
445 #[test]
446 fn test_no_warnings_for_clean_code() {
447 let nodes: Vec<CodeNode> = vec![
448 CodeNode::new("main", "main", NodeKind::Function, "main.rs"),
449 CodeNode::new("helper", "helper", NodeKind::Function, "utils.rs"),
450 ];
451 let node_refs: Vec<&CodeNode> = nodes.iter().collect();
452
453 let warnings = detect_analysis_limitations(&node_refs);
454 assert!(warnings.is_empty());
455 }
456}