Skip to content

Commit 3a11191

Browse files
fix: improve call resolution accuracy with scoped fallback, dedup, and built-in skip
Dogfooding revealed ~52% false positives in call edges. Three targeted fixes reduce edges from ~1430 to ~615 (57% reduction) while preserving all real dependencies: - Scope-aware fallback: standalone calls no longer resolve globally (confidence 0.3); only same-directory (0.7) and parent-directory (0.5) matches are kept - Edge deduplication: track seen caller→target pairs per file to prevent duplicate edges from repeated calls to the same function - Built-in receiver skip: skip resolution for console, Math, JSON, Object, Array, Promise, process, Buffer, and other runtime globals that never resolve to user-defined symbols
1 parent 5480d46 commit 3a11191

1 file changed

Lines changed: 40 additions & 3 deletions

File tree

src/builder.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,37 @@ import { computeConfidence, resolveImportPath, resolveImportsBatch } from './res
1111

1212
export { resolveImportPath } from './resolve.js';
1313

14+
const BUILTIN_RECEIVERS = new Set([
15+
'console',
16+
'Math',
17+
'JSON',
18+
'Object',
19+
'Array',
20+
'String',
21+
'Number',
22+
'Boolean',
23+
'Date',
24+
'RegExp',
25+
'Map',
26+
'Set',
27+
'WeakMap',
28+
'WeakSet',
29+
'Promise',
30+
'Symbol',
31+
'Error',
32+
'TypeError',
33+
'RangeError',
34+
'Proxy',
35+
'Reflect',
36+
'Intl',
37+
'globalThis',
38+
'window',
39+
'document',
40+
'process',
41+
'Buffer',
42+
'require',
43+
]);
44+
1445
export function collectFiles(dir, files = [], config = {}, directories = null) {
1546
const trackDirs = directories !== null;
1647
let entries;
@@ -458,7 +489,9 @@ export async function buildGraph(rootDir, opts = {}) {
458489
}
459490

460491
// Call edges with confidence scoring — using pre-loaded lookup maps (N+1 fix)
492+
const seenCallEdges = new Set();
461493
for (const call of symbols.calls) {
494+
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
462495
let caller = null;
463496
for (const def of symbols.definitions) {
464497
if (def.line <= call.line) {
@@ -499,8 +532,10 @@ export async function buildGraph(rootDir, opts = {}) {
499532
call.receiver === 'self' ||
500533
call.receiver === 'super'
501534
) {
502-
// Global fallback — only for standalone calls or this/self/super calls
503-
targets = nodesByName.get(call.name) || [];
535+
// Scoped fallback — same-dir or parent-dir only, not global
536+
targets = (nodesByName.get(call.name) || []).filter(
537+
(n) => computeConfidence(relPath, n.file, null) >= 0.5,
538+
);
504539
}
505540
// else: method call on a receiver — skip global fallback entirely
506541
}
@@ -515,7 +550,9 @@ export async function buildGraph(rootDir, opts = {}) {
515550
}
516551

517552
for (const t of targets) {
518-
if (t.id !== caller.id) {
553+
const edgeKey = `${caller.id}|${t.id}`;
554+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
555+
seenCallEdges.add(edgeKey);
519556
const confidence = computeConfidence(relPath, t.file, importedFrom);
520557
insertEdge.run(caller.id, t.id, 'calls', confidence, isDynamic);
521558
edgeCount++;

0 commit comments

Comments
 (0)