ucn 3.8.21 → 3.8.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/index.js +2 -2
- package/core/analysis.js +24 -5
- package/core/cache.js +114 -60
- package/core/callers.js +71 -26
- package/core/deadcode.js +31 -2
- package/core/graph-build.js +224 -0
- package/core/graph.js +7 -10
- package/core/parallel-build.js +10 -7
- package/core/project.js +106 -222
- package/core/registry.js +5 -0
- package/core/search.js +9 -5
- package/core/tracing.js +15 -6
- package/languages/go.js +8 -8
- package/package.json +2 -2
package/cli/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const { detectLanguage } = require('../core/parser');
|
|
|
14
14
|
const { ProjectIndex } = require('../core/project');
|
|
15
15
|
const { expandGlob, findProjectRoot } = require('../core/discovery');
|
|
16
16
|
const output = require('../core/output');
|
|
17
|
-
const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName } = require('../core/registry');
|
|
17
|
+
const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName, FILE_LOCAL_COMMANDS } = require('../core/registry');
|
|
18
18
|
const { execute } = require('../core/execute');
|
|
19
19
|
const { ExpandCache } = require('../core/expand-cache');
|
|
20
20
|
|
|
@@ -322,7 +322,7 @@ function runFileCommand(filePath, command, arg) {
|
|
|
322
322
|
const canonical = resolveCommand(command, 'cli') || command;
|
|
323
323
|
|
|
324
324
|
// Commands that need full project index — auto-route to project mode
|
|
325
|
-
const fileLocalCommands =
|
|
325
|
+
const fileLocalCommands = FILE_LOCAL_COMMANDS;
|
|
326
326
|
|
|
327
327
|
if (!fileLocalCommands.has(canonical)) {
|
|
328
328
|
// Auto-detect project root and route to project mode
|
package/core/analysis.js
CHANGED
|
@@ -353,13 +353,24 @@ function related(index, name, options = {}) {
|
|
|
353
353
|
if (related.similarNames.length > similarLimit) related.similarNames = related.similarNames.slice(0, similarLimit);
|
|
354
354
|
|
|
355
355
|
// 3. Shared callers - functions called by the same callers
|
|
356
|
-
|
|
356
|
+
// Cap findCallers to avoid O(hundreds × findCallees) on ambiguous names
|
|
357
|
+
const maxSharedCallerScan = options.all ? 50 : 20;
|
|
358
|
+
const myCallersRaw = index.findCallers(name, { maxResults: maxSharedCallerScan * 3 });
|
|
359
|
+
const myCallers = new Set(myCallersRaw.map(c => c.callerName).filter(Boolean));
|
|
357
360
|
if (myCallers.size > 0) {
|
|
358
361
|
const callerCounts = new Map();
|
|
362
|
+
const calleeCache = new Map();
|
|
363
|
+
let scannedCallers = 0;
|
|
359
364
|
for (const callerName of myCallers) {
|
|
365
|
+
if (scannedCallers >= maxSharedCallerScan) break;
|
|
360
366
|
const callerDef = index.symbols.get(callerName)?.[0];
|
|
361
367
|
if (callerDef) {
|
|
362
|
-
|
|
368
|
+
let callees = calleeCache.get(callerName);
|
|
369
|
+
if (!callees) {
|
|
370
|
+
callees = index.findCallees(callerDef);
|
|
371
|
+
calleeCache.set(callerName, callees);
|
|
372
|
+
}
|
|
373
|
+
scannedCallers++;
|
|
363
374
|
for (const callee of callees) {
|
|
364
375
|
if (callee.name !== name) {
|
|
365
376
|
callerCounts.set(callee.name, (callerCounts.get(callee.name) || 0) + 1);
|
|
@@ -394,9 +405,14 @@ function related(index, name, options = {}) {
|
|
|
394
405
|
const myCalleeNames = new Set(myCallees.map(c => c.name));
|
|
395
406
|
if (myCalleeNames.size > 0) {
|
|
396
407
|
const calleeCounts = new Map();
|
|
408
|
+
// Cap callee scan to avoid O(callees × findCallers) explosion
|
|
409
|
+
const maxCalleeScan = options.all ? 30 : 15;
|
|
410
|
+
let scannedCallees = 0;
|
|
397
411
|
for (const calleeName of myCalleeNames) {
|
|
412
|
+
if (scannedCallees >= maxCalleeScan) break;
|
|
413
|
+
scannedCallees++;
|
|
398
414
|
// Find other functions that also call this callee
|
|
399
|
-
const callers = index.findCallers(calleeName);
|
|
415
|
+
const callers = index.findCallers(calleeName, { maxResults: 50 });
|
|
400
416
|
for (const caller of callers) {
|
|
401
417
|
if (caller.callerName && caller.callerName !== name) {
|
|
402
418
|
calleeCounts.set(caller.callerName, (calleeCounts.get(caller.callerName) || 0) + 1);
|
|
@@ -789,7 +805,9 @@ function about(index, name, options = {}) {
|
|
|
789
805
|
let aboutConfFiltered = 0;
|
|
790
806
|
if (primary.type === 'function' || primary.params !== undefined) {
|
|
791
807
|
// Use maxResults to limit file iteration (with buffer for exclude filtering)
|
|
792
|
-
|
|
808
|
+
// Reduce buffer for highly ambiguous names (many definitions = more noise, less value per caller)
|
|
809
|
+
const callerMultiplier = definitions.length > 5 ? 1.5 : 3;
|
|
810
|
+
const callerCap = maxCallers === Infinity ? undefined : Math.ceil(maxCallers * callerMultiplier);
|
|
793
811
|
allCallers = index.findCallers(symbolName, { includeMethods, includeUncertain: options.includeUncertain, targetDefinitions: [primary], maxResults: callerCap });
|
|
794
812
|
// Apply exclude filter before slicing
|
|
795
813
|
if (options.exclude && options.exclude.length > 0) {
|
|
@@ -837,7 +855,8 @@ function about(index, name, options = {}) {
|
|
|
837
855
|
}
|
|
838
856
|
|
|
839
857
|
// Find tests — scope to the same file/class as the primary definition
|
|
840
|
-
|
|
858
|
+
// Skip expensive test search for highly ambiguous names (>10 other definitions)
|
|
859
|
+
const tests = (others.length > 10 && !options.all) ? [] : index.tests(symbolName, {
|
|
841
860
|
file: options.file,
|
|
842
861
|
className: options.className || primary.className,
|
|
843
862
|
exclude: options.exclude,
|
package/core/cache.js
CHANGED
|
@@ -68,31 +68,22 @@ function saveCache(index, cachePath) {
|
|
|
68
68
|
strippedFiles.push([rp, rest]);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Convert graph paths from absolute to relative
|
|
71
|
+
// Convert graph paths from absolute to relative (Sets serialized as arrays)
|
|
72
72
|
const relGraph = (graph) => {
|
|
73
73
|
const result = [];
|
|
74
74
|
for (const [absKey, absValues] of graph) {
|
|
75
75
|
const relKey = path.relative(root, absKey);
|
|
76
|
-
const relValues = absValues.map(v => path.relative(root, v));
|
|
76
|
+
const relValues = [...absValues].map(v => path.relative(root, v));
|
|
77
77
|
result.push([relKey, relValues]);
|
|
78
78
|
}
|
|
79
79
|
return result;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
if (index.calleeIndex && index.calleeIndex.size > 0) {
|
|
85
|
-
calleeIndexData = [];
|
|
86
|
-
for (const [name, files] of index.calleeIndex) {
|
|
87
|
-
const relFiles = [...files].map(f =>
|
|
88
|
-
path.isAbsolute(f) ? path.relative(root, f) : f
|
|
89
|
-
);
|
|
90
|
-
calleeIndexData.push([name, relFiles]);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
82
|
+
// calleeIndex is NOT persisted in index.json — it's rebuilt lazily from callsCache
|
|
83
|
+
// on first findCallers/buildCalleeIndex call. Removing it saves ~22MB (14%) on large projects.
|
|
93
84
|
|
|
94
85
|
const cacheData = {
|
|
95
|
-
version:
|
|
86
|
+
version: 8, // v8: remove calleeIndex from index.json (rebuilt from callsCache)
|
|
96
87
|
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
97
88
|
configHash,
|
|
98
89
|
root,
|
|
@@ -108,23 +99,22 @@ function saveCache(index, cachePath) {
|
|
|
108
99
|
failedFiles: index.failedFiles
|
|
109
100
|
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
110
101
|
: [],
|
|
111
|
-
...(calleeIndexData && { calleeIndex: calleeIndexData })
|
|
112
102
|
};
|
|
113
103
|
|
|
114
104
|
fs.writeFileSync(cacheFile, JSON.stringify(cacheData));
|
|
115
105
|
|
|
116
|
-
// Save callsCache sharded by directory for lazy loading
|
|
106
|
+
// Save callsCache sharded by directory for lazy loading.
|
|
107
|
+
// Write to a temp directory first, then atomic swap to avoid data loss on crash.
|
|
117
108
|
if (callsCacheData.length > 0) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
fs.rmSync(legacyFile, { force: true });
|
|
109
|
+
const cacheDir = path.dirname(cacheFile);
|
|
110
|
+
const callsDir = path.join(cacheDir, 'calls');
|
|
111
|
+
const callsTmpDir = path.join(cacheDir, 'calls.tmp');
|
|
112
|
+
|
|
113
|
+
// Clean up any leftover temp dir from a previous crashed save
|
|
114
|
+
if (fs.existsSync(callsTmpDir)) {
|
|
115
|
+
fs.rmSync(callsTmpDir, { recursive: true, force: true });
|
|
126
116
|
}
|
|
127
|
-
fs.mkdirSync(
|
|
117
|
+
fs.mkdirSync(callsTmpDir, { recursive: true });
|
|
128
118
|
|
|
129
119
|
// Group by directory
|
|
130
120
|
const shards = new Map();
|
|
@@ -134,17 +124,29 @@ function saveCache(index, cachePath) {
|
|
|
134
124
|
shards.get(dir).push([relPath, entry]);
|
|
135
125
|
}
|
|
136
126
|
|
|
137
|
-
// Write
|
|
127
|
+
// Write all shards to temp directory
|
|
138
128
|
const shardManifest = [];
|
|
139
129
|
for (const [dir, entries] of shards) {
|
|
140
130
|
const hash = crypto.createHash('md5').update(dir).digest('hex').slice(0, 10);
|
|
141
|
-
const shardFile = path.join(
|
|
131
|
+
const shardFile = path.join(callsTmpDir, `${hash}.json`);
|
|
142
132
|
fs.writeFileSync(shardFile, JSON.stringify(entries));
|
|
143
133
|
shardManifest.push([dir, hash, entries.length]);
|
|
144
134
|
}
|
|
145
135
|
|
|
146
|
-
// Write manifest
|
|
147
|
-
fs.writeFileSync(path.join(
|
|
136
|
+
// Write manifest to temp directory
|
|
137
|
+
fs.writeFileSync(path.join(callsTmpDir, 'manifest.json'), JSON.stringify(shardManifest));
|
|
138
|
+
|
|
139
|
+
// Atomic swap: remove old, rename temp to final
|
|
140
|
+
if (fs.existsSync(callsDir)) {
|
|
141
|
+
fs.rmSync(callsDir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
fs.renameSync(callsTmpDir, callsDir);
|
|
144
|
+
|
|
145
|
+
// Clean up legacy monolithic file
|
|
146
|
+
const legacyFile = path.join(cacheDir, 'calls-cache.json');
|
|
147
|
+
if (fs.existsSync(legacyFile)) {
|
|
148
|
+
fs.rmSync(legacyFile, { force: true });
|
|
149
|
+
}
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
return cacheFile;
|
|
@@ -168,7 +170,8 @@ function loadCache(index, cachePath) {
|
|
|
168
170
|
|
|
169
171
|
// Check version compatibility
|
|
170
172
|
// v7: symbols/bindings stripped from file entries (dedup)
|
|
171
|
-
|
|
173
|
+
// v8: calleeIndex removed from index.json (rebuilt from callsCache)
|
|
174
|
+
if (cacheData.version !== 7 && cacheData.version !== 8) {
|
|
172
175
|
return false;
|
|
173
176
|
}
|
|
174
177
|
|
|
@@ -186,12 +189,19 @@ function loadCache(index, cachePath) {
|
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
const root = cacheData.root || index.root;
|
|
192
|
+
// Fast path conversion: string concat is ~70x faster than path.join for
|
|
193
|
+
// cache-stored relative paths (no '..' segments). On Windows, path.relative
|
|
194
|
+
// produces backslash paths, so rootPrefix uses the native separator.
|
|
195
|
+
const rootPrefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
196
|
+
const toAbs = path.sep === '/'
|
|
197
|
+
? (relPath) => rootPrefix + relPath
|
|
198
|
+
: (relPath) => rootPrefix + relPath.replace(/\//g, path.sep);
|
|
189
199
|
|
|
190
200
|
// Reconstruct files Map: relative key → absolute key, restore path and relativePath
|
|
191
201
|
// Initialize symbols/bindings arrays (will be populated from top-level symbols)
|
|
192
202
|
index.files = new Map();
|
|
193
203
|
for (const [relPath, entry] of cacheData.files) {
|
|
194
|
-
const absPath =
|
|
204
|
+
const absPath = toAbs(relPath);
|
|
195
205
|
entry.path = absPath;
|
|
196
206
|
entry.relativePath = relPath;
|
|
197
207
|
if (!entry.symbols) entry.symbols = [];
|
|
@@ -204,7 +214,7 @@ function loadCache(index, cachePath) {
|
|
|
204
214
|
index.symbols = new Map(cacheData.symbols);
|
|
205
215
|
for (const [, defs] of index.symbols) {
|
|
206
216
|
for (const s of defs) {
|
|
207
|
-
if (!s.file && s.relativePath) s.file =
|
|
217
|
+
if (!s.file && s.relativePath) s.file = toAbs(s.relativePath);
|
|
208
218
|
if (!s.bindingId && s.relativePath && s.type && s.startLine) {
|
|
209
219
|
s.bindingId = `${s.relativePath}:${s.type}:${s.startLine}`;
|
|
210
220
|
}
|
|
@@ -222,11 +232,14 @@ function loadCache(index, cachePath) {
|
|
|
222
232
|
}
|
|
223
233
|
}
|
|
224
234
|
|
|
225
|
-
// Reconstruct graphs: relative paths → absolute paths
|
|
235
|
+
// Reconstruct graphs: relative paths → absolute paths (as Sets)
|
|
236
|
+
// Uses string concat (toAbs) instead of path.join — 70x faster on 464K edges
|
|
226
237
|
const absGraph = (data) => {
|
|
227
238
|
const m = new Map();
|
|
228
239
|
for (const [relKey, relValues] of data) {
|
|
229
|
-
|
|
240
|
+
const absValues = new Set();
|
|
241
|
+
for (const v of relValues) absValues.add(toAbs(v));
|
|
242
|
+
m.set(toAbs(relKey), absValues);
|
|
230
243
|
}
|
|
231
244
|
return m;
|
|
232
245
|
};
|
|
@@ -243,10 +256,10 @@ function loadCache(index, cachePath) {
|
|
|
243
256
|
index.extendedByGraph = new Map(cacheData.extendedByGraph);
|
|
244
257
|
}
|
|
245
258
|
|
|
246
|
-
//
|
|
247
|
-
//
|
|
259
|
+
// Prepare lazy calls cache loading — load manifest but defer shard parsing.
|
|
260
|
+
// Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
|
|
248
261
|
if (index.callsCache.size === 0) {
|
|
249
|
-
|
|
262
|
+
_prepareCallsCache(index);
|
|
250
263
|
}
|
|
251
264
|
|
|
252
265
|
// Build directory→files index from loaded data
|
|
@@ -257,17 +270,17 @@ function loadCache(index, cachePath) {
|
|
|
257
270
|
// Restore failedFiles if present (convert relative paths back to absolute)
|
|
258
271
|
if (Array.isArray(cacheData.failedFiles)) {
|
|
259
272
|
index.failedFiles = new Set(
|
|
260
|
-
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f :
|
|
273
|
+
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
261
274
|
);
|
|
262
275
|
}
|
|
263
276
|
|
|
264
|
-
// Restore calleeIndex if persisted
|
|
277
|
+
// Restore calleeIndex if persisted (v7 caches only; v8+ rebuilds lazily)
|
|
265
278
|
if (Array.isArray(cacheData.calleeIndex)) {
|
|
266
279
|
index.calleeIndex = new Map();
|
|
267
280
|
for (const [name, files] of cacheData.calleeIndex) {
|
|
268
281
|
if (!Array.isArray(files)) continue;
|
|
269
282
|
index.calleeIndex.set(name, new Set(
|
|
270
|
-
files.map(f => path.isAbsolute(f) ? f :
|
|
283
|
+
files.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
271
284
|
));
|
|
272
285
|
}
|
|
273
286
|
}
|
|
@@ -292,6 +305,12 @@ function loadCache(index, cachePath) {
|
|
|
292
305
|
* @returns {boolean} - True if cache needs rebuilding
|
|
293
306
|
*/
|
|
294
307
|
function isCacheStale(index) {
|
|
308
|
+
// Ultra-fast path: skip full check if last confirmed-fresh < 2s ago (covers MCP burst calls).
|
|
309
|
+
// Only uses _lastFreshAt (set at the end of a successful full check), not cache save timestamp.
|
|
310
|
+
if (index._lastFreshAt && Date.now() - index._lastFreshAt < 2000) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
295
314
|
// Fast path: check cached files for modifications/deletions first (stat-only).
|
|
296
315
|
// This returns early without the expensive directory walk when any file changed.
|
|
297
316
|
for (const [filePath, fileEntry] of index.files) {
|
|
@@ -337,44 +356,64 @@ function isCacheStale(index) {
|
|
|
337
356
|
}
|
|
338
357
|
}
|
|
339
358
|
|
|
359
|
+
// Record when we last confirmed the cache is fresh (enables 2s skip on burst calls)
|
|
360
|
+
index._lastFreshAt = Date.now();
|
|
340
361
|
return false;
|
|
341
362
|
}
|
|
342
363
|
|
|
343
364
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
365
|
+
* Prepare calls cache for lazy loading — reads manifest but defers shard parsing.
|
|
366
|
+
* Called during loadCache() to set up the manifest without the ~1s shard parse cost.
|
|
367
|
+
* Actual shards are loaded on first ensureCallsCacheLoaded() call.
|
|
346
368
|
* @param {object} index - ProjectIndex instance
|
|
347
|
-
* @returns {boolean} - True if loaded successfully
|
|
348
369
|
*/
|
|
349
|
-
function
|
|
350
|
-
if (index.
|
|
351
|
-
if (index._callsCacheLoaded) return false; // Already attempted, file didn't exist
|
|
352
|
-
index._callsCacheLoaded = true;
|
|
353
|
-
|
|
370
|
+
function _prepareCallsCache(index) {
|
|
371
|
+
if (index._callsCacheLoaded) return;
|
|
354
372
|
const cacheDir = path.join(index.root, '.ucn-cache');
|
|
355
|
-
|
|
356
|
-
// Try sharded format first (calls/manifest.json)
|
|
357
373
|
const manifestFile = path.join(cacheDir, 'calls', 'manifest.json');
|
|
358
374
|
if (fs.existsSync(manifestFile)) {
|
|
359
375
|
try {
|
|
360
376
|
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
|
|
361
|
-
// Store manifest for lazy loading
|
|
362
377
|
index._callsManifest = new Map();
|
|
363
378
|
for (const [dir, hash, count] of manifest) {
|
|
364
379
|
index._callsManifest.set(dir, { hash, count, loaded: false });
|
|
365
380
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
_loadCallsShard(index, hash);
|
|
369
|
-
}
|
|
370
|
-
return true;
|
|
381
|
+
index._callsCachePrepared = true;
|
|
382
|
+
return;
|
|
371
383
|
} catch (e) {
|
|
372
|
-
// Corrupted manifest — fall through
|
|
384
|
+
// Corrupted manifest — fall through
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Check legacy format
|
|
388
|
+
const legacyFile = path.join(cacheDir, 'calls-cache.json');
|
|
389
|
+
if (fs.existsSync(legacyFile)) {
|
|
390
|
+
index._callsCacheLegacyFile = legacyFile;
|
|
391
|
+
index._callsCachePrepared = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Load callsCache from separate file on demand.
|
|
397
|
+
* Only loads if callsCache is empty (not already populated from inline or prior load).
|
|
398
|
+
* @param {object} index - ProjectIndex instance
|
|
399
|
+
* @returns {boolean} - True if loaded successfully
|
|
400
|
+
*/
|
|
401
|
+
function loadCallsCache(index) {
|
|
402
|
+
if (index.callsCache.size > 0) return true; // Already populated
|
|
403
|
+
if (index._callsCacheLoaded) return false; // Already attempted, file didn't exist
|
|
404
|
+
index._callsCacheLoaded = true;
|
|
405
|
+
|
|
406
|
+
// If manifest was prepared lazily, load all shards now
|
|
407
|
+
if (index._callsManifest) {
|
|
408
|
+
for (const [, { hash }] of index._callsManifest) {
|
|
409
|
+
_loadCallsShard(index, hash);
|
|
373
410
|
}
|
|
411
|
+
return index.callsCache.size > 0;
|
|
374
412
|
}
|
|
375
413
|
|
|
376
414
|
// Legacy format: single calls-cache.json
|
|
377
|
-
const callsCacheFile =
|
|
415
|
+
const callsCacheFile = index._callsCacheLegacyFile ||
|
|
416
|
+
path.join(index.root, '.ucn-cache', 'calls-cache.json');
|
|
378
417
|
if (!fs.existsSync(callsCacheFile)) return false;
|
|
379
418
|
|
|
380
419
|
try {
|
|
@@ -393,6 +432,17 @@ function loadCallsCache(index) {
|
|
|
393
432
|
return false;
|
|
394
433
|
}
|
|
395
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Ensure calls cache is fully loaded (trigger lazy load if prepared but not loaded).
|
|
437
|
+
* Call this before any operation that needs callsCache (findCallers, buildCalleeIndex, etc.)
|
|
438
|
+
* @param {object} index - ProjectIndex instance
|
|
439
|
+
*/
|
|
440
|
+
function ensureCallsCacheLoaded(index) {
|
|
441
|
+
if (index._callsCachePrepared && !index._callsCacheLoaded) {
|
|
442
|
+
loadCallsCache(index);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
396
446
|
/**
|
|
397
447
|
* Load a single calls shard by hash.
|
|
398
448
|
* @param {object} index - ProjectIndex instance
|
|
@@ -403,9 +453,13 @@ function _loadCallsShard(index, hash) {
|
|
|
403
453
|
try {
|
|
404
454
|
const data = JSON.parse(fs.readFileSync(shardFile, 'utf-8'));
|
|
405
455
|
if (!Array.isArray(data)) return;
|
|
456
|
+
const rootPrefix = index.root.endsWith(path.sep) ? index.root : index.root + path.sep;
|
|
457
|
+
const toAbsShard = path.sep === '/'
|
|
458
|
+
? (rp) => rootPrefix + rp
|
|
459
|
+
: (rp) => rootPrefix + rp.replace(/\//g, path.sep);
|
|
406
460
|
for (const [relPath, entry] of data) {
|
|
407
461
|
if (!relPath || !entry) continue;
|
|
408
|
-
const absPath = path.isAbsolute(relPath) ? relPath :
|
|
462
|
+
const absPath = path.isAbsolute(relPath) ? relPath : toAbsShard(relPath);
|
|
409
463
|
index.callsCache.set(absPath, entry);
|
|
410
464
|
}
|
|
411
465
|
} catch (e) {
|
|
@@ -413,4 +467,4 @@ function _loadCallsShard(index, hash) {
|
|
|
413
467
|
}
|
|
414
468
|
}
|
|
415
469
|
|
|
416
|
-
module.exports = { saveCache, loadCache, loadCallsCache, isCacheStale };
|
|
470
|
+
module.exports = { saveCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded };
|
package/core/callers.js
CHANGED
|
@@ -13,6 +13,14 @@ const { isTestFile } = require('./discovery');
|
|
|
13
13
|
const { NON_CALLABLE_TYPES } = require('./shared');
|
|
14
14
|
const { scoreEdge } = require('./confidence');
|
|
15
15
|
|
|
16
|
+
/** Set.some() helper — like Array.some() but for Sets */
|
|
17
|
+
function setSome(set, predicate) {
|
|
18
|
+
for (const item of set) {
|
|
19
|
+
if (predicate(item)) return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* Extract a single line from content without splitting the entire string.
|
|
18
26
|
* @param {string} content - Full file content
|
|
@@ -40,6 +48,11 @@ function getLine(content, lineNum) {
|
|
|
40
48
|
*/
|
|
41
49
|
function getCachedCalls(index, filePath, options = {}) {
|
|
42
50
|
try {
|
|
51
|
+
// Trigger lazy calls cache load if prepared but not yet loaded
|
|
52
|
+
if (index._callsCachePrepared && !index._callsCacheLoaded) {
|
|
53
|
+
const { ensureCallsCacheLoaded } = require('./cache');
|
|
54
|
+
ensureCallsCacheLoaded(index);
|
|
55
|
+
}
|
|
43
56
|
const cached = index.callsCache.get(filePath);
|
|
44
57
|
|
|
45
58
|
// Fast path: check mtime first (stat is much faster than read+hash)
|
|
@@ -91,6 +104,8 @@ function getCachedCalls(index, filePath, options = {}) {
|
|
|
91
104
|
}
|
|
92
105
|
const calls = langModule.findCallsInCode(content, parser, callOpts);
|
|
93
106
|
|
|
107
|
+
// Remove old callee index entries before overwriting cache
|
|
108
|
+
if (cached) index._removeFromCalleeIndex(filePath, cached.calls);
|
|
94
109
|
index.callsCache.set(filePath, {
|
|
95
110
|
mtime,
|
|
96
111
|
hash,
|
|
@@ -98,6 +113,8 @@ function getCachedCalls(index, filePath, options = {}) {
|
|
|
98
113
|
content: options.includeContent ? content : undefined
|
|
99
114
|
});
|
|
100
115
|
index.callsCacheDirty = true;
|
|
116
|
+
// Incrementally update callee index with new calls
|
|
117
|
+
index._addToCalleeIndex(filePath, calls);
|
|
101
118
|
|
|
102
119
|
if (options.includeContent) {
|
|
103
120
|
return { calls, content };
|
|
@@ -109,7 +126,14 @@ function getCachedCalls(index, filePath, options = {}) {
|
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
/**
|
|
112
|
-
* Find all
|
|
129
|
+
* Find all call sites that invoke the named symbol.
|
|
130
|
+
*
|
|
131
|
+
* ReceiverType filtering (nominal vs structural):
|
|
132
|
+
* - Nominal languages (Go/Java/Rust): uses call.receiverType (from parser-inferred
|
|
133
|
+
* method receivers, constructors, composite literals) to filter false positives.
|
|
134
|
+
* - Structural languages (JS/TS/Python): checks receiver binding evidence from imports
|
|
135
|
+
* instead of receiverType, since structural typing makes receiver types ambiguous.
|
|
136
|
+
*
|
|
113
137
|
* @param {object} index - ProjectIndex instance
|
|
114
138
|
* @param {string} name - Function name to find callers for
|
|
115
139
|
* @param {object} [options] - Options
|
|
@@ -383,14 +407,14 @@ function findCallers(index, name, options = {}) {
|
|
|
383
407
|
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
384
408
|
const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
385
409
|
if (targetFiles.size > 0 && !targetFiles.has(filePath)) {
|
|
386
|
-
const imports = index.importGraph.get(filePath)
|
|
387
|
-
const importsTarget = imports
|
|
410
|
+
const imports = index.importGraph.get(filePath);
|
|
411
|
+
const importsTarget = imports && setSome(imports, imp => targetFiles.has(imp));
|
|
388
412
|
if (!importsTarget) {
|
|
389
413
|
// Check one level of re-exports (barrel files)
|
|
390
414
|
let foundViaReexport = false;
|
|
391
|
-
for (const imp of imports) {
|
|
392
|
-
const transImports = index.importGraph.get(imp)
|
|
393
|
-
if (transImports
|
|
415
|
+
if (imports) for (const imp of imports) {
|
|
416
|
+
const transImports = index.importGraph.get(imp);
|
|
417
|
+
if (transImports && setSome(transImports, ti => targetFiles.has(ti))) {
|
|
394
418
|
foundViaReexport = true;
|
|
395
419
|
break;
|
|
396
420
|
}
|
|
@@ -448,10 +472,10 @@ function findCallers(index, name, options = {}) {
|
|
|
448
472
|
continue;
|
|
449
473
|
}
|
|
450
474
|
// Multi-segment import — verify via import graph
|
|
451
|
-
const callerImportedFiles = index.importGraph.get(filePath)
|
|
475
|
+
const callerImportedFiles = index.importGraph.get(filePath);
|
|
452
476
|
const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
453
477
|
if (!targetFiles.has(filePath)) {
|
|
454
|
-
const hasImportEdge = callerImportedFiles
|
|
478
|
+
const hasImportEdge = callerImportedFiles && setSome(callerImportedFiles, imp => targetFiles.has(imp));
|
|
455
479
|
if (!hasImportEdge) {
|
|
456
480
|
// No import edge — allow same-package (same directory) calls
|
|
457
481
|
const callerDir = path.dirname(filePath);
|
|
@@ -576,13 +600,13 @@ function findCallers(index, name, options = {}) {
|
|
|
576
600
|
// Check import graph evidence: does this file import from the target definition's file?
|
|
577
601
|
const targetDefs2 = options.targetDefinitions || definitions;
|
|
578
602
|
const targetFiles2 = new Set(targetDefs2.map(d => d.file).filter(Boolean));
|
|
579
|
-
const callerImports = index.importGraph.get(filePath)
|
|
580
|
-
let hasImportLink = targetFiles2.has(filePath) || callerImports
|
|
603
|
+
const callerImports = index.importGraph.get(filePath);
|
|
604
|
+
let hasImportLink = targetFiles2.has(filePath) || (callerImports && setSome(callerImports, imp => targetFiles2.has(imp)));
|
|
581
605
|
// Check one level of re-exports (barrel files) for import evidence
|
|
582
|
-
if (!hasImportLink) {
|
|
606
|
+
if (!hasImportLink && callerImports) {
|
|
583
607
|
for (const imp of callerImports) {
|
|
584
|
-
const transImports = index.importGraph.get(imp)
|
|
585
|
-
if (transImports
|
|
608
|
+
const transImports = index.importGraph.get(imp);
|
|
609
|
+
if (transImports && setSome(transImports, ti => targetFiles2.has(ti))) {
|
|
586
610
|
hasImportLink = true;
|
|
587
611
|
break;
|
|
588
612
|
}
|
|
@@ -652,7 +676,15 @@ function findCallers(index, name, options = {}) {
|
|
|
652
676
|
}
|
|
653
677
|
|
|
654
678
|
/**
|
|
655
|
-
* Find all
|
|
679
|
+
* Find all symbols called from within a function definition.
|
|
680
|
+
*
|
|
681
|
+
* Method resolution uses receiverType when available:
|
|
682
|
+
* - Go: receiverType from method receiver params + _buildTypedLocalTypeMap (New*() patterns)
|
|
683
|
+
* - Java: receiverType from `new Foo()` constructors + typed parameter declarations
|
|
684
|
+
* - Rust: receiverType from impl block context + _buildTypedLocalTypeMap
|
|
685
|
+
* - JS/TS: receiverType from constructor calls + import binding evidence
|
|
686
|
+
* - Python: receiverType from __init__ attribute type inference (getInstanceAttributeTypes)
|
|
687
|
+
*
|
|
656
688
|
* @param {object} index - ProjectIndex instance
|
|
657
689
|
* @param {object} def - Symbol definition with file, name, startLine, endLine
|
|
658
690
|
* @param {object} [options] - Options
|
|
@@ -1160,7 +1192,7 @@ function findCallees(index, def, options = {}) {
|
|
|
1160
1192
|
const defFileEntry = fileEntry;
|
|
1161
1193
|
const callerIsTest = defFileEntry && isTestFile(defFileEntry.relativePath, defFileEntry.language);
|
|
1162
1194
|
// Pre-compute import graph for callee confidence scoring
|
|
1163
|
-
const callerImportSet =
|
|
1195
|
+
const callerImportSet = index.importGraph.get(def.file) || new Set();
|
|
1164
1196
|
|
|
1165
1197
|
for (const { name: calleeName, bindingId, count, isConstructor } of callees.values()) {
|
|
1166
1198
|
const symbols = index.symbols.get(calleeName);
|
|
@@ -1351,6 +1383,7 @@ function _buildLocalTypeMap(index, def, calls) {
|
|
|
1351
1383
|
}
|
|
1352
1384
|
const lines = content.split('\n');
|
|
1353
1385
|
const localTypes = new Map();
|
|
1386
|
+
const regexCache = new Map();
|
|
1354
1387
|
|
|
1355
1388
|
for (const call of calls) {
|
|
1356
1389
|
// Only look at calls within this function's scope
|
|
@@ -1368,18 +1401,21 @@ function _buildLocalTypeMap(index, def, calls) {
|
|
|
1368
1401
|
const sourceLine = lines[call.line - 1];
|
|
1369
1402
|
if (!sourceLine) continue;
|
|
1370
1403
|
|
|
1371
|
-
//
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1404
|
+
// Memoize compiled regex per call name (same name → same pattern)
|
|
1405
|
+
let patterns = regexCache.get(call.name);
|
|
1406
|
+
if (!patterns) {
|
|
1407
|
+
const esc = call.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1408
|
+
patterns = {
|
|
1409
|
+
assign: new RegExp(`(\\w+)\\s*(?::\\s*\\w+)?\\s*=\\s*${esc}\\s*\\(`),
|
|
1410
|
+
with: new RegExp(`with\\s+${esc}\\s*\\([^)]*\\)\\s+as\\s+(\\w+)`)
|
|
1411
|
+
};
|
|
1412
|
+
regexCache.set(call.name, patterns);
|
|
1413
|
+
}
|
|
1414
|
+
const assignMatch = sourceLine.match(patterns.assign);
|
|
1376
1415
|
if (assignMatch) {
|
|
1377
1416
|
localTypes.set(assignMatch[1], call.name);
|
|
1378
1417
|
}
|
|
1379
|
-
|
|
1380
|
-
const withMatch = sourceLine.match(
|
|
1381
|
-
new RegExp(`with\\s+${escapedName}\\s*\\([^)]*\\)\\s+as\\s+(\\w+)`)
|
|
1382
|
-
);
|
|
1418
|
+
const withMatch = sourceLine.match(patterns.with);
|
|
1383
1419
|
if (withMatch) {
|
|
1384
1420
|
localTypes.set(withMatch[1], call.name);
|
|
1385
1421
|
}
|
|
@@ -1395,6 +1431,11 @@ function _buildLocalTypeMap(index, def, calls) {
|
|
|
1395
1431
|
* @param {object} index - ProjectIndex instance
|
|
1396
1432
|
* @param {object} def - Function definition with file, startLine, endLine
|
|
1397
1433
|
* @param {Array} calls - Cached call sites for the file
|
|
1434
|
+
*
|
|
1435
|
+
* Sources: parser-inferred receiverType from method receivers, constructor calls,
|
|
1436
|
+
* composite literals. Used by Go, Java, Rust (nominal languages) to infer local
|
|
1437
|
+
* variable types for method resolution. Not used by JS/TS/Python -- structural
|
|
1438
|
+
* languages use import evidence via _buildLocalTypeMap instead.
|
|
1398
1439
|
*/
|
|
1399
1440
|
function _buildTypedLocalTypeMap(index, def, calls) {
|
|
1400
1441
|
const localTypes = new Map();
|
|
@@ -1412,10 +1453,11 @@ function _buildTypedLocalTypeMap(index, def, calls) {
|
|
|
1412
1453
|
// Handles: x := NewFoo(), x, err := NewFoo(), x := pkg.NewFoo(), x, err := pkg.NewFoo()
|
|
1413
1454
|
const newName = call.isMethod ? call.name : call.name;
|
|
1414
1455
|
if (/^New[A-Z]/.test(newName) && !call.isPotentialCallback) {
|
|
1456
|
+
if (_cachedLines === false) continue; // File unreadable, skip all
|
|
1415
1457
|
if (!_cachedLines) {
|
|
1416
1458
|
try {
|
|
1417
1459
|
_cachedLines = index._readFile(def.file).split('\n');
|
|
1418
|
-
} catch { continue; }
|
|
1460
|
+
} catch { _cachedLines = false; continue; }
|
|
1419
1461
|
}
|
|
1420
1462
|
const sourceLine = _cachedLines[call.line - 1];
|
|
1421
1463
|
if (!sourceLine) continue;
|
|
@@ -1437,7 +1479,10 @@ function _buildTypedLocalTypeMap(index, def, calls) {
|
|
|
1437
1479
|
}
|
|
1438
1480
|
|
|
1439
1481
|
/**
|
|
1440
|
-
*
|
|
1482
|
+
* Find higher-order function usages where `name` is passed as a callback argument.
|
|
1483
|
+
* Handles patterns like .map(fn), setTimeout(fn), promise.then(handler).
|
|
1484
|
+
* Delegates to per-language findCallbackUsages implementations.
|
|
1485
|
+
*
|
|
1441
1486
|
* @param {object} index - ProjectIndex instance
|
|
1442
1487
|
* @param {string} name - Function name
|
|
1443
1488
|
* @returns {Array} Callback usages
|