(ctx: ResolutionContext, onYield: MaybeYield)
| 2894 | const SIDEKIQ_FANOUT_CAP = 80; |
| 2895 | |
| 2896 | async function sidekiqDispatchEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 2897 | let scannedFiles = 0; |
| 2898 | // class node id → its instance `perform` method (null if the class isn't a Sidekiq worker), |
| 2899 | // memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers. |
| 2900 | const performCache = new Map<string, Node | null>(); |
| 2901 | const performOf = (cls: Node): Node | null => { |
| 2902 | let v = performCache.get(cls.id); |
| 2903 | if (v !== undefined) return v; |
| 2904 | v = null; |
| 2905 | const content = ctx.readFile(cls.filePath); |
| 2906 | if (content) { |
| 2907 | const end = cls.endLine ?? cls.startLine; |
| 2908 | const body = content.split('\n').slice(cls.startLine - 1, end).join('\n'); |
| 2909 | if (SIDEKIQ_WORKER_RE.test(body)) { |
| 2910 | v = ctx.getNodesInFile(cls.filePath).find( |
| 2911 | (n) => n.kind === 'method' && n.name === 'perform' && n.startLine >= cls.startLine && n.startLine <= end |
| 2912 | ) ?? null; |
| 2913 | } |
| 2914 | } |
| 2915 | performCache.set(cls.id, v); |
| 2916 | return v; |
| 2917 | }; |
| 2918 | |
| 2919 | // Resolve a (possibly namespaced) worker reference to its `perform`. A namespaced ref is |
| 2920 | // matched by EXACT qualified name first, so same-named workers in different namespaces |
| 2921 | // (forem has four `SendEmailNotificationWorker`s) resolve to the right one; an unqualified |
| 2922 | // ref falls back to the simple name and links only when a single worker bears it — an |
| 2923 | // ambiguous collision bails (precision over recall). |
| 2924 | const resolve = (ref: string): Node | null => { |
| 2925 | if (ref.includes('::')) { |
| 2926 | const q = ctx.getNodesByQualifiedName(ref).find((n) => n.kind === 'class' && performOf(n)); |
| 2927 | if (q) return performOf(q); |
| 2928 | } |
| 2929 | const workers = ctx.getNodesByName(ref.split('::').pop()!).filter((n) => n.kind === 'class' && performOf(n)); |
| 2930 | return workers.length === 1 ? performOf(workers[0]!) : null; |
| 2931 | }; |
| 2932 | |
| 2933 | const edges: Edge[] = []; |
| 2934 | const seen = new Set<string>(); |
| 2935 | for (const file of ctx.getAllFiles()) { |
| 2936 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 2937 | if (!SIDEKIQ_RB_EXT.test(file)) continue; |
| 2938 | const content = ctx.readFile(file); |
| 2939 | if (!content || !/\.perform_(?:async|in|at)\b/.test(content)) continue; |
| 2940 | const safe = stripCommentsForRegex(content, 'ruby'); |
| 2941 | const nodesInFile = ctx.getNodesInFile(file); |
| 2942 | SIDEKIQ_DISPATCH_RE.lastIndex = 0; |
| 2943 | let m: RegExpExecArray | null; |
| 2944 | let added = 0; |
| 2945 | while ((m = SIDEKIQ_DISPATCH_RE.exec(safe)) && added < SIDEKIQ_FANOUT_CAP) { |
| 2946 | const line = safe.slice(0, m.index).split('\n').length; |
| 2947 | const disp = enclosingFn(nodesInFile, line); |
| 2948 | if (!disp) continue; |
| 2949 | const target = resolve(m[1]!); |
| 2950 | if (!target || target.id === disp.id) continue; |
| 2951 | const key = `${disp.id}>${target.id}`; |
| 2952 | if (seen.has(key)) continue; |
| 2953 | seen.add(key); |
no test coverage detected