use crate::core::CodeNode;
use regex::Regex;
use std::sync::OnceLock;
#[cfg(test)]
use crate::core::NodeKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BehaviorMarker {
CallbackHandler,
Middleware,
LifecycleMethod,
PluginRegistration,
LazyLoading,
PublicExport,
DynamicDispatch,
EventHandler,
FactoryMethod,
ConfigDriven,
}
pub struct BddContextDetector;
impl BddContextDetector {
pub fn detect_markers(node: &CodeNode) -> Vec<BehaviorMarker> {
let mut markers = Vec::new();
if Self::is_callback_handler(node) {
markers.push(BehaviorMarker::CallbackHandler);
}
if Self::is_middleware(node) {
markers.push(BehaviorMarker::Middleware);
}
if Self::is_lifecycle_method(node) {
markers.push(BehaviorMarker::LifecycleMethod);
}
if Self::is_event_handler(node) {
markers.push(BehaviorMarker::EventHandler);
}
if Self::is_public_export(node) {
markers.push(BehaviorMarker::PublicExport);
}
if Self::is_plugin_registration(node) {
markers.push(BehaviorMarker::PluginRegistration);
}
if Self::is_factory_method(node) {
markers.push(BehaviorMarker::FactoryMethod);
}
if Self::is_config_driven(node) {
markers.push(BehaviorMarker::ConfigDriven);
}
markers
}
fn is_callback_handler(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
callback_patterns().is_match(&name_lower)
|| node
.attributes
.iter()
.any(|attr| callback_attr_patterns().is_match(attr))
}
fn is_middleware(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
middleware_patterns().is_match(&name_lower)
|| node
.attributes
.iter()
.any(|attr| middleware_attr_patterns().is_match(attr))
}
fn is_lifecycle_method(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
lifecycle_patterns().is_match(&name_lower)
|| node
.attributes
.iter()
.any(|attr| lifecycle_attr_patterns().is_match(attr))
}
fn is_event_handler(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
(node.name.starts_with("on")
&& node.name.len() > 2
&& node.name.chars().nth(2).is_some_and(|c| c.is_uppercase()))
|| matches!(
node.name.as_str(),
"onopen" | "onmessage" | "onerror" | "onclose" | "onabort"
| "onconnect" | "ondisconnect" | "ontimeout" | "ondata"
| "onprogress" | "onload" | "onready" | "oncomplete"
)
|| name_lower.starts_with("handle")
|| name_lower.ends_with("listener")
|| Self::is_swift_delegate_method(node)
|| node
.attributes
.iter()
.any(|attr| event_attr_patterns().is_match(attr))
}
fn is_swift_delegate_method(node: &CodeNode) -> bool {
if node.language != crate::core::Language::Swift {
return false;
}
let name = &node.name;
name.contains("Did")
|| name.contains("Will")
|| name.contains("Should")
|| name.starts_with("locationManager")
|| name.starts_with("webView")
|| name.starts_with("tableView")
|| name.starts_with("collectionView")
|| name.starts_with("photoOutput")
|| name.starts_with("audioPlayer")
|| name.starts_with("urlSession")
|| name.starts_with("mapView")
}
fn is_public_export(node: &CodeNode) -> bool {
node.name.starts_with("export")
|| node.attributes.iter().any(|attr| {
attr.contains("export")
|| attr.contains("public")
|| attr.contains("@api")
|| attr.contains("@public")
})
}
fn is_plugin_registration(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
plugin_patterns().is_match(&name_lower)
|| node.attributes.iter().any(|attr| {
plugin_attr_patterns().is_match(attr)
|| attr.contains("register")
|| attr.contains("plugin")
})
}
fn is_config_driven(node: &CodeNode) -> bool {
matches!(
node.name.as_str(),
"migrate"
| "serialize"
| "deserialize"
| "transform"
| "validate"
| "sanitize"
| "comparator"
| "reducer"
| "partialize"
| "onRehydrateStorage"
| "onFinishHydration"
| "getStorage"
| "setStorage"
)
}
fn is_factory_method(node: &CodeNode) -> bool {
let name_lower = node.name.to_lowercase();
factory_patterns().is_match(&name_lower)
}
}
fn callback_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(
r"(?ix)
(callback|handler|onload|onsuccess|onerror|onchange|onclick|
onsubmit|onblur|onfocus|onmouseenter|onmouseleave|
then|catch|finally|resolve|reject)
",
)
.unwrap()
})
}
fn callback_attr_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| Regex::new(r"(?i)(callback|handler|listener|async|promise)").unwrap())
}
fn middleware_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(
r"(?ix)
(middleware|interceptor|filter|validator|authenticator|
authorization|permission|check|guard|protect)
",
)
.unwrap()
})
}
fn middleware_attr_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(
r"(?i)
(@middleware|@interceptor|@filter|@guard|@route|@post|@get|@put|@delete|
@patch|@use)
",
)
.unwrap()
})
}
fn lifecycle_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(
r"(?ix)
(setup|teardown|setdown|cleanup|initialize|init|mount|unmount|
install|uninstall|enable|disable|start|stop|configure|
beforeeach|aftereach|beforeall|afterall|before|after)
",
)
.unwrap()
})
}
fn lifecycle_attr_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(
r"(?i)
(@setup|@teardown|@beforeeach|@aftereach|@beforeall|@afterall|
@lifecycle|@hook)
",
)
.unwrap()
})
}
fn event_attr_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| Regex::new(r"(?i)(@event|@listener|@subscribe|@on|@emit)").unwrap())
}
fn plugin_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(
r"(?ix)
(plugin|extension|addon|provider|factory|builder|creator|
register|install|use|apply)
",
)
.unwrap()
})
}
fn plugin_attr_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(r"(?i)(@plugin|@provider|@injectable|@factory|@register)").unwrap()
})
}
fn factory_patterns() -> &'static Regex {
static INSTANCE: OnceLock<Regex> = OnceLock::new();
INSTANCE.get_or_init(|| {
Regex::new(
r"(?ix)
(create|make|build|factory|builder|constructor|new|instantiate|
produce|generate|create_.*|make_.*)
",
)
.unwrap()
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_node(name: &str, attrs: Vec<&str>) -> CodeNode {
CodeNode {
id: crate::core::NodeId::from_u32(1),
name: name.to_string(),
full_name: format!("test.{}", name),
kind: NodeKind::Function,
location: crate::core::SourceLocation {
file: "test.rs".to_string(),
line_start: 1,
line_end: 10,
column_start: 0,
column_end: 0,
},
language: crate::core::Language::Rust,
visibility: crate::core::Visibility::Public,
lines_of_code: 5,
parent_id: None,
is_async: false,
is_test: false,
is_generated: false,
attributes: attrs.iter().map(|s| s.to_string()).collect(),
documentation: None,
}
}
#[test]
fn test_callback_handler_detection() {
let node = make_node("onSuccess", vec![]);
assert!(BddContextDetector::is_callback_handler(&node));
let node = make_node("onClick", vec![]);
assert!(BddContextDetector::is_event_handler(&node));
let node = make_node("thenHandler", vec![]);
assert!(BddContextDetector::is_callback_handler(&node));
}
#[test]
fn test_middleware_detection() {
let node = make_node("authMiddleware", vec![]);
assert!(BddContextDetector::is_middleware(&node));
let node = make_node("validator", vec!["@middleware"]);
assert!(BddContextDetector::is_middleware(&node));
}
#[test]
fn test_lifecycle_detection() {
let node = make_node("setUp", vec![]);
assert!(BddContextDetector::is_lifecycle_method(&node));
let node = make_node("beforeEach", vec![]);
assert!(BddContextDetector::is_lifecycle_method(&node));
let node = make_node("tearDown", vec![]);
assert!(BddContextDetector::is_lifecycle_method(&node));
}
#[test]
fn test_factory_detection() {
let node = make_node("createUser", vec![]);
assert!(BddContextDetector::is_factory_method(&node));
let node = make_node("buildConfig", vec![]);
assert!(BddContextDetector::is_factory_method(&node));
}
#[test]
fn test_public_export_detection() {
let node = make_node("exportData", vec![]);
assert!(BddContextDetector::is_public_export(&node));
let node = make_node("normalFunction", vec!["@api"]);
assert!(BddContextDetector::is_public_export(&node));
}
#[test]
fn test_lowercase_event_handler_detection() {
for name in &[
"onopen",
"onmessage",
"onerror",
"onclose",
"onload",
"onprogress",
] {
let node = make_node(name, vec![]);
assert!(
BddContextDetector::is_event_handler(&node),
"'{}' should be detected as event handler",
name
);
}
}
#[test]
fn test_config_driven_detection() {
for name in &[
"migrate",
"serialize",
"deserialize",
"partialize",
"onRehydrateStorage",
"reducer",
] {
let node = make_node(name, vec![]);
assert!(
BddContextDetector::is_config_driven(&node),
"'{}' should be detected as config-driven",
name
);
}
}
#[test]
fn test_config_driven_in_detect_markers() {
let node = make_node("migrate", vec![]);
let markers = BddContextDetector::detect_markers(&node);
assert!(
markers.contains(&BehaviorMarker::ConfigDriven),
"migrate should have ConfigDriven marker. Markers: {:?}",
markers
);
}
fn make_swift_node(name: &str, attrs: Vec<&str>) -> CodeNode {
CodeNode {
id: crate::core::NodeId::from_u32(1),
name: name.to_string(),
full_name: format!("test.{}", name),
kind: NodeKind::Function,
location: crate::core::SourceLocation {
file: "test.swift".to_string(),
line_start: 1,
line_end: 10,
column_start: 0,
column_end: 0,
},
language: crate::core::Language::Swift,
visibility: crate::core::Visibility::Public,
lines_of_code: 5,
parent_id: None,
is_async: false,
is_test: false,
is_generated: false,
attributes: attrs.iter().map(|s| s.to_string()).collect(),
documentation: None,
}
}
#[test]
fn test_swift_delegate_method_detection() {
for name in &[
"applicationDidFinishLaunching",
"applicationWillTerminate",
"applicationShouldTerminate",
"locationManagerDidChangeAuthorization",
"windowDidLoad",
"scrollViewDidScroll",
] {
let node = make_swift_node(name, vec![]);
assert!(
BddContextDetector::is_event_handler(&node),
"Swift delegate method '{}' should be detected as event handler",
name
);
}
}
#[test]
fn test_swift_framework_delegate_prefixes() {
for name in &[
"locationManagerDidUpdateLocations",
"webViewDidFinishNavigation",
"tableViewDidSelectRow",
"collectionViewDidSelectItem",
"urlSessionDidBecomeInvalid",
"mapViewDidChangeVisibleRegion",
] {
let node = make_swift_node(name, vec![]);
assert!(
BddContextDetector::is_event_handler(&node),
"Swift delegate '{}' should be detected",
name
);
}
}
#[test]
fn test_non_swift_did_not_delegate() {
let node = make_node("applicationDidFinishLaunching", vec![]);
assert!(
!BddContextDetector::is_swift_delegate_method(&node),
"Rust node should NOT be detected as Swift delegate"
);
}
#[test]
fn test_multiple_markers() {
let node = make_node("handleUserClick", vec!["@event"]);
let markers = BddContextDetector::detect_markers(&node);
assert!(!markers.is_empty());
}
}