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 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
239 #[test]
240 fn test_react_component_detection() {
241 let component = CodeNode::new(
242 "UserProfile",
243 "UserProfile",
244 NodeKind::Function,
245 "profile.tsx",
246 );
247 assert!(HeuristicsMatcher::is_react_component(&component));
248
249 let non_component = CodeNode::new("helper", "helper", NodeKind::Function, "utils.tsx");
250 assert!(!HeuristicsMatcher::is_react_component(&non_component));
251
252 let wrong_ext = CodeNode::new(
254 "UserProfile",
255 "UserProfile",
256 NodeKind::Function,
257 "profile.rs",
258 );
259 assert!(!HeuristicsMatcher::is_react_component(&wrong_ext));
260
261 let class_comp = CodeNode::new("AppContainer", "AppContainer", NodeKind::Class, "app.tsx");
263 assert!(HeuristicsMatcher::is_react_component(&class_comp));
264 }
265
266 #[test]
267 fn test_callback_style_detection() {
268 let callback = CodeNode::new(
269 "on_click_handler",
270 "on_click_handler",
271 NodeKind::Function,
272 "a.rs",
273 );
274 assert!(HeuristicsMatcher::is_callback_style(&callback));
275
276 let callback_fn = CodeNode::new("sortFn", "sortFn", NodeKind::Function, "a.ts");
277 assert!(HeuristicsMatcher::is_callback_style(&callback_fn));
278
279 let regular = CodeNode::new("process_data", "process_data", NodeKind::Function, "a.rs");
280 assert!(!HeuristicsMatcher::is_callback_style(®ular));
281 }
282
283 #[test]
284 fn test_dependency_injection_detection() {
285 let factory = CodeNode::new("UserFactory", "UserFactory", NodeKind::Class, "factory.ts");
286 assert!(HeuristicsMatcher::is_dependency_injection(&factory));
287
288 let provider = CodeNode::new("AuthProvider", "AuthProvider", NodeKind::Class, "auth.ts");
289 assert!(HeuristicsMatcher::is_dependency_injection(&provider));
290
291 let regular = CodeNode::new("UserService", "UserService", NodeKind::Class, "service.ts");
292 assert!(!HeuristicsMatcher::is_dependency_injection(®ular));
293 }
294
295 #[test]
296 fn test_infer_uncertain_edges_from_patterns() {
297 let handler = CodeNode::new("onClick", "onClick", NodeKind::Function, "button.ts");
298 let widget = CodeNode::new("HomeWidget", "HomeWidget", NodeKind::Class, "home.dart");
299 let regular = CodeNode::new("calculate", "calculate", NodeKind::Function, "math.ts");
300
301 let nodes: Vec<&CodeNode> = vec![&handler, &widget, ®ular];
302 let edges = HeuristicsMatcher::infer_uncertain_edges(&nodes);
303
304 assert!(edges
306 .iter()
307 .any(|e| matches!(e.kind, UncertainEdgeKind::EventHandler)));
308 assert!(edges
309 .iter()
310 .any(|e| matches!(e.kind, UncertainEdgeKind::WidgetTree)));
311 assert!(!edges.iter().any(|e| e.to == regular.id));
313 }
314
315 #[test]
316 fn test_detect_analysis_limitations_callbacks() {
317 let nodes: Vec<CodeNode> = (0..7)
319 .map(|i| {
320 CodeNode::new(
321 &format!("on_event_{}", i),
322 &format!("on_event_{}", i),
323 NodeKind::Function,
324 "events.ts",
325 )
326 })
327 .collect();
328 let node_refs: Vec<&CodeNode> = nodes.iter().collect();
329
330 let warnings = detect_analysis_limitations(&node_refs);
331 assert!(!warnings.is_empty());
332 assert!(warnings.iter().any(|w| w.message.contains("callback")));
333 }
334
335 #[test]
336 fn test_detect_analysis_limitations_flutter_widgets() {
337 let widgets: Vec<CodeNode> = vec![CodeNode::new(
338 "HomeWidget",
339 "HomeWidget",
340 NodeKind::Class,
341 "home.dart",
342 )];
343 let node_refs: Vec<&CodeNode> = widgets.iter().collect();
344
345 let warnings = detect_analysis_limitations(&node_refs);
346 assert!(warnings.iter().any(|w| w.message.contains("Flutter")));
347 }
348
349 #[test]
350 fn test_uncertain_edge_kind_display() {
351 assert_eq!(UncertainEdgeKind::Callback.to_string(), "callback");
352 assert_eq!(
353 UncertainEdgeKind::DynamicDispatch.to_string(),
354 "dynamic dispatch"
355 );
356 assert_eq!(UncertainEdgeKind::WidgetTree.to_string(), "widget tree");
357 assert_eq!(UncertainEdgeKind::EventHandler.to_string(), "event handler");
358 assert_eq!(
359 UncertainEdgeKind::DependencyInjection.to_string(),
360 "dependency injection"
361 );
362 assert_eq!(UncertainEdgeKind::Reflection.to_string(), "reflection");
363 }
364
365 #[test]
366 fn test_no_warnings_for_clean_code() {
367 let nodes: Vec<CodeNode> = vec![
368 CodeNode::new("main", "main", NodeKind::Function, "main.rs"),
369 CodeNode::new("helper", "helper", NodeKind::Function, "utils.rs"),
370 ];
371 let node_refs: Vec<&CodeNode> = nodes.iter().collect();
372
373 let warnings = detect_analysis_limitations(&node_refs);
374 assert!(warnings.is_empty());
375 }
376}