|
| 1 | +//! `EventTopic` Node emission + `Publishes` / `Subscribes` edge construction |
| 2 | +//! (T10-4 partial / T5-33 subset). |
| 3 | +//! |
| 4 | +//! Bridges the gap between per-file `RawEventTopic` (T5-2..T5-31 detectors) |
| 5 | +//! and the queryable graph layer: each unique `(enclosing_fn, lib)` pair from |
| 6 | +//! a publish call site emits one `NodeKind::EventTopic` Node, and the enclosing |
| 7 | +//! function node receives a `RelType::Publishes` outgoing edge to it. |
| 8 | +//! Consumer call sites emit `RelType::Subscribes` edges analogously. |
| 9 | +//! |
| 10 | +//! ## Algorithm |
| 11 | +//! |
| 12 | +//! 1. **Dedup topics** — group `RawEventTopic` entries by `(enclosing_fn, lib)` |
| 13 | +//! to avoid duplicate `EventTopic` nodes when the same function calls the |
| 14 | +//! same produce API twice. `topic_literal` is used as the EventTopic name |
| 15 | +//! when available (dynamic topics with `None` are skipped — no fabricated |
| 16 | +//! nodes). |
| 17 | +//! |
| 18 | +//! 2. **Emit edges** — for each unique publish/subscribe site, resolve |
| 19 | +//! `enclosing_fn` via `SymbolTable` to find the owning Function/Method |
| 20 | +//! node in the same file. On miss, silently drop (no dangling edge). |
| 21 | +//! |
| 22 | +//! ## Scope note |
| 23 | +//! |
| 24 | +//! This module intentionally does NOT emit `EventTopicMirror` heuristic |
| 25 | +//! edges — that is T5-33's scope (requires at least one Publish + one |
| 26 | +//! Subscribe detector per lib to be gated correctly per D7). The mirror |
| 27 | +//! edges will be added by T5-33 without touching this file. |
| 28 | +
|
| 29 | +use crate::resolution::index::SymbolTable; |
| 30 | +use ecp_core::analyzer::types::{LocalGraph, PubSub}; |
| 31 | +use ecp_core::graph::{Edge, Node, NodeKind, RelType}; |
| 32 | +use ecp_core::pool::{StrRef, StringPool}; |
| 33 | +use ecp_core::uid; |
| 34 | +use rustc_hash::FxHashMap; |
| 35 | + |
| 36 | +/// Promote `RawEventTopic`s to `EventTopic` Nodes + `Publishes` / `Subscribes` |
| 37 | +/// edges. Returns the count of emitted `Publishes` + `Subscribes` edges. |
| 38 | +/// |
| 39 | +/// Must run BEFORE the File-node append loop in `builder::build()` so that |
| 40 | +/// `EventTopic` node indices stay in the raw-symbols-and-extras region |
| 41 | +/// (consistent with how `SchemaField` nodes are placed by `schema_field_mirrors`). |
| 42 | +pub fn emit_edges( |
| 43 | + local_graphs: &[LocalGraph], |
| 44 | + symbol_table: &SymbolTable, |
| 45 | + string_pool: &mut StringPool, |
| 46 | + nodes: &mut Vec<Node>, |
| 47 | + edges: &mut Vec<Edge>, |
| 48 | +) -> usize { |
| 49 | + let reason_pub = string_pool.add("post_process:event_topic:publishes"); |
| 50 | + let reason_sub = string_pool.add("post_process:event_topic:subscribes"); |
| 51 | + |
| 52 | + // Dedup map: normalised topic name → EventTopic node idx. |
| 53 | + let mut topic_node_idx: FxHashMap<String, u32> = FxHashMap::default(); |
| 54 | + |
| 55 | + let mut emitted = 0usize; |
| 56 | + |
| 57 | + for (lg_idx, local_graph) in local_graphs.iter().enumerate() { |
| 58 | + let Some(ref event_topics) = local_graph.event_topics else { |
| 59 | + continue; |
| 60 | + }; |
| 61 | + if event_topics.is_empty() { |
| 62 | + continue; |
| 63 | + } |
| 64 | + |
| 65 | + let path_str = local_graph.file_path.to_string_lossy().replace('\\', "/"); |
| 66 | + let file_idx = lg_idx as u32; |
| 67 | + |
| 68 | + for raw_et in event_topics.iter() { |
| 69 | + // Skip dynamic topics without a literal. |
| 70 | + let Some(ref topic_lit) = raw_et.topic_literal else { |
| 71 | + continue; |
| 72 | + }; |
| 73 | + |
| 74 | + // Skip empty enclosing function (shouldn't happen in practice). |
| 75 | + if raw_et.enclosing_fn.is_empty() { |
| 76 | + continue; |
| 77 | + } |
| 78 | + |
| 79 | + // Resolve enclosing function in this file's symbol table. |
| 80 | + let Some(fn_node_idx) = symbol_table.lookup_in_file(&path_str, &raw_et.enclosing_fn) |
| 81 | + else { |
| 82 | + continue; |
| 83 | + }; |
| 84 | + |
| 85 | + // Use the topic literal as the EventTopic name. |
| 86 | + let topic_name: &str = topic_lit; |
| 87 | + |
| 88 | + // Emit or reuse EventTopic node, keyed by (topic_name, lib) to |
| 89 | + // keep a single canonical node per topic-per-framework. |
| 90 | + let et_key = format!("{}:{}", topic_name, raw_et.lib.as_str()); |
| 91 | + let et_node_idx = *topic_node_idx.entry(et_key).or_insert_with(|| { |
| 92 | + let topic_name_ref: StrRef = string_pool.add(topic_name); |
| 93 | + let node_uid = uid::compute(NodeKind::EventTopic, &path_str, None, topic_name); |
| 94 | + let idx = nodes.len() as u32; |
| 95 | + nodes.push(Node { |
| 96 | + uid: node_uid, |
| 97 | + name: topic_name_ref, |
| 98 | + file_idx, |
| 99 | + kind: NodeKind::EventTopic, |
| 100 | + span: raw_et.span, |
| 101 | + community_id: 0, |
| 102 | + owner_class: StrRef::default(), |
| 103 | + content_hash: 0, |
| 104 | + }); |
| 105 | + idx |
| 106 | + }); |
| 107 | + |
| 108 | + let (rel_type, reason) = match raw_et.direction { |
| 109 | + PubSub::Publish => (RelType::Publishes, reason_pub), |
| 110 | + PubSub::Subscribe => (RelType::Subscribes, reason_sub), |
| 111 | + }; |
| 112 | + |
| 113 | + edges.push(Edge { |
| 114 | + source: fn_node_idx, |
| 115 | + target: et_node_idx, |
| 116 | + rel_type, |
| 117 | + confidence: 0.9, |
| 118 | + reason, |
| 119 | + }); |
| 120 | + emitted += 1; |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + emitted |
| 125 | +} |
0 commit comments