use crate::cross_file::diagnostics::{
CrossFileDiagnostic, CrossFileDiagnosticKind, DiagnosticSeverity,
};
use crate::cross_file::graph::{DependencyEdge, DependencyGraph};
use crate::cross_file::registry::{FileId, ModuleRegistry};
use vize_carton::{cstr, CompactString, FxHashMap, FxHashSet, ToCompactString};
#[derive(Debug, Clone)]
pub struct EmitFlow {
pub emitter: FileId,
pub listener: FileId,
pub event_name: CompactString,
pub is_declared: bool,
pub is_called: bool,
pub is_handled: bool,
pub emit_offset: Option<u32>,
pub handler_offset: Option<u32>,
}
pub fn analyze_emits(
registry: &ModuleRegistry,
graph: &DependencyGraph,
) -> (Vec<EmitFlow>, Vec<CrossFileDiagnostic>) {
let mut flows = Vec::new();
let mut diagnostics = Vec::new();
let mut component_emits: FxHashMap<FileId, ComponentEmitInfo> = FxHashMap::default();
for entry in registry.vue_components() {
let info = extract_emit_info(&entry.analysis);
component_emits.insert(entry.id, info);
}
for node in graph.nodes() {
let Some(parent_entry) = registry.get(node.file_id) else {
continue;
};
let parent_listeners = extract_event_listeners(&parent_entry.analysis);
for (child_id, edge_type) in &node.imports {
if *edge_type != DependencyEdge::ComponentUsage {
continue;
}
let Some(child_info) = component_emits.get(child_id) else {
continue;
};
let child_name = registry
.get(*child_id)
.and_then(|e| e.component_name.clone());
for emit in &child_info.declared_emits {
let is_called = child_info.called_emits.contains(emit);
let listener_info = child_name.as_ref().and_then(|name| {
parent_listeners
.get(name.as_str())
.and_then(|events| events.get(emit.as_str()))
});
let is_handled = listener_info.is_some();
flows.push(EmitFlow {
emitter: *child_id,
listener: node.file_id,
event_name: emit.clone(),
is_declared: true,
is_called,
is_handled,
emit_offset: child_info.emit_offsets.get(emit).copied(),
handler_offset: listener_info.copied(),
});
if !is_called {
diagnostics.push(
CrossFileDiagnostic::new(
CrossFileDiagnosticKind::UnusedEmit {
emit_name: emit.clone(),
},
DiagnosticSeverity::Warning,
*child_id,
0,
cstr!(
"Event '{}' is declared in defineEmits but never emitted",
emit
),
)
.with_suggestion(
"Remove from defineEmits if not needed, or emit the event",
),
);
}
}
for emit in &child_info.called_emits {
if !child_info.declared_emits.contains(emit) {
diagnostics.push(
CrossFileDiagnostic::new(
CrossFileDiagnosticKind::UndeclaredEmit {
emit_name: emit.clone(),
},
DiagnosticSeverity::Error,
*child_id,
child_info.emit_offsets.get(emit).copied().unwrap_or(0),
cstr!("Event '{emit}' is emitted but not declared in defineEmits",),
)
.with_suggestion(cstr!("Add '{emit}' to defineEmits")),
);
}
}
if let Some(child_name) = child_name {
if let Some(listeners) = parent_listeners.get(child_name.as_str()) {
for (event, offset) in listeners {
if !child_info.declared_emits.contains(event.as_str())
&& !child_info.called_emits.contains(event.as_str())
&& !is_native_event(event)
{
diagnostics.push(
CrossFileDiagnostic::new(
CrossFileDiagnosticKind::UnmatchedEventListener {
event_name: CompactString::new(event.as_str()),
},
DiagnosticSeverity::Warning,
node.file_id,
*offset,
cstr!(
"Listening for '{event}' but child component doesn't emit it",
),
)
.with_related(
*child_id,
0,
cstr!("'{child_name}' component"),
),
);
}
}
}
}
}
}
(flows, diagnostics)
}
#[derive(Debug, Default)]
struct ComponentEmitInfo {
declared_emits: FxHashSet<CompactString>,
called_emits: FxHashSet<CompactString>,
emit_offsets: FxHashMap<CompactString, u32>,
}
#[inline]
fn extract_emit_info(analysis: &crate::Croquis) -> ComponentEmitInfo {
let mut info = ComponentEmitInfo::default();
for emit in analysis.macros.emits() {
info.declared_emits.insert(emit.name.clone());
}
for emit_call in analysis.macros.emit_calls() {
if !emit_call.is_dynamic {
info.called_emits.insert(emit_call.event_name.clone());
info.emit_offsets
.insert(emit_call.event_name.clone(), emit_call.start);
}
}
info
}
fn extract_event_listeners(
analysis: &crate::Croquis,
) -> FxHashMap<CompactString, FxHashMap<CompactString, u32>> {
let mut result: FxHashMap<CompactString, FxHashMap<CompactString, u32>> = FxHashMap::default();
for usage in &analysis.component_usages {
let component_name = usage.name.to_compact_string();
let events = result.entry(component_name).or_default();
for event in &usage.events {
events.insert(event.name.to_compact_string(), event.start);
}
}
result
}
fn is_native_event(event: &str) -> bool {
matches!(
event,
"click"
| "dblclick"
| "mousedown"
| "mouseup"
| "mousemove"
| "mouseenter"
| "mouseleave"
| "mouseover"
| "mouseout"
| "keydown"
| "keyup"
| "keypress"
| "focus"
| "blur"
| "change"
| "input"
| "submit"
| "scroll"
| "resize"
| "load"
| "error"
| "contextmenu"
| "wheel"
| "touchstart"
| "touchmove"
| "touchend"
| "touchcancel"
| "pointerdown"
| "pointermove"
| "pointerup"
| "pointercancel"
| "pointerenter"
| "pointerleave"
| "drag"
| "dragstart"
| "dragend"
| "dragenter"
| "dragleave"
| "dragover"
| "drop"
| "copy"
| "cut"
| "paste"
)
}
#[cfg(test)]
mod tests {
use super::is_native_event;
#[test]
fn test_native_event_detection() {
assert!(is_native_event("click"));
assert!(is_native_event("keydown"));
assert!(is_native_event("submit"));
assert!(!is_native_event("update"));
assert!(!is_native_event("custom-event"));
}
}