From 12ab4aa29c9f053b5343aa1fc651c0afd43cbce4 Mon Sep 17 00:00:00 2001 From: Caleb Kaiser <42076840+caleb-kaiser@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:58:20 -0400 Subject: [PATCH] Avoid callback synthesis scans on minified files --- .../closure-collection-synthesizer.test.ts | 71 +++++++++++++++++++ src/resolution/callback-synthesizer.ts | 60 ++++++++++------ 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/__tests__/closure-collection-synthesizer.test.ts b/__tests__/closure-collection-synthesizer.test.ts index b516cb496..7de960744 100644 --- a/__tests__/closure-collection-synthesizer.test.ts +++ b/__tests__/closure-collection-synthesizer.test.ts @@ -121,4 +121,75 @@ describe('closure-collection synthesizer', () => { expect(rows.some((r: any) => r.field === 'names')).toBe(false); expect(rows.some((r: any) => r.target_name === 'addName')).toBe(false); }); + + it('skips minified-style closure-collection files while still indexing their methods', async () => { + fs.writeFileSync( + path.join(dir, 'Request.swift'), + `class Request { + var validators: [() -> Void] = [] + + func didCompleteTask() { + validators.forEach { $0() } + } +} +` + ); + + fs.writeFileSync( + path.join(dir, 'DataRequest.swift'), + `class DataRequest: Request { + func validate(_ validation: @escaping () -> Void) -> Self { + let validator: () -> Void = { validation() } + validators.append(validator) + return self + } +} +` + ); + + fs.writeFileSync( + path.join(dir, 'Minified.swift'), + `class MinifiedRequest { var handlers: [() -> Void] = []; func runHandlers() { handlers.forEach { $0() } } } class MinifiedDataRequest: MinifiedRequest { func addHandler(_ handler: @escaping () -> Void) -> Self { handlers.append(handler); return self } } // ${'x'.repeat(280)}` + ); + + const cg = await CodeGraph.init(dir, { silent: true }); + await cg.indexAll(); + + const db = (cg as any).db.db; + const rows = db + .prepare( + `SELECT s.name source_name, s.file_path source_file, + t.name target_name, t.file_path target_file, + json_extract(e.metadata,'$.registeredAt') registeredAt + FROM edges e + JOIN nodes s ON s.id = e.source + JOIN nodes t ON t.id = e.target + WHERE json_extract(e.metadata,'$.synthesizedBy') = 'closure-collection' + ORDER BY s.file_path, t.file_path` + ) + .all(); + const minifiedMethods = db + .prepare( + `SELECT name + FROM nodes + WHERE file_path = 'Minified.swift' AND kind = 'method' + ORDER BY name` + ) + .all() + .map((r: any) => r.name); + cg.close?.(); + + expect(minifiedMethods).toEqual(['addHandler', 'runHandlers']); + + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + source_name: 'didCompleteTask', + source_file: 'Request.swift', + target_name: 'validate', + target_file: 'DataRequest.swift', + }); + expect(rows[0].registeredAt).toMatch(/DataRequest\.swift:\d+/); + expect(rows.some((r: any) => r.target_name === 'addHandler')).toBe(false); + expect(rows.some((r: any) => String(r.registeredAt).includes('Minified.swift'))).toBe(false); + }); }); diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index ec4649ba3..4faa11d0a 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -221,31 +221,45 @@ function closureCollectionEdges(queries: QueryBuilder, ctx: ResolutionContext): registrars.set(field, arr); }; + const nodesByFile = new Map(); for (const m of methodAndFunctionNodes(queries)) { - const content = ctx.readFile(m.filePath); - const src = content && sliceLines(content, m.startLine, m.endLine); - if (!src) continue; - const hasForEach = src.includes('.forEach'); - const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert('); - if (!hasForEach && !hasAppend) continue; - const lineAt = (idx: number) => (m.startLine ?? 1) + src.slice(0, idx).split('\n').length - 1; - - if (hasForEach) { - CC_DISPATCH_RE.lastIndex = 0; - let d: RegExpExecArray | null; - while ((d = CC_DISPATCH_RE.exec(src))) { - const arr = dispatchers.get(d[1]!) ?? []; - if (!arr.some((n) => n.node.id === m.id)) arr.push({ node: m, line: lineAt(d.index) }); - dispatchers.set(d[1]!, arr); + const arr = nodesByFile.get(m.filePath) ?? []; + arr.push(m); + nodesByFile.set(m.filePath, arr); + } + + for (const [filePath, nodes] of nodesByFile) { + if (isGeneratedFile(filePath)) continue; + const content = ctx.readFile(filePath); + if (!content) continue; + const newlineCount = (content.match(/\n/g)?.length ?? 0) + 1; + if (content.length / newlineCount > 200) continue; + + for (const m of nodes) { + const src = sliceLines(content, m.startLine, m.endLine); + if (!src) continue; + const hasForEach = src.includes('.forEach'); + const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert('); + if (!hasForEach && !hasAppend) continue; + const lineAt = (idx: number) => (m.startLine ?? 1) + src.slice(0, idx).split('\n').length - 1; + + if (hasForEach) { + CC_DISPATCH_RE.lastIndex = 0; + let d: RegExpExecArray | null; + while ((d = CC_DISPATCH_RE.exec(src))) { + const arr = dispatchers.get(d[1]!) ?? []; + if (!arr.some((n) => n.node.id === m.id)) arr.push({ node: m, line: lineAt(d.index) }); + dispatchers.set(d[1]!, arr); + } + } + if (hasAppend) { + CC_APPEND_WRITE_RE.lastIndex = 0; + let w: RegExpExecArray | null; + while ((w = CC_APPEND_WRITE_RE.exec(src))) addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver + CC_APPEND_DIRECT_RE.lastIndex = 0; + let a: RegExpExecArray | null; + while ((a = CC_APPEND_DIRECT_RE.exec(src))) addReg(a[1], m, lineAt(a.index)); } - } - if (hasAppend) { - CC_APPEND_WRITE_RE.lastIndex = 0; - let w: RegExpExecArray | null; - while ((w = CC_APPEND_WRITE_RE.exec(src))) addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver - CC_APPEND_DIRECT_RE.lastIndex = 0; - let a: RegExpExecArray | null; - while ((a = CC_APPEND_DIRECT_RE.exec(src))) addReg(a[1], m, lineAt(a.index)); } }