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    /// Infer uncertain edges from node patterns
103    pub fn infer_uncertain_edges(nodes: &[&CodeNode]) -> Vec<UncertainEdge> {
104        let mut edges = Vec::new();
105
106        for node in nodes {
107            // Event handlers likely connected to event sources
108            if Self::is_event_handler(node) {
109                edges.push(UncertainEdge {
110                    from: "event_source".to_string(),
111                    to: node.id.clone(),
112                    kind: UncertainEdgeKind::EventHandler,
113                    confidence: 0.7,
114                    reason: format!("'{}' looks like an event handler", node.name),
115                });
116            }
117
118            // Callbacks likely invoked dynamically
119            if Self::is_callback_style(node) {
120                edges.push(UncertainEdge {
121                    from: "caller".to_string(),
122                    to: node.id.clone(),
123                    kind: UncertainEdgeKind::Callback,
124                    confidence: 0.6,
125                    reason: format!("'{}' is likely passed as a callback", node.name),
126                });
127            }
128
129            // Flutter widgets part of widget tree
130            if Self::is_flutter_widget(node) {
131                edges.push(UncertainEdge {
132                    from: "parent_widget".to_string(),
133                    to: node.id.clone(),
134                    kind: UncertainEdgeKind::WidgetTree,
135                    confidence: 0.8,
136                    reason: format!("'{}' is a Flutter widget in the widget tree", node.name),
137                });
138            }
139        }
140
141        edges
142    }
143}
144
145/// Warnings about analysis limitations
146#[derive(Debug, Clone)]
147pub struct AnalysisWarning {
148    pub message: String,
149    pub suggestion: String,
150}
151
152impl AnalysisWarning {
153    pub fn new(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
154        Self {
155            message: message.into(),
156            suggestion: suggestion.into(),
157        }
158    }
159}
160
161/// Check for common patterns that limit static analysis accuracy
162pub fn detect_analysis_limitations(nodes: &[&CodeNode]) -> Vec<AnalysisWarning> {
163    let mut warnings = Vec::new();
164
165    let callback_count = nodes
166        .iter()
167        .filter(|n| HeuristicsMatcher::is_callback_style(n))
168        .count();
169    if callback_count > 5 {
170        warnings.push(AnalysisWarning::new(
171            format!("Found {} callback-style nodes", callback_count),
172            "Callbacks may be invoked dynamically. Verify runtime behavior.",
173        ));
174    }
175
176    let event_handler_count = nodes
177        .iter()
178        .filter(|n| HeuristicsMatcher::is_event_handler(n))
179        .count();
180    if event_handler_count > 3 {
181        warnings.push(AnalysisWarning::new(
182            format!("Found {} event handlers", event_handler_count),
183            "Event handlers are connected at runtime. Check event sources.",
184        ));
185    }
186
187    let widget_count = nodes
188        .iter()
189        .filter(|n| HeuristicsMatcher::is_flutter_widget(n))
190        .count();
191    if widget_count > 0 {
192        warnings.push(AnalysisWarning::new(
193            format!("Detected {} Flutter widgets", widget_count),
194            "Widget tree hierarchy is determined at runtime.",
195        ));
196    }
197
198    warnings
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_flutter_widget_detection() {
207        let widget = CodeNode::new("HomeWidget", "HomeWidget", NodeKind::Class, "home.dart");
208        assert!(HeuristicsMatcher::is_flutter_widget(&widget));
209
210        let state = CodeNode::new("HomeState", "HomeState", NodeKind::Class, "home.dart");
211        assert!(HeuristicsMatcher::is_flutter_widget(&state));
212
213        let non_widget = CodeNode::new(
214            "UserService",
215            "UserService",
216            NodeKind::Class,
217            "service.dart",
218        );
219        assert!(!HeuristicsMatcher::is_flutter_widget(&non_widget));
220    }
221
222    #[test]
223    fn test_event_handler_detection() {
224        let handler = CodeNode::new("onClick", "onClick", NodeKind::Function, "button.ts");
225        assert!(HeuristicsMatcher::is_event_handler(&handler));
226
227        let handler2 = CodeNode::new(
228            "handleSubmit",
229            "handleSubmit",
230            NodeKind::Function,
231            "form.ts",
232        );
233        assert!(HeuristicsMatcher::is_event_handler(&handler2));
234
235        let non_handler = CodeNode::new("calculate", "calculate", NodeKind::Function, "math.ts");
236        assert!(!HeuristicsMatcher::is_event_handler(&non_handler));
237    }
238}