* Closure-collection dispatch: dispatcher iterates a closure-collection property * invoking each element; registrar appends a closure to the same-named property. * Emits dispatcher → registrar so a flow reaches the registration site (where the * appended closure's body — and its callers — live).
(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield)
| 250 | * name shared across unrelated classes can't fan out into noise. |
| 251 | */ |
| 252 | async function closureCollectionEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 253 | const dispatchers = new Map<string, Array<{ node: Node; line: number }>>(); // field → dispatcher methods + forEach line |
| 254 | const registrars = new Map<string, Array<{ node: Node; line: number }>>(); // field → registrar methods + append line |
| 255 | |
| 256 | const addReg = (field: string | undefined, node: Node, absLine: number) => { |
| 257 | if (!field || /^\d+$/.test(field)) return; // `$0.append` mis-captures the `0`; the write-RE owns that field |
| 258 | const arr = registrars.get(field) ?? []; |
| 259 | if (!arr.some((r) => r.node.id === node.id)) arr.push({ node, line: absLine }); |
| 260 | registrars.set(field, arr); |
| 261 | }; |
| 262 | |
| 263 | // Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so |
| 264 | // on a repo with a huge file this is the heaviest synthesis pass — yield |
| 265 | // mid-scan (and mid-match-loop below: a single generated function dense with |
| 266 | // matches must not starve the watchdog either) so it can't wedge the #850 |
| 267 | // watchdog on its own (#1091, #1235). |
| 268 | let scanned = 0; |
| 269 | let matchTick = 0; |
| 270 | for (const m of methodAndFunctionNodes(queries)) { |
| 271 | if ((++scanned & 127) === 0) await onYield(); |
| 272 | if (!CC_LANGUAGES.has(m.language)) continue; |
| 273 | const content = ctx.readFile(m.filePath); |
| 274 | const src = content && sliceLines(content, m.startLine, m.endLine); |
| 275 | if (!src) continue; |
| 276 | const hasForEach = src.includes('.forEach'); |
| 277 | const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert('); |
| 278 | if (!hasForEach && !hasAppend) continue; |
| 279 | const lineAt = makeLineAt(src, m.startLine ?? 1); |
| 280 | |
| 281 | if (hasForEach) { |
| 282 | CC_DISPATCH_RE.lastIndex = 0; |
| 283 | let d: RegExpExecArray | null; |
| 284 | while ((d = CC_DISPATCH_RE.exec(src))) { |
| 285 | if ((++matchTick & 255) === 0) await onYield(); |
| 286 | const arr = dispatchers.get(d[1]!) ?? []; |
| 287 | if (!arr.some((n) => n.node.id === m.id)) arr.push({ node: m, line: lineAt(d.index) }); |
| 288 | dispatchers.set(d[1]!, arr); |
| 289 | } |
| 290 | } |
| 291 | if (hasAppend) { |
| 292 | CC_APPEND_WRITE_RE.lastIndex = 0; |
| 293 | let w: RegExpExecArray | null; |
| 294 | while ((w = CC_APPEND_WRITE_RE.exec(src))) { |
| 295 | if ((++matchTick & 255) === 0) await onYield(); |
| 296 | addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver |
| 297 | } |
| 298 | CC_APPEND_DIRECT_RE.lastIndex = 0; |
| 299 | let a: RegExpExecArray | null; |
| 300 | while ((a = CC_APPEND_DIRECT_RE.exec(src))) { |
| 301 | if ((++matchTick & 255) === 0) await onYield(); |
| 302 | addReg(a[1], m, lineAt(a.index)); |
| 303 | } |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | const edges: Edge[] = []; |
| 308 | const seen = new Set<string>(); |
| 309 | for (const [field, disps] of dispatchers) { |
no test coverage detected