Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')).
(ctx: ResolutionContext, onYield: MaybeYield)
| 280 | |
| 281 | /** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */ |
| 282 | async function eventEmitterEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 283 | const emitsByEvent = new Map<string, Set<string>>(); // event → dispatcher node ids |
| 284 | const handlersByEvent = new Map<string, Map<string, string>>(); // event → handler id → registration site (file:line) |
| 285 | |
| 286 | let scanned = 0; |
| 287 | for (const file of ctx.getAllFiles()) { |
| 288 | if ((++scanned & 255) === 0) await onYield(); // #1091: yield mid-scan on huge graphs |
| 289 | const content = ctx.readFile(file); |
| 290 | if (!content) continue; |
| 291 | const hasEmit = content.includes('.emit(') || content.includes('.fire(') || content.includes('.dispatchEvent('); |
| 292 | const hasOn = content.includes('.on(') || content.includes('.once(') || content.includes('.addListener('); |
| 293 | if (!hasEmit && !hasOn) continue; |
| 294 | const nodesInFile = ctx.getNodesInFile(file); |
| 295 | const lineOf = (idx: number) => content.slice(0, idx).split('\n').length; |
| 296 | |
| 297 | if (hasEmit) { |
| 298 | EMIT_RE.lastIndex = 0; |
| 299 | let m: RegExpExecArray | null; |
| 300 | while ((m = EMIT_RE.exec(content))) { |
| 301 | const disp = enclosingFn(nodesInFile, lineOf(m.index)); |
| 302 | if (!disp) continue; |
| 303 | const set = emitsByEvent.get(m[1]!) ?? new Set<string>(); |
| 304 | set.add(disp.id); emitsByEvent.set(m[1]!, set); |
| 305 | } |
| 306 | } |
| 307 | if (hasOn) { |
| 308 | ON_RE.lastIndex = 0; |
| 309 | let m: RegExpExecArray | null; |
| 310 | while ((m = ON_RE.exec(content))) { |
| 311 | const handlerName = m[2] || m[3]; |
| 312 | if (!handlerName) continue; |
| 313 | const handler = ctx.getNodesByName(handlerName).find((n) => n.kind === 'function' || n.kind === 'method'); |
| 314 | if (!handler) continue; |
| 315 | const map = handlersByEvent.get(m[1]!) ?? new Map<string, string>(); |
| 316 | map.set(handler.id, `${file}:${lineOf(m.index)}`); handlersByEvent.set(m[1]!, map); |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | const edges: Edge[] = []; |
| 322 | const seen = new Set<string>(); |
| 323 | for (const [event, dispatchers] of emitsByEvent) { |
| 324 | const handlers = handlersByEvent.get(event); |
| 325 | if (!handlers) continue; |
| 326 | // Precision guard: a generic event name with many handlers/dispatchers can't |
| 327 | // be matched without receiver-type info (Phase 3) — skip rather than over-link. |
| 328 | if (dispatchers.size > EVENT_FANOUT_CAP || handlers.size > EVENT_FANOUT_CAP) continue; |
| 329 | for (const d of dispatchers) for (const [h, registeredAt] of handlers) { |
| 330 | if (d === h) continue; |
| 331 | const key = `${d}>${h}`; |
| 332 | if (seen.has(key)) continue; |
| 333 | seen.add(key); |
| 334 | edges.push({ source: d, target: h, kind: 'calls', provenance: 'heuristic', metadata: { synthesizedBy: 'event-emitter', event, registeredAt } }); |
| 335 | } |
| 336 | } |
| 337 | return edges; |
| 338 | } |
| 339 |
no test coverage detected