use crate::db::models::Relationship;
use once_cell::sync::Lazy;
use regex::Regex;
static EMIT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"(?m)([A-Za-z_][A-Za-z0-9_$]*)\s*\.\s*(emit|fire|publish|trigger|dispatch)\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_.\-:]+)['"]"#,
)
.unwrap()
});
static LISTEN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"(?m)([A-Za-z_][A-Za-z0-9_$]*)\s*\.\s*(on|once|addListener|listen|subscribe|addEventListener)\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_.\-:]+)['"]"#,
)
.unwrap()
});
static DOM_DISPATCH_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?m)([A-Za-z_][A-Za-z0-9_$]*)\s*\.\s*dispatchEvent\s*\(\s*new\s+[A-Za-z_]+\(\s*['"]([A-Za-z_][A-Za-z0-9_.\-:]+)['"]"#).unwrap()
});
#[derive(Debug, Clone)]
pub struct EventEdge {
pub rel_type: &'static str,
pub receiver: String,
pub event_name: String,
pub line: u32,
}
pub fn detect_event_edges(source: &str) -> Vec<EventEdge> {
let mut out: Vec<EventEdge> = Vec::new();
for cap in EMIT_RE.captures_iter(source) {
let line = source[..cap.get(0).unwrap().start()].matches('\n').count() as u32 + 1;
out.push(EventEdge {
rel_type: "emits",
receiver: cap[1].to_string(),
event_name: cap[3].to_string(),
line,
});
}
for cap in DOM_DISPATCH_RE.captures_iter(source) {
let line = source[..cap.get(0).unwrap().start()].matches('\n').count() as u32 + 1;
out.push(EventEdge {
rel_type: "emits",
receiver: cap[1].to_string(),
event_name: cap[2].to_string(),
line,
});
}
for cap in LISTEN_RE.captures_iter(source) {
let line = source[..cap.get(0).unwrap().start()].matches('\n').count() as u32 + 1;
out.push(EventEdge {
rel_type: "listens_on",
receiver: cap[1].to_string(),
event_name: cap[3].to_string(),
line,
});
}
out
}
pub fn to_relationships(edges: &[EventEdge]) -> Vec<Relationship> {
edges
.iter()
.map(|e| {
let event_qn = format!("event::{}", e.event_name);
Relationship {
id: None,
source_qualified: e.receiver.clone(),
target_qualified: event_qn,
rel_type: e.rel_type.to_string(),
confidence: 0.85,
metadata: serde_json::json!({
"resolution_method": "name",
"event_name": e.event_name,
}),
..Default::default()
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_node_event_emitter() {
let src = r#"
emitter.emit("user:created", payload);
bus.on("user:created", handler);
subscriber.once("ping", cb);
"#;
let edges = detect_event_edges(src);
assert!(edges.iter().any(|e| e.rel_type == "emits"
&& e.receiver == "emitter"
&& e.event_name == "user:created"));
assert!(edges.iter().any(|e| e.rel_type == "listens_on"
&& e.receiver == "bus"
&& e.event_name == "user:created"));
assert!(edges.iter().any(|e| e.rel_type == "listens_on"
&& e.receiver == "subscriber"
&& e.event_name == "ping"));
}
#[test]
fn detects_dom_dispatch_event() {
let src = r#"
target.dispatchEvent(new CustomEvent("save"));
"#;
let edges = detect_event_edges(src);
assert!(edges
.iter()
.any(|e| e.rel_type == "emits" && e.event_name == "save"));
}
#[test]
fn empty_source_returns_no_edges() {
let edges = detect_event_edges("");
assert!(edges.is_empty());
}
}