Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')).
(ctx: ResolutionContext, onYield: MaybeYield)
| 327 | |
| 328 | /** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */ |
| 329 | async function eventEmitterEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 330 | let scannedFiles = 0; |
| 331 | const emitsByEvent = new Map<string, Set<string>>(); // event → dispatcher node ids |
| 332 | const handlersByEvent = new Map<string, Map<string, string>>(); // event → handler id → registration site (file:line) |
| 333 | |
| 334 | let scanned = 0; |
| 335 | for (const file of ctx.getAllFiles()) { |
| 336 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 337 | if ((++scanned & 255) === 0) await onYield(); // #1091: yield mid-scan on huge graphs |
| 338 | const content = ctx.readFile(file); |
| 339 | if (!content) continue; |
| 340 | const hasEmit = content.includes('.emit(') || content.includes('.fire(') || content.includes('.dispatchEvent('); |
| 341 | const hasOn = content.includes('.on(') || content.includes('.once(') || content.includes('.addListener('); |
| 342 | if (!hasEmit && !hasOn) continue; |
| 343 | const nodesInFile = ctx.getNodesInFile(file); |
| 344 | const lineOf = makeLineAt(content, 1); |
| 345 | |
| 346 | if (hasEmit) { |
| 347 | EMIT_RE.lastIndex = 0; |
| 348 | let m: RegExpExecArray | null; |
| 349 | while ((m = EMIT_RE.exec(content))) { |
| 350 | const disp = enclosingFn(nodesInFile, lineOf(m.index)); |
| 351 | if (!disp) continue; |
| 352 | const set = emitsByEvent.get(m[1]!) ?? new Set<string>(); |
| 353 | set.add(disp.id); emitsByEvent.set(m[1]!, set); |
| 354 | } |
| 355 | } |
| 356 | if (hasOn) { |
| 357 | ON_RE.lastIndex = 0; |
| 358 | let m: RegExpExecArray | null; |
| 359 | while ((m = ON_RE.exec(content))) { |
| 360 | const handlerName = m[2] || m[3]; |
| 361 | if (!handlerName) continue; |
| 362 | const handler = ctx.getNodesByName(handlerName).find((n) => n.kind === 'function' || n.kind === 'method'); |
| 363 | if (!handler) continue; |
| 364 | const map = handlersByEvent.get(m[1]!) ?? new Map<string, string>(); |
| 365 | map.set(handler.id, `${file}:${lineOf(m.index)}`); handlersByEvent.set(m[1]!, map); |
| 366 | } |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | const edges: Edge[] = []; |
| 371 | const seen = new Set<string>(); |
| 372 | for (const [event, dispatchers] of emitsByEvent) { |
| 373 | const handlers = handlersByEvent.get(event); |
| 374 | if (!handlers) continue; |
| 375 | // Precision guard: a generic event name with many handlers/dispatchers can't |
| 376 | // be matched without receiver-type info (Phase 3) — skip rather than over-link. |
| 377 | if (dispatchers.size > EVENT_FANOUT_CAP || handlers.size > EVENT_FANOUT_CAP) continue; |
| 378 | for (const d of dispatchers) for (const [h, registeredAt] of handlers) { |
| 379 | if (d === h) continue; |
| 380 | const key = `${d}>${h}`; |
| 381 | if (seen.has(key)) continue; |
| 382 | seen.add(key); |
| 383 | edges.push({ source: d, target: h, kind: 'calls', provenance: 'heuristic', metadata: { synthesizedBy: 'event-emitter', event, registeredAt } }); |
| 384 | } |
| 385 | } |
| 386 | return edges; |
no test coverage detected