use arbor_core::{CodeNode, NodeKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UncertainEdgeKind {
Callback,
DynamicDispatch,
WidgetTree,
EventHandler,
DependencyInjection,
Reflection,
}
impl std::fmt::Display for UncertainEdgeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UncertainEdgeKind::Callback => write!(f, "callback"),
UncertainEdgeKind::DynamicDispatch => write!(f, "dynamic dispatch"),
UncertainEdgeKind::WidgetTree => write!(f, "widget tree"),
UncertainEdgeKind::EventHandler => write!(f, "event handler"),
UncertainEdgeKind::DependencyInjection => write!(f, "dependency injection"),
UncertainEdgeKind::Reflection => write!(f, "reflection"),
}
}
}
#[derive(Debug, Clone)]
pub struct UncertainEdge {
pub from: String,
pub to: String,
pub kind: UncertainEdgeKind,
pub confidence: f32, pub reason: String,
}
pub struct HeuristicsMatcher;
impl HeuristicsMatcher {
pub fn is_flutter_widget(node: &CodeNode) -> bool {
node.kind == NodeKind::Class
&& (node.name.ends_with("Widget")
|| node.name.ends_with("State")
|| node.name.ends_with("Page")
|| node.name.ends_with("Screen")
|| node.name.ends_with("View"))
}
pub fn is_react_component(node: &CodeNode) -> bool {
(node.kind == NodeKind::Function || node.kind == NodeKind::Class)
&& node.file.ends_with(".tsx")
&& node.name.chars().next().is_some_and(|c| c.is_uppercase())
}
pub fn is_event_handler(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
(node.kind == NodeKind::Function || node.kind == NodeKind::Method)
&& (name_lower.starts_with("on")
|| name_lower.starts_with("handle")
|| name_lower.ends_with("handler")
|| name_lower.ends_with("callback")
|| name_lower.ends_with("listener"))
}
pub fn is_callback_style(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
name_lower.ends_with("fn")
|| name_lower.ends_with("callback")
|| name_lower.ends_with("handler")
|| name_lower.starts_with("on_")
}
pub fn is_dependency_injection(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
name_lower.ends_with("factory")
|| name_lower.ends_with("provider")
|| name_lower.ends_with("injector")
|| name_lower.ends_with("container")
|| name_lower.contains("singleton")
}
pub fn infer_uncertain_edges(nodes: &[&CodeNode]) -> Vec<UncertainEdge> {
let mut edges = Vec::new();
for node in nodes {
if Self::is_event_handler(node) {
edges.push(UncertainEdge {
from: "event_source".to_string(),
to: node.id.clone(),
kind: UncertainEdgeKind::EventHandler,
confidence: 0.7,
reason: format!("'{}' looks like an event handler", node.name),
});
}
if Self::is_callback_style(node) {
edges.push(UncertainEdge {
from: "caller".to_string(),
to: node.id.clone(),
kind: UncertainEdgeKind::Callback,
confidence: 0.6,
reason: format!("'{}' is likely passed as a callback", node.name),
});
}
if Self::is_flutter_widget(node) {
edges.push(UncertainEdge {
from: "parent_widget".to_string(),
to: node.id.clone(),
kind: UncertainEdgeKind::WidgetTree,
confidence: 0.8,
reason: format!("'{}' is a Flutter widget in the widget tree", node.name),
});
}
}
edges
}
}
#[derive(Debug, Clone)]
pub struct AnalysisWarning {
pub message: String,
pub suggestion: String,
}
impl AnalysisWarning {
pub fn new(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
Self {
message: message.into(),
suggestion: suggestion.into(),
}
}
}
pub fn detect_analysis_limitations(nodes: &[&CodeNode]) -> Vec<AnalysisWarning> {
let mut warnings = Vec::new();
let callback_count = nodes
.iter()
.filter(|n| HeuristicsMatcher::is_callback_style(n))
.count();
if callback_count > 5 {
warnings.push(AnalysisWarning::new(
format!("Found {} callback-style nodes", callback_count),
"Callbacks may be invoked dynamically. Verify runtime behavior.",
));
}
let event_handler_count = nodes
.iter()
.filter(|n| HeuristicsMatcher::is_event_handler(n))
.count();
if event_handler_count > 3 {
warnings.push(AnalysisWarning::new(
format!("Found {} event handlers", event_handler_count),
"Event handlers are connected at runtime. Check event sources.",
));
}
let widget_count = nodes
.iter()
.filter(|n| HeuristicsMatcher::is_flutter_widget(n))
.count();
if widget_count > 0 {
warnings.push(AnalysisWarning::new(
format!("Detected {} Flutter widgets", widget_count),
"Widget tree hierarchy is determined at runtime.",
));
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flutter_widget_detection() {
let widget = CodeNode::new("HomeWidget", "HomeWidget", NodeKind::Class, "home.dart");
assert!(HeuristicsMatcher::is_flutter_widget(&widget));
let state = CodeNode::new("HomeState", "HomeState", NodeKind::Class, "home.dart");
assert!(HeuristicsMatcher::is_flutter_widget(&state));
let non_widget = CodeNode::new(
"UserService",
"UserService",
NodeKind::Class,
"service.dart",
);
assert!(!HeuristicsMatcher::is_flutter_widget(&non_widget));
}
#[test]
fn test_event_handler_detection() {
let handler = CodeNode::new("onClick", "onClick", NodeKind::Function, "button.ts");
assert!(HeuristicsMatcher::is_event_handler(&handler));
let handler2 = CodeNode::new(
"handleSubmit",
"handleSubmit",
NodeKind::Function,
"form.ts",
);
assert!(HeuristicsMatcher::is_event_handler(&handler2));
let non_handler = CodeNode::new("calculate", "calculate", NodeKind::Function, "math.ts");
assert!(!HeuristicsMatcher::is_event_handler(&non_handler));
}
#[test]
fn test_react_component_detection() {
let component = CodeNode::new(
"UserProfile",
"UserProfile",
NodeKind::Function,
"profile.tsx",
);
assert!(HeuristicsMatcher::is_react_component(&component));
let non_component = CodeNode::new("helper", "helper", NodeKind::Function, "utils.tsx");
assert!(!HeuristicsMatcher::is_react_component(&non_component));
let wrong_ext = CodeNode::new(
"UserProfile",
"UserProfile",
NodeKind::Function,
"profile.rs",
);
assert!(!HeuristicsMatcher::is_react_component(&wrong_ext));
let class_comp = CodeNode::new("AppContainer", "AppContainer", NodeKind::Class, "app.tsx");
assert!(HeuristicsMatcher::is_react_component(&class_comp));
}
#[test]
fn test_callback_style_detection() {
let callback = CodeNode::new(
"on_click_handler",
"on_click_handler",
NodeKind::Function,
"a.rs",
);
assert!(HeuristicsMatcher::is_callback_style(&callback));
let callback_fn = CodeNode::new("sortFn", "sortFn", NodeKind::Function, "a.ts");
assert!(HeuristicsMatcher::is_callback_style(&callback_fn));
let regular = CodeNode::new("process_data", "process_data", NodeKind::Function, "a.rs");
assert!(!HeuristicsMatcher::is_callback_style(®ular));
}
#[test]
fn test_dependency_injection_detection() {
let factory = CodeNode::new("UserFactory", "UserFactory", NodeKind::Class, "factory.ts");
assert!(HeuristicsMatcher::is_dependency_injection(&factory));
let provider = CodeNode::new("AuthProvider", "AuthProvider", NodeKind::Class, "auth.ts");
assert!(HeuristicsMatcher::is_dependency_injection(&provider));
let regular = CodeNode::new("UserService", "UserService", NodeKind::Class, "service.ts");
assert!(!HeuristicsMatcher::is_dependency_injection(®ular));
}
#[test]
fn test_infer_uncertain_edges_from_patterns() {
let handler = CodeNode::new("onClick", "onClick", NodeKind::Function, "button.ts");
let widget = CodeNode::new("HomeWidget", "HomeWidget", NodeKind::Class, "home.dart");
let regular = CodeNode::new("calculate", "calculate", NodeKind::Function, "math.ts");
let nodes: Vec<&CodeNode> = vec![&handler, &widget, ®ular];
let edges = HeuristicsMatcher::infer_uncertain_edges(&nodes);
assert!(edges
.iter()
.any(|e| matches!(e.kind, UncertainEdgeKind::EventHandler)));
assert!(edges
.iter()
.any(|e| matches!(e.kind, UncertainEdgeKind::WidgetTree)));
assert!(!edges.iter().any(|e| e.to == regular.id));
}
#[test]
fn test_detect_analysis_limitations_callbacks() {
let nodes: Vec<CodeNode> = (0..7)
.map(|i| {
CodeNode::new(
&format!("on_event_{}", i),
&format!("on_event_{}", i),
NodeKind::Function,
"events.ts",
)
})
.collect();
let node_refs: Vec<&CodeNode> = nodes.iter().collect();
let warnings = detect_analysis_limitations(&node_refs);
assert!(!warnings.is_empty());
assert!(warnings.iter().any(|w| w.message.contains("callback")));
}
#[test]
fn test_detect_analysis_limitations_flutter_widgets() {
let widgets: Vec<CodeNode> = vec![CodeNode::new(
"HomeWidget",
"HomeWidget",
NodeKind::Class,
"home.dart",
)];
let node_refs: Vec<&CodeNode> = widgets.iter().collect();
let warnings = detect_analysis_limitations(&node_refs);
assert!(warnings.iter().any(|w| w.message.contains("Flutter")));
}
#[test]
fn test_uncertain_edge_kind_display() {
assert_eq!(UncertainEdgeKind::Callback.to_string(), "callback");
assert_eq!(
UncertainEdgeKind::DynamicDispatch.to_string(),
"dynamic dispatch"
);
assert_eq!(UncertainEdgeKind::WidgetTree.to_string(), "widget tree");
assert_eq!(UncertainEdgeKind::EventHandler.to_string(), "event handler");
assert_eq!(
UncertainEdgeKind::DependencyInjection.to_string(),
"dependency injection"
);
assert_eq!(UncertainEdgeKind::Reflection.to_string(), "reflection");
}
#[test]
fn test_no_warnings_for_clean_code() {
let nodes: Vec<CodeNode> = vec![
CodeNode::new("main", "main", NodeKind::Function, "main.rs"),
CodeNode::new("helper", "helper", NodeKind::Function, "utils.rs"),
];
let node_refs: Vec<&CodeNode> = nodes.iter().collect();
let warnings = detect_analysis_limitations(&node_refs);
assert!(warnings.is_empty());
}
}