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/core/project.js
CHANGED
|
@@ -23,6 +23,7 @@ const tracingModule = require('./tracing');
|
|
|
23
23
|
const searchModule = require('./search');
|
|
24
24
|
const analysisModule = require('./analysis');
|
|
25
25
|
const graphModule = require('./graph');
|
|
26
|
+
const graphBuildModule = require('./graph-build');
|
|
26
27
|
const reportingModule = require('./reporting');
|
|
27
28
|
|
|
28
29
|
// Lazy-initialized per-language keyword sets (populated on first isKeyword call)
|
|
@@ -76,6 +77,7 @@ class ProjectIndex {
|
|
|
76
77
|
this._opContentCache = new Map();
|
|
77
78
|
this._opUsagesCache = new Map();
|
|
78
79
|
this._opCallsCountCache = new Map();
|
|
80
|
+
this._opEnclosingFnCache = new Map();
|
|
79
81
|
this._opDepth = 0;
|
|
80
82
|
}
|
|
81
83
|
this._opDepth++;
|
|
@@ -83,10 +85,17 @@ class ProjectIndex {
|
|
|
83
85
|
|
|
84
86
|
/** End a per-operation content cache scope (only clears when outermost scope ends) */
|
|
85
87
|
_endOp() {
|
|
88
|
+
if (!this._opContentCache) return; // Mismatched call — no active operation
|
|
86
89
|
if (--this._opDepth <= 0) {
|
|
87
90
|
this._opContentCache = null;
|
|
88
91
|
this._opUsagesCache = null;
|
|
89
92
|
this._opCallsCountCache = null;
|
|
93
|
+
this._opEnclosingFnCache = null;
|
|
94
|
+
// Free cached file content from callsCache entries (retained during
|
|
95
|
+
// operation for _readFile caching, not needed between operations)
|
|
96
|
+
for (const entry of this.callsCache.values()) {
|
|
97
|
+
if (entry.content !== undefined) entry.content = undefined;
|
|
98
|
+
}
|
|
90
99
|
this._opDepth = 0;
|
|
91
100
|
}
|
|
92
101
|
}
|
|
@@ -473,30 +482,78 @@ class ProjectIndex {
|
|
|
473
482
|
}
|
|
474
483
|
}
|
|
475
484
|
|
|
476
|
-
//
|
|
485
|
+
// Incrementally update callee index before deleting cached calls
|
|
486
|
+
const oldCached = this.callsCache.get(filePath);
|
|
487
|
+
if (oldCached) {
|
|
488
|
+
this._removeFromCalleeIndex(filePath, oldCached.calls);
|
|
489
|
+
}
|
|
477
490
|
this.callsCache.delete(filePath);
|
|
478
491
|
|
|
479
|
-
// Invalidate callee index (will be rebuilt lazily)
|
|
480
|
-
this.calleeIndex = null;
|
|
481
|
-
|
|
482
492
|
// Invalidate attribute type cache for this file
|
|
483
493
|
if (this._attrTypeCache) this._attrTypeCache.delete(filePath);
|
|
494
|
+
|
|
495
|
+
// Invalidate lazy Java file index (will be rebuilt on next use)
|
|
496
|
+
this._javaFileIndex = null;
|
|
484
497
|
}
|
|
485
498
|
|
|
486
499
|
/**
|
|
487
500
|
* Build directory→files index for O(1) same-package lookups.
|
|
488
501
|
* Replaces O(N) full-index scans in findCallers and countSymbolUsages.
|
|
489
502
|
*/
|
|
490
|
-
_buildDirIndex() {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
503
|
+
_buildDirIndex() { graphBuildModule.buildDirIndex(this); }
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Add a file's calls to the callee index (name → Set<filePath>).
|
|
507
|
+
* Used by buildCalleeIndex (full build) and getCachedCalls (incremental update).
|
|
508
|
+
*/
|
|
509
|
+
_addToCalleeIndex(filePath, calls) {
|
|
510
|
+
if (!this.calleeIndex || !calls) return;
|
|
511
|
+
for (const call of calls) {
|
|
512
|
+
const name = call.name;
|
|
513
|
+
if (!this.calleeIndex.has(name)) {
|
|
514
|
+
this.calleeIndex.set(name, new Set());
|
|
515
|
+
}
|
|
516
|
+
this.calleeIndex.get(name).add(filePath);
|
|
517
|
+
if (call.resolvedName && call.resolvedName !== name) {
|
|
518
|
+
if (!this.calleeIndex.has(call.resolvedName)) {
|
|
519
|
+
this.calleeIndex.set(call.resolvedName, new Set());
|
|
520
|
+
}
|
|
521
|
+
this.calleeIndex.get(call.resolvedName).add(filePath);
|
|
522
|
+
}
|
|
523
|
+
if (call.resolvedNames) {
|
|
524
|
+
for (const rn of call.resolvedNames) {
|
|
525
|
+
if (rn !== name) {
|
|
526
|
+
if (!this.calleeIndex.has(rn)) {
|
|
527
|
+
this.calleeIndex.set(rn, new Set());
|
|
528
|
+
}
|
|
529
|
+
this.calleeIndex.get(rn).add(filePath);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Remove a file's calls from the callee index.
|
|
538
|
+
* Used by removeFileSymbols for incremental updates instead of full invalidation.
|
|
539
|
+
*/
|
|
540
|
+
_removeFromCalleeIndex(filePath, calls) {
|
|
541
|
+
if (!this.calleeIndex || !calls) return;
|
|
542
|
+
for (const call of calls) {
|
|
543
|
+
const removeName = (n) => {
|
|
544
|
+
const fileSet = this.calleeIndex.get(n);
|
|
545
|
+
if (fileSet) {
|
|
546
|
+
fileSet.delete(filePath);
|
|
547
|
+
if (fileSet.size === 0) this.calleeIndex.delete(n);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
removeName(call.name);
|
|
551
|
+
if (call.resolvedName && call.resolvedName !== call.name) removeName(call.resolvedName);
|
|
552
|
+
if (call.resolvedNames) {
|
|
553
|
+
for (const rn of call.resolvedNames) {
|
|
554
|
+
if (rn !== call.name) removeName(rn);
|
|
555
|
+
}
|
|
498
556
|
}
|
|
499
|
-
list.push(filePath);
|
|
500
557
|
}
|
|
501
558
|
}
|
|
502
559
|
|
|
@@ -507,37 +564,15 @@ class ProjectIndex {
|
|
|
507
564
|
*/
|
|
508
565
|
buildCalleeIndex() {
|
|
509
566
|
const { getCachedCalls } = require('./callers');
|
|
567
|
+
const { ensureCallsCacheLoaded } = require('./cache');
|
|
568
|
+
ensureCallsCacheLoaded(this);
|
|
510
569
|
this.calleeIndex = new Map();
|
|
511
570
|
|
|
512
571
|
for (const [filePath] of this.files) {
|
|
513
572
|
// Fast path: use pre-populated callsCache (avoids stat per file)
|
|
514
573
|
const cached = this.callsCache.get(filePath);
|
|
515
574
|
const calls = cached ? cached.calls : getCachedCalls(this, filePath);
|
|
516
|
-
|
|
517
|
-
for (const call of calls) {
|
|
518
|
-
const name = call.name;
|
|
519
|
-
if (!this.calleeIndex.has(name)) {
|
|
520
|
-
this.calleeIndex.set(name, new Set());
|
|
521
|
-
}
|
|
522
|
-
this.calleeIndex.get(name).add(filePath);
|
|
523
|
-
// Also index resolvedName and resolvedNames for alias resolution
|
|
524
|
-
if (call.resolvedName && call.resolvedName !== name) {
|
|
525
|
-
if (!this.calleeIndex.has(call.resolvedName)) {
|
|
526
|
-
this.calleeIndex.set(call.resolvedName, new Set());
|
|
527
|
-
}
|
|
528
|
-
this.calleeIndex.get(call.resolvedName).add(filePath);
|
|
529
|
-
}
|
|
530
|
-
if (call.resolvedNames) {
|
|
531
|
-
for (const rn of call.resolvedNames) {
|
|
532
|
-
if (rn !== name) {
|
|
533
|
-
if (!this.calleeIndex.has(rn)) {
|
|
534
|
-
this.calleeIndex.set(rn, new Set());
|
|
535
|
-
}
|
|
536
|
-
this.calleeIndex.get(rn).add(filePath);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
575
|
+
this._addToCalleeIndex(filePath, calls);
|
|
541
576
|
}
|
|
542
577
|
}
|
|
543
578
|
|
|
@@ -558,194 +593,32 @@ class ProjectIndex {
|
|
|
558
593
|
* Progressively strips trailing segments to find the class file.
|
|
559
594
|
*/
|
|
560
595
|
_resolveJavaPackageImport(importModule, javaFileIndex) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
for (let i = segments.length; i > 0; i--) {
|
|
571
|
-
const className = segments[i - 1];
|
|
572
|
-
const candidates = javaFileIndex.get(className);
|
|
573
|
-
if (candidates) {
|
|
574
|
-
const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
|
|
575
|
-
for (const absPath of candidates) {
|
|
576
|
-
if (absPath.endsWith(fileSuffix)) {
|
|
577
|
-
return absPath;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
} else {
|
|
583
|
-
// Fallback: scan all files (used by imports() method outside buildImportGraph)
|
|
584
|
-
for (let i = segments.length; i > 0; i--) {
|
|
585
|
-
const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
|
|
586
|
-
for (const absPath of this.files.keys()) {
|
|
587
|
-
if (absPath.endsWith(fileSuffix)) {
|
|
588
|
-
return absPath;
|
|
596
|
+
if (!javaFileIndex) {
|
|
597
|
+
// Lazy-build index to avoid O(N) fallback scan of all files
|
|
598
|
+
if (!this._javaFileIndex) {
|
|
599
|
+
this._javaFileIndex = new Map();
|
|
600
|
+
for (const [fp, fe] of this.files) {
|
|
601
|
+
if (fe.language === 'java') {
|
|
602
|
+
const name = path.basename(fp, '.java');
|
|
603
|
+
if (!this._javaFileIndex.has(name)) this._javaFileIndex.set(name, []);
|
|
604
|
+
this._javaFileIndex.get(name).push(fp);
|
|
589
605
|
}
|
|
590
606
|
}
|
|
591
607
|
}
|
|
608
|
+
javaFileIndex = this._javaFileIndex;
|
|
592
609
|
}
|
|
593
|
-
|
|
594
|
-
// For wildcard imports (com.pkg.model.*), the package may be a directory
|
|
595
|
-
// containing .java files. Check if any file lives under this package path.
|
|
596
|
-
if (isWildcard) {
|
|
597
|
-
const dirSuffix = '/' + segments.join('/') + '/';
|
|
598
|
-
for (const absPath of this.files.keys()) {
|
|
599
|
-
if (absPath.includes(dirSuffix)) {
|
|
600
|
-
return absPath;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return null;
|
|
610
|
+
return graphBuildModule._resolveJavaPackageImport(this, importModule, javaFileIndex);
|
|
606
611
|
}
|
|
607
612
|
|
|
608
613
|
/**
|
|
609
614
|
* Build import/export relationship graphs
|
|
610
615
|
*/
|
|
611
|
-
buildImportGraph() {
|
|
612
|
-
this.importGraph.clear();
|
|
613
|
-
this.exportGraph.clear();
|
|
614
|
-
|
|
615
|
-
// Pre-build directory→files map for Go package linking (O(1) lookup vs O(n) scan)
|
|
616
|
-
const dirToGoFiles = new Map();
|
|
617
|
-
// Pre-build filename→files map for Java import resolution (O(1) vs O(n) scan)
|
|
618
|
-
const javaFileIndex = new Map();
|
|
619
|
-
for (const [fp, fe] of this.files) {
|
|
620
|
-
if (langTraits(fe.language)?.packageScope === 'directory') {
|
|
621
|
-
const dir = path.dirname(fp);
|
|
622
|
-
if (!dirToGoFiles.has(dir)) dirToGoFiles.set(dir, []);
|
|
623
|
-
dirToGoFiles.get(dir).push(fp);
|
|
624
|
-
} else if (fe.language === 'java') {
|
|
625
|
-
const name = path.basename(fp, '.java');
|
|
626
|
-
if (!javaFileIndex.has(name)) javaFileIndex.set(name, []);
|
|
627
|
-
javaFileIndex.get(name).push(fp);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
632
|
-
const importedFiles = [];
|
|
633
|
-
const seenModules = new Set();
|
|
634
|
-
|
|
635
|
-
for (const importModule of fileEntry.imports) {
|
|
636
|
-
// Skip null modules (e.g., dynamic include! macros in Rust)
|
|
637
|
-
if (!importModule) continue;
|
|
638
|
-
|
|
639
|
-
// Deduplicate: same module imported multiple times in one file
|
|
640
|
-
// (e.g., lazy imports inside different functions)
|
|
641
|
-
if (seenModules.has(importModule)) continue;
|
|
642
|
-
seenModules.add(importModule);
|
|
643
|
-
|
|
644
|
-
let resolved = resolveImport(importModule, filePath, {
|
|
645
|
-
aliases: this.config.aliases,
|
|
646
|
-
language: fileEntry.language,
|
|
647
|
-
root: this.root
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
// Java package imports: resolve by progressive suffix matching
|
|
651
|
-
// Handles regular, static (com.pkg.Class.method), and wildcard (com.pkg.Class.*) imports
|
|
652
|
-
if (!resolved && fileEntry.language === 'java' && !importModule.startsWith('.')) {
|
|
653
|
-
resolved = this._resolveJavaPackageImport(importModule, javaFileIndex);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (resolved && this.files.has(resolved)) {
|
|
657
|
-
// For Go, a package import means all files in that directory are dependencies
|
|
658
|
-
// (Go packages span multiple files in the same directory)
|
|
659
|
-
const filesToLink = [resolved];
|
|
660
|
-
if (langTraits(fileEntry.language)?.packageScope === 'directory') {
|
|
661
|
-
const pkgDir = path.dirname(resolved);
|
|
662
|
-
const dirFiles = dirToGoFiles.get(pkgDir) || [];
|
|
663
|
-
const importerIsTest = filePath.endsWith('_test.go');
|
|
664
|
-
for (const fp of dirFiles) {
|
|
665
|
-
if (fp !== resolved) {
|
|
666
|
-
if (!importerIsTest && fp.endsWith('_test.go')) continue;
|
|
667
|
-
filesToLink.push(fp);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
for (const linkedFile of filesToLink) {
|
|
673
|
-
importedFiles.push(linkedFile);
|
|
674
|
-
if (!this.exportGraph.has(linkedFile)) {
|
|
675
|
-
this.exportGraph.set(linkedFile, []);
|
|
676
|
-
}
|
|
677
|
-
this.exportGraph.get(linkedFile).push(filePath);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
this.importGraph.set(filePath, importedFiles);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
616
|
+
buildImportGraph() { graphBuildModule.buildImportGraph(this); }
|
|
685
617
|
|
|
686
618
|
/**
|
|
687
619
|
* Build inheritance relationship graphs
|
|
688
620
|
*/
|
|
689
|
-
buildInheritanceGraph() {
|
|
690
|
-
this.extendsGraph.clear();
|
|
691
|
-
this.extendedByGraph.clear();
|
|
692
|
-
|
|
693
|
-
// Collect all class/interface/struct names for alias resolution
|
|
694
|
-
const classNames = new Set();
|
|
695
|
-
for (const [, fileEntry] of this.files) {
|
|
696
|
-
for (const symbol of fileEntry.symbols) {
|
|
697
|
-
if (['class', 'interface', 'struct', 'trait', 'record'].includes(symbol.type)) {
|
|
698
|
-
classNames.add(symbol.name);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
704
|
-
for (const symbol of fileEntry.symbols) {
|
|
705
|
-
if (!['class', 'interface', 'struct', 'trait', 'record'].includes(symbol.type)) {
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (symbol.extends) {
|
|
710
|
-
// Parse comma-separated parents (Python MRO: "Flyable, Swimmable")
|
|
711
|
-
const parents = symbol.extends.split(',').map(s => s.trim()).filter(Boolean);
|
|
712
|
-
|
|
713
|
-
// Resolve aliased parent names via import aliases
|
|
714
|
-
// e.g., const { BaseHandler: Handler } = require('./base')
|
|
715
|
-
// class Child extends Handler → resolve Handler to BaseHandler
|
|
716
|
-
const resolvedParents = parents.map(parent => {
|
|
717
|
-
if (classNames.has(parent)) return parent;
|
|
718
|
-
if (fileEntry.importAliases) {
|
|
719
|
-
const alias = fileEntry.importAliases.find(a => a.local === parent);
|
|
720
|
-
if (alias && classNames.has(alias.original)) return alias.original;
|
|
721
|
-
}
|
|
722
|
-
return parent;
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
// Store with file scope to avoid collisions when same class name
|
|
726
|
-
// appears in multiple files (F-002 fix)
|
|
727
|
-
if (!this.extendsGraph.has(symbol.name)) {
|
|
728
|
-
this.extendsGraph.set(symbol.name, []);
|
|
729
|
-
}
|
|
730
|
-
this.extendsGraph.get(symbol.name).push({
|
|
731
|
-
file: filePath,
|
|
732
|
-
parents: resolvedParents
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
for (const parent of resolvedParents) {
|
|
736
|
-
if (!this.extendedByGraph.has(parent)) {
|
|
737
|
-
this.extendedByGraph.set(parent, []);
|
|
738
|
-
}
|
|
739
|
-
this.extendedByGraph.get(parent).push({
|
|
740
|
-
name: symbol.name,
|
|
741
|
-
type: symbol.type,
|
|
742
|
-
file: filePath
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
621
|
+
buildInheritanceGraph() { graphBuildModule.buildInheritanceGraph(this); }
|
|
749
622
|
|
|
750
623
|
/**
|
|
751
624
|
* Get inheritance parents for a class, scoped by file to handle
|
|
@@ -768,7 +641,7 @@ class ProjectIndex {
|
|
|
768
641
|
if (contextFile) {
|
|
769
642
|
const imports = this.importGraph.get(contextFile);
|
|
770
643
|
if (imports) {
|
|
771
|
-
const imported = entries.find(e => imports.
|
|
644
|
+
const imported = entries.find(e => imports.has(e.file));
|
|
772
645
|
if (imported) return imported.parents;
|
|
773
646
|
}
|
|
774
647
|
}
|
|
@@ -800,7 +673,7 @@ class ProjectIndex {
|
|
|
800
673
|
if (contextFile) {
|
|
801
674
|
const imports = this.importGraph.get(contextFile);
|
|
802
675
|
if (imports) {
|
|
803
|
-
const imported = classSymbols.find(s => imports.
|
|
676
|
+
const imported = classSymbols.find(s => imports.has(s.file));
|
|
804
677
|
if (imported) return imported.file;
|
|
805
678
|
}
|
|
806
679
|
}
|
|
@@ -1010,7 +883,7 @@ class ProjectIndex {
|
|
|
1010
883
|
for (const candidate of tiedCandidates) {
|
|
1011
884
|
let importerCount = 0;
|
|
1012
885
|
for (const [, importedFiles] of this.importGraph) {
|
|
1013
|
-
if (importedFiles.
|
|
886
|
+
if (importedFiles.has(candidate.def.file)) {
|
|
1014
887
|
importerCount++;
|
|
1015
888
|
}
|
|
1016
889
|
}
|
|
@@ -1085,8 +958,7 @@ class ProjectIndex {
|
|
|
1085
958
|
const hasFilters = options.exclude && options.exclude.length > 0;
|
|
1086
959
|
|
|
1087
960
|
// Pre-compute which files can reference THIS specific definition
|
|
1088
|
-
const
|
|
1089
|
-
const importersSet = new Set(importers);
|
|
961
|
+
const importersSet = this.exportGraph.get(defFile) || new Set();
|
|
1090
962
|
const defEntry = this.files.get(defFile);
|
|
1091
963
|
const isDirectoryScope = langTraits(defEntry?.language)?.packageScope === 'directory';
|
|
1092
964
|
const defDir = isDirectoryScope ? path.dirname(defFile) : null;
|
|
@@ -1149,7 +1021,7 @@ class ProjectIndex {
|
|
|
1149
1021
|
|
|
1150
1022
|
// Count imports from import graph (files that import from defFile and use this name)
|
|
1151
1023
|
let imports = 0;
|
|
1152
|
-
for (const importer of
|
|
1024
|
+
for (const importer of importersSet) {
|
|
1153
1025
|
const fe = this.files.get(importer);
|
|
1154
1026
|
if (!fe) continue;
|
|
1155
1027
|
if (hasFilters && !this.matchesFilters(fe.relativePath, { exclude: options.exclude })) continue;
|
|
@@ -1208,7 +1080,7 @@ class ProjectIndex {
|
|
|
1208
1080
|
|
|
1209
1081
|
while (queue.length > 0) {
|
|
1210
1082
|
const file = queue.pop();
|
|
1211
|
-
const importersArr = this.exportGraph.get(file) ||
|
|
1083
|
+
const importersArr = this.exportGraph.get(file) || new Set();
|
|
1212
1084
|
for (const importer of importersArr) {
|
|
1213
1085
|
if (!relevantFiles.has(importer)) {
|
|
1214
1086
|
relevantFiles.add(importer);
|
|
@@ -1734,6 +1606,15 @@ class ProjectIndex {
|
|
|
1734
1606
|
const fileEntry = this.files.get(filePath);
|
|
1735
1607
|
if (!fileEntry) return null;
|
|
1736
1608
|
|
|
1609
|
+
// Per-operation cache: avoid rescanning symbols for same (file, line)
|
|
1610
|
+
const cacheKey = filePath + '\0' + lineNum;
|
|
1611
|
+
if (this._opEnclosingFnCache) {
|
|
1612
|
+
const cached = this._opEnclosingFnCache.get(cacheKey);
|
|
1613
|
+
if (cached !== undefined) {
|
|
1614
|
+
return cached === null ? null : (returnSymbol ? cached : cached.name);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1737
1618
|
let best = null;
|
|
1738
1619
|
for (const symbol of fileEntry.symbols) {
|
|
1739
1620
|
if (!NON_CALLABLE_TYPES.has(symbol.type) &&
|
|
@@ -1744,8 +1625,11 @@ class ProjectIndex {
|
|
|
1744
1625
|
}
|
|
1745
1626
|
}
|
|
1746
1627
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1628
|
+
|
|
1629
|
+
if (this._opEnclosingFnCache) {
|
|
1630
|
+
this._opEnclosingFnCache.set(cacheKey, best);
|
|
1631
|
+
}
|
|
1632
|
+
return best ? (returnSymbol ? best : best.name) : null;
|
|
1749
1633
|
}
|
|
1750
1634
|
|
|
1751
1635
|
/** Get instance attribute types for a class in a file */
|
package/core/registry.js
CHANGED
|
@@ -143,6 +143,10 @@ const BROAD_COMMANDS = new Set([
|
|
|
143
143
|
'deadcode', 'usages', 'reverseTrace', 'circularDeps',
|
|
144
144
|
]);
|
|
145
145
|
|
|
146
|
+
// Commands that can operate on a single file without a project index.
|
|
147
|
+
// Used by CLI to decide whether to build a file-local or project-wide index.
|
|
148
|
+
const FILE_LOCAL_COMMANDS = new Set(['toc', 'fn', 'class', 'find', 'usages', 'search', 'lines', 'typedef', 'api']);
|
|
149
|
+
|
|
146
150
|
// ============================================================================
|
|
147
151
|
// HELPERS
|
|
148
152
|
// ============================================================================
|
|
@@ -263,6 +267,7 @@ module.exports = {
|
|
|
263
267
|
REVERSE_PARAM_MAP,
|
|
264
268
|
FLAG_APPLICABILITY,
|
|
265
269
|
BROAD_COMMANDS,
|
|
270
|
+
FILE_LOCAL_COMMANDS,
|
|
266
271
|
resolveCommand,
|
|
267
272
|
normalizeParams,
|
|
268
273
|
getCliCommandSet,
|
package/core/search.js
CHANGED
|
@@ -221,11 +221,15 @@ function usages(index, name, options = {}) {
|
|
|
221
221
|
let _importedHasDef = null;
|
|
222
222
|
const importedFileHasDef = () => {
|
|
223
223
|
if (_importedHasDef !== null) return _importedHasDef;
|
|
224
|
-
const importedFiles = index.importGraph.get(filePath)
|
|
225
|
-
_importedHasDef =
|
|
224
|
+
const importedFiles = index.importGraph.get(filePath);
|
|
225
|
+
_importedHasDef = false;
|
|
226
|
+
if (importedFiles) for (const imp of importedFiles) {
|
|
226
227
|
const impEntry = index.files.get(imp);
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if (impEntry?.symbols?.some(s => s.name === name)) {
|
|
229
|
+
_importedHasDef = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
229
233
|
return _importedHasDef;
|
|
230
234
|
};
|
|
231
235
|
|
|
@@ -1008,7 +1012,7 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1008
1012
|
|
|
1009
1013
|
while (queue.length > 0) {
|
|
1010
1014
|
const current = queue.shift();
|
|
1011
|
-
const directImporters = index.exportGraph?.get(current) ||
|
|
1015
|
+
const directImporters = index.exportGraph?.get(current) || new Set();
|
|
1012
1016
|
for (const imp of directImporters) {
|
|
1013
1017
|
importers.add(imp);
|
|
1014
1018
|
// Check if this importer re-exports the symbol (barrel pattern).
|
package/core/tracing.js
CHANGED
|
@@ -435,13 +435,22 @@ function reverseTrace(index, name, options = {}) {
|
|
|
435
435
|
if (callerEntries.length > maxChildren) {
|
|
436
436
|
node.truncatedChildren = callerEntries.length - maxChildren;
|
|
437
437
|
// Count entry points in truncated branches so summary is accurate
|
|
438
|
+
// Use callerCache to avoid redundant findCallers calls
|
|
438
439
|
for (const { def: cDef } of callerEntries.slice(maxChildren)) {
|
|
439
|
-
const
|
|
440
|
-
if (!visited.has(
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
440
|
+
const cKey = `${cDef.file}:${cDef.startLine}`;
|
|
441
|
+
if (!visited.has(cKey)) {
|
|
442
|
+
const cCacheKey = cDef.bindingId
|
|
443
|
+
? `${cDef.name}:${cDef.bindingId}`
|
|
444
|
+
: `${cDef.name}:${cKey}`;
|
|
445
|
+
let cCallers = callerCache.get(cCacheKey);
|
|
446
|
+
if (!cCallers) {
|
|
447
|
+
cCallers = index.findCallers(cDef.name, {
|
|
448
|
+
includeMethods, includeUncertain,
|
|
449
|
+
targetDefinitions: cDef.bindingId ? [cDef] : undefined,
|
|
450
|
+
maxResults: 1, // Only need to know if any exist
|
|
451
|
+
});
|
|
452
|
+
callerCache.set(cCacheKey, cCallers);
|
|
453
|
+
}
|
|
445
454
|
if (cCallers.length === 0) {
|
|
446
455
|
entryPoints.push({ name: cDef.name, file: cDef.relativePath || path.relative(index.root, cDef.file), line: cDef.startLine });
|
|
447
456
|
}
|
package/languages/go.js
CHANGED
|
@@ -45,14 +45,14 @@ function extractGoParams(paramsNode) {
|
|
|
45
45
|
*/
|
|
46
46
|
function extractReceiver(receiverNode) {
|
|
47
47
|
if (!receiverNode) return null;
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
return text
|
|
48
|
+
// receiverNode is a parameter_list: (r *Router)
|
|
49
|
+
// Find the parameter_declaration child
|
|
50
|
+
const param = receiverNode.namedChildren.find(c => c.type === 'parameter_declaration');
|
|
51
|
+
if (!param) return receiverNode.text.replace(/^\(|\)$/g, '').trim();
|
|
52
|
+
// The type is the last named child (name is first for named receivers)
|
|
53
|
+
const typeNode = param.namedChildren[param.namedChildren.length - 1];
|
|
54
|
+
if (!typeNode) return null;
|
|
55
|
+
return typeNode.text;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// --- Single-pass helpers: extracted from find* callbacks ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.23",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"ucn-mcp": "mcp/server.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/regression-mcp.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js",
|
|
12
|
+
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/regression-mcp.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js",
|
|
13
13
|
"benchmark:agent": "node test/agent-understanding-benchmark.js",
|
|
14
14
|
"lint": "eslint core/ cli/ mcp/ languages/"
|
|
15
15
|
},
|