(ctx: ResolutionContext, onYield: MaybeYield)
| 2675 | } |
| 2676 | |
| 2677 | async function springEventEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 2678 | let scannedFiles = 0; |
| 2679 | // Pass 1 — event-type → listener methods, scanning only event-relevant files. |
| 2680 | // This is the ONLY full read sweep: publisher files are recorded here so |
| 2681 | // pass 2 re-reads just those instead of every .java file again (#1212 — |
| 2682 | // the double full-repo read was one of the tail's longest unyielded spans). |
| 2683 | const listeners = new Map<string, Node[]>(); |
| 2684 | const publisherFiles: string[] = []; |
| 2685 | for (const file of ctx.getAllFiles()) { |
| 2686 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 2687 | if (!SPRING_JAVA_EXT.test(file)) continue; |
| 2688 | const content = ctx.readFile(file); |
| 2689 | if (!content) continue; |
| 2690 | if (content.includes('.publishEvent(')) publisherFiles.push(file); |
| 2691 | const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener'); |
| 2692 | const hasAppListener = SPRING_APP_LISTENER_RE.test(content); |
| 2693 | if (!hasAnno && !hasAppListener) continue; |
| 2694 | const lines = content.split('\n'); |
| 2695 | for (const node of ctx.getNodesInFile(file)) { |
| 2696 | if (node.kind !== 'method') continue; |
| 2697 | // Collect this method's own leading annotation block (consecutive `@`-lines from startLine). |
| 2698 | const annoLines: string[] = []; |
| 2699 | for (let i = node.startLine - 1; i < lines.length && i < node.startLine + 7; i++) { |
| 2700 | const t = (lines[i] ?? '').trim(); |
| 2701 | if (!t.startsWith('@')) break; // reached the declaration → stop (no bleed into next method) |
| 2702 | annoLines.push(t); |
| 2703 | } |
| 2704 | const head = annoLines.join('\n'); |
| 2705 | const annotated = hasAnno && SPRING_LISTENER_ANNO_RE.test(head); |
| 2706 | const isAppListener = hasAppListener && node.name === 'onApplicationEvent'; |
| 2707 | if (!annotated && !isAppListener) continue; |
| 2708 | let type = springFirstParamType(node.signature); |
| 2709 | if (!type && annotated) { |
| 2710 | const m = SPRING_ANNO_TYPE_RE.exec(head); |
| 2711 | if (m) type = m[1]!; |
| 2712 | } |
| 2713 | if (!type) continue; |
| 2714 | let arr = listeners.get(type); |
| 2715 | if (!arr) { arr = []; listeners.set(type, arr); } |
| 2716 | arr.push(node); |
| 2717 | } |
| 2718 | } |
| 2719 | if (!listeners.size) return []; |
| 2720 | |
| 2721 | // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of |
| 2722 | // XEvent. Only the publisher files recorded in pass 1 are (re-)read. |
| 2723 | const edges: Edge[] = []; |
| 2724 | const seen = new Set<string>(); |
| 2725 | for (const file of publisherFiles) { |
| 2726 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 2727 | const content = ctx.readFile(file); |
| 2728 | if (!content || !content.includes('.publishEvent(')) continue; |
| 2729 | const safe = stripCommentsForRegex(content, 'java'); |
| 2730 | const nodesInFile = ctx.getNodesInFile(file); |
| 2731 | SPRING_PUBLISH_RE.lastIndex = 0; |
| 2732 | let m: RegExpExecArray | null; |
| 2733 | let added = 0; |
| 2734 | while ((m = SPRING_PUBLISH_RE.exec(safe)) && added < SPRING_FANOUT_CAP) { |
no test coverage detected