Skip to main content

arbor_graph/
heuristics.rs

1//! Heuristics for detecting runtime edges and framework patterns
2//!
3//! Real codebases aren't clean. This module provides best-effort detection of:
4//! - Dynamic/callback calls
5//! - Framework-specific patterns (Flutter widgets, etc.)
6//! - Possible runtime dependencies
7
8use arbor_core::{CodeNode, NodeKind};
9
10/// Types of uncertain edges
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum UncertainEdgeKind {
13    /// Callback or closure passed as argument
14    Callback,
15    /// Dynamic dispatch (trait objects, interfaces)
16    DynamicDispatch,
17    /// Framework widget tree (Flutter, React, etc.)
18    WidgetTree,
19    /// Event handler registration
20    EventHandler,
21    /// Dependency injection
22    DependencyInjection,
23    /// Reflection or runtime lookup
24    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/// An edge that might exist at runtime but cannot be proven statically
41#[derive(Debug, Clone)]
42pub struct UncertainEdge {
43    pub from: String,
44    pub to: String,
45    pub kind: UncertainEdgeKind,
46    pub confidence: f32, // 0.0 to 1.0
47    pub reason: String,
48}
49
50/// Pattern matchers for different frameworks and languages
51pub struct HeuristicsMatcher;
52
53impl HeuristicsMatcher {
54    /// Check if a node looks like a Flutter widget
55    pub fn is_flutter_widget(node: &CodeNode) -> bool {
56        // Widget classes typically extend StatelessWidget or StatefulWidget
57        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    /// Check if a node looks like a React component
66    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    /// Check if a node looks like an event handler
73    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    /// Check if a node looks like a callback parameter
84    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    /// Check if a node looks like a factory or provider (DI pattern)
93    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    /// Check if a node is likely a production entry point.
103    ///
104    /// Entry points are the outermost functions that receive external requests —
105    /// HTTP handlers, CLI commands, cron jobs, webhook receivers, main functions.
106    /// They're the roots of execution trees; if a changed function reaches one,
107    /// it means the change can affect real production traffic.
108    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        // Main / program entry
116        if node.name == "main" || node.name == "__main__" {
117            return true;
118        }
119
120        // HTTP route handlers (Express, Gin, axum, FastAPI, Flask, Django, Rails)
121        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            // Only count as entry point if in a routes/views/handlers/controllers file
133            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        // Webhook / event receivers
145        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        // Background jobs / cron / workers
155        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        // CLI command handlers
175        if name.contains("command") || name.ends_with("_cmd") || name == "cli" || name == "invoke" {
176            return true;
177        }
178
179        false
180    }
181
182    /// Infer uncertain edges from node patterns
183    pub fn infer_uncertain_edges(nodes: &[&CodeNode]) -> Vec<UncertainEdge> {
184        let mut edges = Vec::new();
185
186        for node in nodes {
187            // Event handlers likely connected to event sources
188            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            // Callbacks likely invoked dynamically
199            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            // Flutter widgets part of widget tree
210            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/// Warnings about analysis limitations
226#[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
241/// Check for common patterns that limit static analysis accuracy
242pub 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        // Not a .tsx file -> not a React component
333        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        // Class in .tsx is also a React component
342        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(&regular));
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(&regular));
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, &regular];
382        let edges = HeuristicsMatcher::infer_uncertain_edges(&nodes);
383
384        // Should have edges for handler (EventHandler) and widget (WidgetTree)
385        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        // Regular function shouldn't produce uncertain edges
392        assert!(!edges.iter().any(|e| e.to == regular.id));
393    }
394
395    #[test]
396    fn test_detect_analysis_limitations_callbacks() {
397        // Create 6+ callback-style nodes to trigger the warning
398        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}