Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions __tests__/closure-collection-synthesizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
60 changes: 37 additions & 23 deletions src/resolution/callback-synthesizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,31 +221,45 @@ function closureCollectionEdges(queries: QueryBuilder, ctx: ResolutionContext):
registrars.set(field, arr);
};

const nodesByFile = new Map<string, Node[]>();
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));
}
}

Expand Down