arbor_graph/
heuristics.rs1use 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().map_or(false, |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 infer_uncertain_edges(nodes: &[&CodeNode]) -> Vec<UncertainEdge> {
104 let mut edges = Vec::new();
105
106 for node in nodes {
107 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 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 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#[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
161pub 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}