ucn 3.7.45 → 3.7.47
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/.claude/skills/ucn/SKILL.md +6 -4
- package/README.md +23 -24
- package/cli/index.js +14 -6
- package/core/cache.js +176 -51
- package/core/callers.js +315 -51
- package/core/deadcode.js +42 -16
- package/core/discovery.js +1 -1
- package/core/execute.js +148 -11
- package/core/output.js +26 -4
- package/core/project.js +290 -52
- package/core/registry.js +1 -0
- package/core/shared.js +1 -1
- package/core/stacktrace.js +31 -2
- package/core/verify.js +11 -0
- package/languages/go.js +331 -23
- package/languages/index.js +20 -1
- package/languages/java.js +109 -4
- package/languages/rust.js +93 -4
- package/mcp/server.js +33 -16
- package/package.json +11 -10
package/core/project.js
CHANGED
|
@@ -14,7 +14,7 @@ const { extractImports, extractExports, resolveImport } = require('./imports');
|
|
|
14
14
|
const { parse, parseFile, cleanHtmlScriptTags } = require('./parser');
|
|
15
15
|
const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
|
|
16
16
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
17
|
-
const { escapeRegExp, NON_CALLABLE_TYPES } = require('./shared');
|
|
17
|
+
const { escapeRegExp, NON_CALLABLE_TYPES, addTestExclusions } = require('./shared');
|
|
18
18
|
const stacktrace = require('./stacktrace');
|
|
19
19
|
const indexCache = require('./cache');
|
|
20
20
|
const deadcodeModule = require('./deadcode');
|
|
@@ -157,7 +157,7 @@ class ProjectIndex {
|
|
|
157
157
|
|
|
158
158
|
const globOpts = {
|
|
159
159
|
root: this.root,
|
|
160
|
-
maxFiles: options.maxFiles ||
|
|
160
|
+
maxFiles: options.maxFiles || this.config.maxFiles || 50000,
|
|
161
161
|
followSymlinks: options.followSymlinks
|
|
162
162
|
};
|
|
163
163
|
|
|
@@ -170,10 +170,18 @@ class ProjectIndex {
|
|
|
170
170
|
|
|
171
171
|
const files = expandGlob(pattern, globOpts);
|
|
172
172
|
|
|
173
|
+
// Track if files were truncated by maxFiles limit
|
|
174
|
+
if (files.length >= globOpts.maxFiles) {
|
|
175
|
+
this.truncated = { indexed: files.length, maxFiles: globOpts.maxFiles };
|
|
176
|
+
} else {
|
|
177
|
+
this.truncated = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
if (!quiet) {
|
|
174
181
|
console.error(`Indexing ${files.length} files in ${this.root}...`);
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
let deletedInRebuild = 0;
|
|
177
185
|
if (options.forceRebuild) {
|
|
178
186
|
// Incremental rebuild: only remove files that no longer exist on disk.
|
|
179
187
|
// indexFile() already skips unchanged files and calls removeFileSymbols()
|
|
@@ -184,6 +192,7 @@ class ProjectIndex {
|
|
|
184
192
|
this.removeFileSymbols(cachedPath);
|
|
185
193
|
this.files.delete(cachedPath);
|
|
186
194
|
this.callsCache.delete(cachedPath);
|
|
195
|
+
deletedInRebuild++;
|
|
187
196
|
}
|
|
188
197
|
}
|
|
189
198
|
}
|
|
@@ -193,10 +202,11 @@ class ProjectIndex {
|
|
|
193
202
|
this._attrTypeCache = null;
|
|
194
203
|
|
|
195
204
|
let indexed = 0;
|
|
205
|
+
let changed = 0;
|
|
196
206
|
if (!this.failedFiles) this.failedFiles = new Set();
|
|
197
207
|
for (const file of files) {
|
|
198
208
|
try {
|
|
199
|
-
this.indexFile(file)
|
|
209
|
+
if (this.indexFile(file)) changed++;
|
|
200
210
|
indexed++;
|
|
201
211
|
this.failedFiles.delete(file); // Succeeded now, remove from failed
|
|
202
212
|
} catch (e) {
|
|
@@ -207,8 +217,11 @@ class ProjectIndex {
|
|
|
207
217
|
}
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
// Skip graph rebuild when incremental rebuild found no changes
|
|
221
|
+
if (changed > 0 || deletedInRebuild > 0 || !options.forceRebuild) {
|
|
222
|
+
this.buildImportGraph();
|
|
223
|
+
this.buildInheritanceGraph();
|
|
224
|
+
}
|
|
212
225
|
|
|
213
226
|
this.buildTime = Date.now() - startTime;
|
|
214
227
|
|
|
@@ -234,14 +247,22 @@ class ProjectIndex {
|
|
|
234
247
|
* Index a single file
|
|
235
248
|
*/
|
|
236
249
|
indexFile(filePath) {
|
|
250
|
+
const stat = fs.statSync(filePath);
|
|
251
|
+
const existing = this.files.get(filePath);
|
|
252
|
+
|
|
253
|
+
// Fast path: skip read entirely when mtime+size both match
|
|
254
|
+
if (existing && existing.mtime === stat.mtimeMs && existing.size === stat.size) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
237
258
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
238
259
|
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
239
|
-
const stat = fs.statSync(filePath);
|
|
240
260
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
261
|
+
// Content-based skip: mtime changed but content didn't (touch, git checkout)
|
|
262
|
+
if (existing && existing.hash === hash) {
|
|
263
|
+
existing.mtime = stat.mtimeMs;
|
|
264
|
+
existing.size = stat.size;
|
|
265
|
+
return false;
|
|
245
266
|
}
|
|
246
267
|
|
|
247
268
|
if (existing) {
|
|
@@ -251,7 +272,11 @@ class ProjectIndex {
|
|
|
251
272
|
const language = detectLanguage(filePath);
|
|
252
273
|
if (!language) return;
|
|
253
274
|
|
|
254
|
-
|
|
275
|
+
// Parse content once — the tree-sitter cache in safeParse ensures the tree
|
|
276
|
+
// is shared across parse()/extractImports()/extractExports() (5→1 parse per file)
|
|
277
|
+
const parsed = parse(content, language);
|
|
278
|
+
parsed.filePath = filePath;
|
|
279
|
+
parsed.relativePath = filePath;
|
|
255
280
|
const { imports, dynamicCount, importAliases } = extractImports(content, language);
|
|
256
281
|
const { exports } = extractExports(content, language);
|
|
257
282
|
|
|
@@ -280,6 +305,7 @@ class ProjectIndex {
|
|
|
280
305
|
mtime: stat.mtimeMs,
|
|
281
306
|
size: stat.size,
|
|
282
307
|
imports: imports.map(i => i.module),
|
|
308
|
+
importNames: imports.flatMap(i => i.names || []),
|
|
283
309
|
exports: exports.map(e => e.name),
|
|
284
310
|
exportDetails: exports,
|
|
285
311
|
symbols: [],
|
|
@@ -313,6 +339,7 @@ class ProjectIndex {
|
|
|
313
339
|
...(item.receiver && { receiver: item.receiver }),
|
|
314
340
|
...(item.className && { className: item.className }),
|
|
315
341
|
...(item.memberType && { memberType: item.memberType }),
|
|
342
|
+
...(item.fieldType && { fieldType: item.fieldType }),
|
|
316
343
|
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
317
344
|
...(item.nameLine && { nameLine: item.nameLine })
|
|
318
345
|
};
|
|
@@ -331,6 +358,12 @@ class ProjectIndex {
|
|
|
331
358
|
};
|
|
332
359
|
|
|
333
360
|
for (const fn of parsed.functions) {
|
|
361
|
+
// Go/Rust methods: set className from receiver for consistent method resolution.
|
|
362
|
+
// Go/Rust methods are standalone functions with receiver, not class members,
|
|
363
|
+
// so className is never set by the class member loop below.
|
|
364
|
+
if (fn.receiver && !fn.className) {
|
|
365
|
+
fn.className = fn.receiver.replace(/^\*/, '');
|
|
366
|
+
}
|
|
334
367
|
addSymbol(fn, fn.isConstructor ? 'constructor' : 'function');
|
|
335
368
|
}
|
|
336
369
|
|
|
@@ -349,6 +382,7 @@ class ProjectIndex {
|
|
|
349
382
|
}
|
|
350
383
|
|
|
351
384
|
this.files.set(filePath, fileEntry);
|
|
385
|
+
return true;
|
|
352
386
|
}
|
|
353
387
|
|
|
354
388
|
/**
|
|
@@ -382,7 +416,7 @@ class ProjectIndex {
|
|
|
382
416
|
* Handles regular imports, static imports (strips member name), and wildcards (strips .*).
|
|
383
417
|
* Progressively strips trailing segments to find the class file.
|
|
384
418
|
*/
|
|
385
|
-
_resolveJavaPackageImport(importModule) {
|
|
419
|
+
_resolveJavaPackageImport(importModule, javaFileIndex) {
|
|
386
420
|
const isWildcard = importModule.endsWith('.*');
|
|
387
421
|
// Strip wildcard suffix (e.g., "com.pkg.Class.*" -> "com.pkg.Class")
|
|
388
422
|
const mod = isWildcard ? importModule.slice(0, -2) : importModule;
|
|
@@ -390,11 +424,28 @@ class ProjectIndex {
|
|
|
390
424
|
|
|
391
425
|
// Try progressively shorter paths: full path, then strip last segment, etc.
|
|
392
426
|
// This handles static imports where path includes member name after class
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
for (
|
|
396
|
-
|
|
397
|
-
|
|
427
|
+
if (javaFileIndex) {
|
|
428
|
+
// Fast path: use pre-built filename→files index (O(candidates) vs O(all files))
|
|
429
|
+
for (let i = segments.length; i > 0; i--) {
|
|
430
|
+
const className = segments[i - 1];
|
|
431
|
+
const candidates = javaFileIndex.get(className);
|
|
432
|
+
if (candidates) {
|
|
433
|
+
const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
|
|
434
|
+
for (const absPath of candidates) {
|
|
435
|
+
if (absPath.endsWith(fileSuffix)) {
|
|
436
|
+
return absPath;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
// Fallback: scan all files (used by imports() method outside buildImportGraph)
|
|
443
|
+
for (let i = segments.length; i > 0; i--) {
|
|
444
|
+
const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
|
|
445
|
+
for (const absPath of this.files.keys()) {
|
|
446
|
+
if (absPath.endsWith(fileSuffix)) {
|
|
447
|
+
return absPath;
|
|
448
|
+
}
|
|
398
449
|
}
|
|
399
450
|
}
|
|
400
451
|
}
|
|
@@ -420,6 +471,22 @@ class ProjectIndex {
|
|
|
420
471
|
this.importGraph.clear();
|
|
421
472
|
this.exportGraph.clear();
|
|
422
473
|
|
|
474
|
+
// Pre-build directory→files map for Go package linking (O(1) lookup vs O(n) scan)
|
|
475
|
+
const dirToGoFiles = new Map();
|
|
476
|
+
// Pre-build filename→files map for Java import resolution (O(1) vs O(n) scan)
|
|
477
|
+
const javaFileIndex = new Map();
|
|
478
|
+
for (const [fp, fe] of this.files) {
|
|
479
|
+
if (fe.language === 'go') {
|
|
480
|
+
const dir = path.dirname(fp);
|
|
481
|
+
if (!dirToGoFiles.has(dir)) dirToGoFiles.set(dir, []);
|
|
482
|
+
dirToGoFiles.get(dir).push(fp);
|
|
483
|
+
} else if (fe.language === 'java') {
|
|
484
|
+
const name = path.basename(fp, '.java');
|
|
485
|
+
if (!javaFileIndex.has(name)) javaFileIndex.set(name, []);
|
|
486
|
+
javaFileIndex.get(name).push(fp);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
423
490
|
for (const [filePath, fileEntry] of this.files) {
|
|
424
491
|
const importedFiles = [];
|
|
425
492
|
const seenModules = new Set();
|
|
@@ -442,16 +509,32 @@ class ProjectIndex {
|
|
|
442
509
|
// Java package imports: resolve by progressive suffix matching
|
|
443
510
|
// Handles regular, static (com.pkg.Class.method), and wildcard (com.pkg.Class.*) imports
|
|
444
511
|
if (!resolved && fileEntry.language === 'java' && !importModule.startsWith('.')) {
|
|
445
|
-
resolved = this._resolveJavaPackageImport(importModule);
|
|
512
|
+
resolved = this._resolveJavaPackageImport(importModule, javaFileIndex);
|
|
446
513
|
}
|
|
447
514
|
|
|
448
515
|
if (resolved && this.files.has(resolved)) {
|
|
449
|
-
|
|
516
|
+
// For Go, a package import means all files in that directory are dependencies
|
|
517
|
+
// (Go packages span multiple files in the same directory)
|
|
518
|
+
const filesToLink = [resolved];
|
|
519
|
+
if (fileEntry.language === 'go') {
|
|
520
|
+
const pkgDir = path.dirname(resolved);
|
|
521
|
+
const dirFiles = dirToGoFiles.get(pkgDir) || [];
|
|
522
|
+
const importerIsTest = filePath.endsWith('_test.go');
|
|
523
|
+
for (const fp of dirFiles) {
|
|
524
|
+
if (fp !== resolved) {
|
|
525
|
+
if (!importerIsTest && fp.endsWith('_test.go')) continue;
|
|
526
|
+
filesToLink.push(fp);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
450
530
|
|
|
451
|
-
|
|
452
|
-
|
|
531
|
+
for (const linkedFile of filesToLink) {
|
|
532
|
+
importedFiles.push(linkedFile);
|
|
533
|
+
if (!this.exportGraph.has(linkedFile)) {
|
|
534
|
+
this.exportGraph.set(linkedFile, []);
|
|
535
|
+
}
|
|
536
|
+
this.exportGraph.get(linkedFile).push(filePath);
|
|
453
537
|
}
|
|
454
|
-
this.exportGraph.get(resolved).push(filePath);
|
|
455
538
|
}
|
|
456
539
|
}
|
|
457
540
|
|
|
@@ -754,14 +837,41 @@ class ProjectIndex {
|
|
|
754
837
|
// Sort by score descending, then by index order for stability
|
|
755
838
|
scored.sort((a, b) => b.score - a.score);
|
|
756
839
|
|
|
757
|
-
// Tiebreaker: when top candidates have equal score, prefer by
|
|
840
|
+
// Tiebreaker: when top candidates have equal score, prefer by import popularity
|
|
841
|
+
// (how many files import the candidate's file), then by usage count
|
|
758
842
|
if (scored.length > 1 && scored[0].score === scored[1].score) {
|
|
759
843
|
const tiedScore = scored[0].score;
|
|
760
844
|
const tiedCandidates = scored.filter(s => s.score === tiedScore);
|
|
845
|
+
|
|
846
|
+
// Count how many files import each candidate's file (import popularity)
|
|
847
|
+
// For Go, count importers of any file in the same directory (same package)
|
|
761
848
|
for (const candidate of tiedCandidates) {
|
|
849
|
+
let importerCount = 0;
|
|
850
|
+
for (const [, importedFiles] of this.importGraph) {
|
|
851
|
+
if (importedFiles.includes(candidate.def.file)) {
|
|
852
|
+
importerCount++;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// For Go, also count importers of sibling files (same package)
|
|
856
|
+
const candidateEntry = this.files.get(candidate.def.file);
|
|
857
|
+
if (candidateEntry?.language === 'go') {
|
|
858
|
+
const candidateDir = path.dirname(candidate.def.file);
|
|
859
|
+
for (const [, importedFiles] of this.importGraph) {
|
|
860
|
+
for (const imp of importedFiles) {
|
|
861
|
+
if (imp !== candidate.def.file && path.dirname(imp) === candidateDir) {
|
|
862
|
+
importerCount++;
|
|
863
|
+
break; // count each importer once
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
candidate.importerCount = importerCount;
|
|
762
869
|
candidate.usageCount = this.countSymbolUsages(candidate.def).total;
|
|
763
870
|
}
|
|
764
|
-
|
|
871
|
+
// Sort by import popularity first, then usage count
|
|
872
|
+
tiedCandidates.sort((a, b) =>
|
|
873
|
+
(b.importerCount - a.importerCount) || (b.usageCount - a.usageCount)
|
|
874
|
+
);
|
|
765
875
|
// Rebuild scored array: sorted tied candidates first, then rest
|
|
766
876
|
const rest = scored.filter(s => s.score !== tiedScore);
|
|
767
877
|
scored.length = 0;
|
|
@@ -902,8 +1012,21 @@ class ProjectIndex {
|
|
|
902
1012
|
// 1. The file where it's defined
|
|
903
1013
|
// 2. Files that import from the definition file
|
|
904
1014
|
// 3. Transitively: files that import from re-exporters of this symbol
|
|
1015
|
+
// 4. Go: all files in the same package directory (same-package references need no import)
|
|
905
1016
|
const relevantFiles = new Set([defFile]);
|
|
906
1017
|
const queue = [defFile];
|
|
1018
|
+
|
|
1019
|
+
// Go same-package: add all .go files in the same directory
|
|
1020
|
+
const defEntry = this.files.get(defFile);
|
|
1021
|
+
if (defEntry?.language === 'go') {
|
|
1022
|
+
const pkgDir = path.dirname(defFile);
|
|
1023
|
+
for (const fp of this.files.keys()) {
|
|
1024
|
+
if (fp !== defFile && fp.endsWith('.go') && path.dirname(fp) === pkgDir) {
|
|
1025
|
+
relevantFiles.add(fp);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
907
1030
|
while (queue.length > 0) {
|
|
908
1031
|
const file = queue.pop();
|
|
909
1032
|
const importers = this.exportGraph.get(file) || [];
|
|
@@ -1059,7 +1182,7 @@ class ProjectIndex {
|
|
|
1059
1182
|
// Filters: namespace access to external packages (DropdownMenuPrimitive.Separator).
|
|
1060
1183
|
if (u.receiver && !['self', 'this', 'cls', 'super'].includes(u.receiver) &&
|
|
1061
1184
|
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
|
|
1062
|
-
const hasMethodDef =
|
|
1185
|
+
const hasMethodDef = definitions.some(d => d.className);
|
|
1063
1186
|
if (!hasMethodDef && !importedFileHasDef()) {
|
|
1064
1187
|
continue;
|
|
1065
1188
|
}
|
|
@@ -1294,6 +1417,7 @@ class ProjectIndex {
|
|
|
1294
1417
|
dynamicImports,
|
|
1295
1418
|
uncertain: stats.uncertain,
|
|
1296
1419
|
includeMethods: !!options.includeMethods,
|
|
1420
|
+
projectLanguage: this._getPredominantLanguage(),
|
|
1297
1421
|
// Structural facts for reliability hints
|
|
1298
1422
|
...(def.isMethod && { isMethod: true }),
|
|
1299
1423
|
...(def.className && { className: def.className }),
|
|
@@ -1436,7 +1560,8 @@ class ProjectIndex {
|
|
|
1436
1560
|
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
1437
1561
|
skipped: 0,
|
|
1438
1562
|
dynamicImports,
|
|
1439
|
-
uncertain: stats.uncertain
|
|
1563
|
+
uncertain: stats.uncertain,
|
|
1564
|
+
projectLanguage: this._getPredominantLanguage()
|
|
1440
1565
|
}
|
|
1441
1566
|
};
|
|
1442
1567
|
} finally { this._endOp(); }
|
|
@@ -1446,6 +1571,27 @@ class ProjectIndex {
|
|
|
1446
1571
|
// HELPER METHODS
|
|
1447
1572
|
// ========================================================================
|
|
1448
1573
|
|
|
1574
|
+
/**
|
|
1575
|
+
* Get the predominant language of the project (cached).
|
|
1576
|
+
* Returns 'go', 'javascript', etc. if >80% of files are that language.
|
|
1577
|
+
*/
|
|
1578
|
+
_getPredominantLanguage() {
|
|
1579
|
+
if (this._predominantLang !== undefined) return this._predominantLang;
|
|
1580
|
+
const counts = {};
|
|
1581
|
+
for (const [, fe] of this.files) {
|
|
1582
|
+
counts[fe.language] = (counts[fe.language] || 0) + 1;
|
|
1583
|
+
}
|
|
1584
|
+
const total = this.files.size;
|
|
1585
|
+
for (const [lang, count] of Object.entries(counts)) {
|
|
1586
|
+
if (count / total > 0.8) {
|
|
1587
|
+
this._predominantLang = lang;
|
|
1588
|
+
return lang;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
this._predominantLang = null;
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1449
1595
|
/**
|
|
1450
1596
|
* Get line content from a file
|
|
1451
1597
|
*/
|
|
@@ -2054,11 +2200,23 @@ class ProjectIndex {
|
|
|
2054
2200
|
|
|
2055
2201
|
let fileIterator;
|
|
2056
2202
|
if (filePath) {
|
|
2203
|
+
// Try exact resolution first
|
|
2057
2204
|
const resolved = this.resolveFilePathForQuery(filePath);
|
|
2058
|
-
if (typeof resolved
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2205
|
+
if (typeof resolved === 'string') {
|
|
2206
|
+
const fileEntry = this.files.get(resolved);
|
|
2207
|
+
if (!fileEntry) return { error: 'file-not-found', filePath };
|
|
2208
|
+
fileIterator = [[resolved, fileEntry]];
|
|
2209
|
+
} else {
|
|
2210
|
+
// Fall back to pattern filter (substring match on relative path)
|
|
2211
|
+
const matches = [];
|
|
2212
|
+
for (const [absPath, fe] of this.files) {
|
|
2213
|
+
if (fe.relativePath.includes(filePath)) {
|
|
2214
|
+
matches.push([absPath, fe]);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (matches.length === 0) return { error: 'file-not-found', filePath };
|
|
2218
|
+
fileIterator = matches;
|
|
2219
|
+
}
|
|
2062
2220
|
} else {
|
|
2063
2221
|
fileIterator = this.files.entries();
|
|
2064
2222
|
}
|
|
@@ -2392,6 +2550,8 @@ class ProjectIndex {
|
|
|
2392
2550
|
let evalUsage = 0;
|
|
2393
2551
|
let reflectionUsage = 0;
|
|
2394
2552
|
|
|
2553
|
+
const predominantLang = this._getPredominantLanguage();
|
|
2554
|
+
|
|
2395
2555
|
for (const [filePath, fileEntry] of this.files) {
|
|
2396
2556
|
// Skip node_modules - we don't care about their patterns
|
|
2397
2557
|
if (filePath.includes('node_modules')) continue;
|
|
@@ -2399,14 +2559,16 @@ class ProjectIndex {
|
|
|
2399
2559
|
try {
|
|
2400
2560
|
const content = this._readFile(filePath);
|
|
2401
2561
|
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2562
|
+
if (fileEntry.language !== 'go') {
|
|
2563
|
+
// Dynamic imports: import(), require(variable), __import__
|
|
2564
|
+
dynamicImports += (content.match(/import\s*\([^'"]/g) || []).length;
|
|
2565
|
+
dynamicImports += (content.match(/require\s*\([^'"]/g) || []).length;
|
|
2566
|
+
dynamicImports += (content.match(/__import__\s*\(/g) || []).length;
|
|
2406
2567
|
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2568
|
+
// eval, Function constructor
|
|
2569
|
+
evalUsage += (content.match(/(^|[^a-zA-Z_])eval\s*\(/gm) || []).length;
|
|
2570
|
+
evalUsage += (content.match(/new\s+Function\s*\(/g) || []).length;
|
|
2571
|
+
}
|
|
2410
2572
|
|
|
2411
2573
|
// Reflection: getattr, hasattr, Reflect
|
|
2412
2574
|
reflectionUsage += (content.match(/\bgetattr\s*\(/g) || []).length;
|
|
@@ -2727,7 +2889,7 @@ class ProjectIndex {
|
|
|
2727
2889
|
if (!def) {
|
|
2728
2890
|
return null;
|
|
2729
2891
|
}
|
|
2730
|
-
const defIsMethod = def.isMethod || def.type === 'method' || def.className;
|
|
2892
|
+
const defIsMethod = def.isMethod || def.type === 'method' || def.className || def.receiver;
|
|
2731
2893
|
|
|
2732
2894
|
// Use findCallers for className-scoped or method queries (sophisticated binding resolution)
|
|
2733
2895
|
// Fall back to usages-based approach for simple function queries (backward compatible)
|
|
@@ -2740,10 +2902,11 @@ class ProjectIndex {
|
|
|
2740
2902
|
targetDefinitions: [def],
|
|
2741
2903
|
});
|
|
2742
2904
|
|
|
2743
|
-
// When
|
|
2744
|
-
//
|
|
2745
|
-
//
|
|
2746
|
-
|
|
2905
|
+
// When the target definition has a className (including Go/Rust methods which
|
|
2906
|
+
// now get className from receiver), filter out method calls whose receiver
|
|
2907
|
+
// clearly belongs to a different type. This helps with common method names
|
|
2908
|
+
// like .close(), .get() etc. where many types have the same method.
|
|
2909
|
+
if (def.className) {
|
|
2747
2910
|
const targetClassName = def.className;
|
|
2748
2911
|
callerResults = callerResults.filter(c => {
|
|
2749
2912
|
// Keep non-method calls and self/this/cls calls (already resolved by findCallers)
|
|
@@ -2804,20 +2967,22 @@ class ProjectIndex {
|
|
|
2804
2967
|
}
|
|
2805
2968
|
}
|
|
2806
2969
|
}
|
|
2807
|
-
// Unique method heuristic: if the called method exists on exactly one class
|
|
2970
|
+
// Unique method heuristic: if the called method exists on exactly one class/type
|
|
2808
2971
|
// and it matches the target, include the call (no other class could match)
|
|
2809
2972
|
const methodDefs = this.symbols.get(name);
|
|
2810
2973
|
if (methodDefs) {
|
|
2811
2974
|
const classNames = new Set();
|
|
2812
2975
|
for (const d of methodDefs) {
|
|
2813
2976
|
if (d.className) classNames.add(d.className);
|
|
2977
|
+
// Go/Rust: use receiver type as className equivalent
|
|
2978
|
+
else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
|
|
2814
2979
|
}
|
|
2815
2980
|
if (classNames.size === 1 && classNames.has(targetClassName)) {
|
|
2816
2981
|
return true;
|
|
2817
2982
|
}
|
|
2818
2983
|
}
|
|
2819
|
-
//
|
|
2820
|
-
//
|
|
2984
|
+
// Type-scoped query but receiver type unknown — filter it out.
|
|
2985
|
+
// Unknown receivers are likely unrelated.
|
|
2821
2986
|
return false;
|
|
2822
2987
|
});
|
|
2823
2988
|
}
|
|
@@ -2874,11 +3039,27 @@ class ProjectIndex {
|
|
|
2874
3039
|
|
|
2875
3040
|
// Analyze each call site, filtering out method calls for non-method definitions
|
|
2876
3041
|
callSites = [];
|
|
3042
|
+
const defFileEntry = this.files.get(def.file);
|
|
3043
|
+
const defLang = defFileEntry?.language;
|
|
3044
|
+
const targetDir = defLang === 'go' ? path.basename(path.dirname(def.file)) : null;
|
|
2877
3045
|
for (const call of calls) {
|
|
2878
3046
|
const analysis = this.analyzeCallSite(call, name);
|
|
2879
3047
|
// Skip method calls (obj.parse()) when target is a standalone function (parse())
|
|
3048
|
+
// For Go, allow calls where receiver matches the package directory name
|
|
3049
|
+
// (e.g., controller.FilterActive() where file is in pkg/controller/)
|
|
2880
3050
|
if (analysis.isMethodCall && !defIsMethod) {
|
|
2881
|
-
|
|
3051
|
+
if (targetDir) {
|
|
3052
|
+
// Get receiver from parsed calls cache
|
|
3053
|
+
const parsedCalls = this.getCachedCalls(call.file);
|
|
3054
|
+
const matchedCall = parsedCalls?.find(c => c.name === name && c.line === call.line);
|
|
3055
|
+
if (matchedCall?.receiver === targetDir) {
|
|
3056
|
+
// Receiver matches package directory — keep it
|
|
3057
|
+
} else {
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
} else {
|
|
3061
|
+
continue;
|
|
3062
|
+
}
|
|
2882
3063
|
}
|
|
2883
3064
|
callSites.push({
|
|
2884
3065
|
file: call.relativePath,
|
|
@@ -3041,8 +3222,12 @@ class ProjectIndex {
|
|
|
3041
3222
|
const isMethod = !!(primary.isMethod || primary.type === 'method' || primary.className);
|
|
3042
3223
|
const includeMethods = options.includeMethods ?? isMethod;
|
|
3043
3224
|
|
|
3044
|
-
// Get usage counts by type
|
|
3045
|
-
const
|
|
3225
|
+
// Get usage counts by type (exclude test files by default, matching usages command behavior)
|
|
3226
|
+
const usageOpts = { codeOnly: true };
|
|
3227
|
+
if (!options.includeTests) {
|
|
3228
|
+
usageOpts.exclude = addTestExclusions(options.exclude);
|
|
3229
|
+
}
|
|
3230
|
+
const usages = this.usages(symbolName, usageOpts);
|
|
3046
3231
|
const usagesByType = {
|
|
3047
3232
|
definitions: usages.filter(u => u.isDefinition).length,
|
|
3048
3233
|
calls: usages.filter(u => u.usageType === 'call').length,
|
|
@@ -3056,7 +3241,9 @@ class ProjectIndex {
|
|
|
3056
3241
|
let allCallers = null;
|
|
3057
3242
|
let allCallees = null;
|
|
3058
3243
|
if (primary.type === 'function' || primary.params !== undefined) {
|
|
3059
|
-
|
|
3244
|
+
// Use maxResults to limit file iteration (with buffer for exclude filtering)
|
|
3245
|
+
const callerCap = maxCallers === Infinity ? undefined : maxCallers * 3;
|
|
3246
|
+
allCallers = this.findCallers(symbolName, { includeMethods, includeUncertain: options.includeUncertain, targetDefinitions: [primary], maxResults: callerCap });
|
|
3060
3247
|
// Apply exclude filter before slicing
|
|
3061
3248
|
if (options.exclude && options.exclude.length > 0) {
|
|
3062
3249
|
allCallers = allCallers.filter(c => this.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
@@ -3203,6 +3390,14 @@ class ProjectIndex {
|
|
|
3203
3390
|
}
|
|
3204
3391
|
|
|
3205
3392
|
for (const [filePath, fileEntry] of this.files) {
|
|
3393
|
+
// Apply --file filter
|
|
3394
|
+
if (options.file) {
|
|
3395
|
+
const fp = fileEntry.relativePath;
|
|
3396
|
+
if (!fp.includes(options.file) && !fp.endsWith(options.file)) {
|
|
3397
|
+
filesSkipped++;
|
|
3398
|
+
continue;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3206
3401
|
// Apply exclude/in filters
|
|
3207
3402
|
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
3208
3403
|
if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude, in: options.in })) {
|
|
@@ -3349,7 +3544,8 @@ class ProjectIndex {
|
|
|
3349
3544
|
symbols: totalSymbols, // Total symbol count, not unique names
|
|
3350
3545
|
buildTime: this.buildTime,
|
|
3351
3546
|
byLanguage: {},
|
|
3352
|
-
byType: {}
|
|
3547
|
+
byType: {},
|
|
3548
|
+
...(this.truncated && { truncated: this.truncated })
|
|
3353
3549
|
};
|
|
3354
3550
|
|
|
3355
3551
|
for (const [filePath, fileEntry] of this.files) {
|
|
@@ -3520,12 +3716,18 @@ class ProjectIndex {
|
|
|
3520
3716
|
displayFiles = files.slice(0, top);
|
|
3521
3717
|
}
|
|
3522
3718
|
|
|
3719
|
+
// Count files with no symbols (generated/empty files)
|
|
3720
|
+
const emptyFiles = files.filter(f => f.functions === 0 && f.classes === 0 && f.state === 0).length;
|
|
3721
|
+
|
|
3523
3722
|
return {
|
|
3524
3723
|
meta: {
|
|
3525
3724
|
complete: totalDynamic === 0,
|
|
3526
3725
|
skipped: 0,
|
|
3527
3726
|
dynamicImports: totalDynamic,
|
|
3528
|
-
uncertain: 0
|
|
3727
|
+
uncertain: 0,
|
|
3728
|
+
projectLanguage: this._getPredominantLanguage(),
|
|
3729
|
+
...(fileFilter && { filteredBy: options.file, matchedFiles: files.length }),
|
|
3730
|
+
...(emptyFiles > 0 && fileFilter && { emptyFiles })
|
|
3529
3731
|
},
|
|
3530
3732
|
totals: {
|
|
3531
3733
|
files: files.length,
|
|
@@ -3555,6 +3757,9 @@ class ProjectIndex {
|
|
|
3555
3757
|
/** Load index from cache file */
|
|
3556
3758
|
loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
|
|
3557
3759
|
|
|
3760
|
+
/** Load callsCache from separate file on demand (called by findCallers/findCallees) */
|
|
3761
|
+
loadCallsCache() { return indexCache.loadCallsCache(this); }
|
|
3762
|
+
|
|
3558
3763
|
/** Check if cache is stale (any files changed or new files added) */
|
|
3559
3764
|
isCacheStale() { return indexCache.isCacheStale(this); }
|
|
3560
3765
|
|
|
@@ -3875,7 +4080,40 @@ class ProjectIndex {
|
|
|
3875
4080
|
const allDefs = this.symbols.get(symbol.name) || [];
|
|
3876
4081
|
const targetDefs = allDefs.filter(d => d.file === change.filePath && d.startLine === symbol.startLine);
|
|
3877
4082
|
|
|
3878
|
-
|
|
4083
|
+
let callers = this.findCallers(symbol.name, {
|
|
4084
|
+
targetDefinitions: targetDefs.length > 0 ? targetDefs : undefined,
|
|
4085
|
+
includeMethods: true,
|
|
4086
|
+
includeUncertain: false,
|
|
4087
|
+
});
|
|
4088
|
+
|
|
4089
|
+
// For Go/Java/Rust methods with a className, filter callers whose
|
|
4090
|
+
// receiver clearly belongs to a different type (same logic as impact()).
|
|
4091
|
+
const targetDef = targetDefs[0] || symbol;
|
|
4092
|
+
if (targetDef.className && (lang === 'go' || lang === 'java' || lang === 'rust')) {
|
|
4093
|
+
const targetClassName = targetDef.className;
|
|
4094
|
+
callers = callers.filter(c => {
|
|
4095
|
+
if (!c.isMethod) return true;
|
|
4096
|
+
const r = c.receiver;
|
|
4097
|
+
if (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
4098
|
+
// Use receiverType from findCallers when available
|
|
4099
|
+
if (c.receiverType) {
|
|
4100
|
+
return c.receiverType === targetClassName ||
|
|
4101
|
+
c.receiverType === targetDef.receiver?.replace(/^\*/, '');
|
|
4102
|
+
}
|
|
4103
|
+
// Unique method heuristic: if the method exists on exactly one class/type, include
|
|
4104
|
+
const methodDefs = this.symbols.get(symbol.name);
|
|
4105
|
+
if (methodDefs) {
|
|
4106
|
+
const classNames = new Set();
|
|
4107
|
+
for (const d of methodDefs) {
|
|
4108
|
+
if (d.className) classNames.add(d.className);
|
|
4109
|
+
else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
|
|
4110
|
+
}
|
|
4111
|
+
if (classNames.size === 1 && classNames.has(targetClassName)) return true;
|
|
4112
|
+
}
|
|
4113
|
+
// Unknown receiver + multiple classes with this method → filter out
|
|
4114
|
+
return false;
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
3879
4117
|
|
|
3880
4118
|
for (const c of callers) {
|
|
3881
4119
|
callerFileSet.add(c.file);
|
package/core/registry.js
CHANGED
package/core/shared.js
CHANGED
|
@@ -48,6 +48,6 @@ function escapeRegExp(text) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Symbol types that are not callable (used to filter class/struct/type declarations from call analysis)
|
|
51
|
-
const NON_CALLABLE_TYPES = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'state', 'impl']);
|
|
51
|
+
const NON_CALLABLE_TYPES = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'state', 'impl', 'field']);
|
|
52
52
|
|
|
53
53
|
module.exports = { pickBestDefinition, addTestExclusions, escapeRegExp, NON_CALLABLE_TYPES };
|