ucn 3.8.21 → 3.8.22
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/callers.js +26 -3
- package/core/graph-build.js +224 -0
- package/core/project.js +5 -191
- package/core/registry.js +5 -0
- package/languages/go.js +8 -8
- package/package.json +1 -1
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/callers.js
CHANGED
|
@@ -109,7 +109,14 @@ function getCachedCalls(index, filePath, options = {}) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Find all
|
|
112
|
+
* Find all call sites that invoke the named symbol.
|
|
113
|
+
*
|
|
114
|
+
* ReceiverType filtering (nominal vs structural):
|
|
115
|
+
* - Nominal languages (Go/Java/Rust): uses call.receiverType (from parser-inferred
|
|
116
|
+
* method receivers, constructors, composite literals) to filter false positives.
|
|
117
|
+
* - Structural languages (JS/TS/Python): checks receiver binding evidence from imports
|
|
118
|
+
* instead of receiverType, since structural typing makes receiver types ambiguous.
|
|
119
|
+
*
|
|
113
120
|
* @param {object} index - ProjectIndex instance
|
|
114
121
|
* @param {string} name - Function name to find callers for
|
|
115
122
|
* @param {object} [options] - Options
|
|
@@ -652,7 +659,15 @@ function findCallers(index, name, options = {}) {
|
|
|
652
659
|
}
|
|
653
660
|
|
|
654
661
|
/**
|
|
655
|
-
* Find all
|
|
662
|
+
* Find all symbols called from within a function definition.
|
|
663
|
+
*
|
|
664
|
+
* Method resolution uses receiverType when available:
|
|
665
|
+
* - Go: receiverType from method receiver params + _buildTypedLocalTypeMap (New*() patterns)
|
|
666
|
+
* - Java: receiverType from `new Foo()` constructors + typed parameter declarations
|
|
667
|
+
* - Rust: receiverType from impl block context + _buildTypedLocalTypeMap
|
|
668
|
+
* - JS/TS: receiverType from constructor calls + import binding evidence
|
|
669
|
+
* - Python: receiverType from __init__ attribute type inference (getInstanceAttributeTypes)
|
|
670
|
+
*
|
|
656
671
|
* @param {object} index - ProjectIndex instance
|
|
657
672
|
* @param {object} def - Symbol definition with file, name, startLine, endLine
|
|
658
673
|
* @param {object} [options] - Options
|
|
@@ -1395,6 +1410,11 @@ function _buildLocalTypeMap(index, def, calls) {
|
|
|
1395
1410
|
* @param {object} index - ProjectIndex instance
|
|
1396
1411
|
* @param {object} def - Function definition with file, startLine, endLine
|
|
1397
1412
|
* @param {Array} calls - Cached call sites for the file
|
|
1413
|
+
*
|
|
1414
|
+
* Sources: parser-inferred receiverType from method receivers, constructor calls,
|
|
1415
|
+
* composite literals. Used by Go, Java, Rust (nominal languages) to infer local
|
|
1416
|
+
* variable types for method resolution. Not used by JS/TS/Python -- structural
|
|
1417
|
+
* languages use import evidence via _buildLocalTypeMap instead.
|
|
1398
1418
|
*/
|
|
1399
1419
|
function _buildTypedLocalTypeMap(index, def, calls) {
|
|
1400
1420
|
const localTypes = new Map();
|
|
@@ -1437,7 +1457,10 @@ function _buildTypedLocalTypeMap(index, def, calls) {
|
|
|
1437
1457
|
}
|
|
1438
1458
|
|
|
1439
1459
|
/**
|
|
1440
|
-
*
|
|
1460
|
+
* Find higher-order function usages where `name` is passed as a callback argument.
|
|
1461
|
+
* Handles patterns like .map(fn), setTimeout(fn), promise.then(handler).
|
|
1462
|
+
* Delegates to per-language findCallbackUsages implementations.
|
|
1463
|
+
*
|
|
1441
1464
|
* @param {object} index - ProjectIndex instance
|
|
1442
1465
|
* @param {string} name - Function name
|
|
1443
1466
|
* @returns {Array} Callback usages
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/graph-build.js - Import/export and inheritance graph construction
|
|
3
|
+
*
|
|
4
|
+
* Extracted from project.js. All functions take an `index` (ProjectIndex)
|
|
5
|
+
* as the first argument instead of using `this`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { resolveImport } = require('./imports');
|
|
10
|
+
const { langTraits } = require('../languages');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build directory→files index for O(1) same-package lookups.
|
|
14
|
+
* Replaces O(N) full-index scans in findCallers and countSymbolUsages.
|
|
15
|
+
*/
|
|
16
|
+
function buildDirIndex(index) {
|
|
17
|
+
index.dirToFiles = new Map();
|
|
18
|
+
for (const filePath of index.files.keys()) {
|
|
19
|
+
const dir = path.dirname(filePath);
|
|
20
|
+
let list = index.dirToFiles.get(dir);
|
|
21
|
+
if (!list) {
|
|
22
|
+
list = [];
|
|
23
|
+
index.dirToFiles.set(dir, list);
|
|
24
|
+
}
|
|
25
|
+
list.push(filePath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a Java package import to a project file.
|
|
31
|
+
* Handles regular imports, static imports (strips member name), and wildcards (strips .*).
|
|
32
|
+
* Progressively strips trailing segments to find the class file.
|
|
33
|
+
*/
|
|
34
|
+
function _resolveJavaPackageImport(index, importModule, javaFileIndex) {
|
|
35
|
+
const isWildcard = importModule.endsWith('.*');
|
|
36
|
+
// Strip wildcard suffix (e.g., "com.pkg.Class.*" -> "com.pkg.Class")
|
|
37
|
+
const mod = isWildcard ? importModule.slice(0, -2) : importModule;
|
|
38
|
+
const segments = mod.split('.');
|
|
39
|
+
|
|
40
|
+
// Try progressively shorter paths: full path, then strip last segment, etc.
|
|
41
|
+
// This handles static imports where path includes member name after class
|
|
42
|
+
if (javaFileIndex) {
|
|
43
|
+
// Fast path: use pre-built filename→files index (O(candidates) vs O(all files))
|
|
44
|
+
for (let i = segments.length; i > 0; i--) {
|
|
45
|
+
const className = segments[i - 1];
|
|
46
|
+
const candidates = javaFileIndex.get(className);
|
|
47
|
+
if (candidates) {
|
|
48
|
+
const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
|
|
49
|
+
for (const absPath of candidates) {
|
|
50
|
+
if (absPath.endsWith(fileSuffix)) {
|
|
51
|
+
return absPath;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// Fallback: scan all files (used by imports() method outside buildImportGraph)
|
|
58
|
+
for (let i = segments.length; i > 0; i--) {
|
|
59
|
+
const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
|
|
60
|
+
for (const absPath of index.files.keys()) {
|
|
61
|
+
if (absPath.endsWith(fileSuffix)) {
|
|
62
|
+
return absPath;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// For wildcard imports (com.pkg.model.*), the package may be a directory
|
|
69
|
+
// containing .java files. Check if any file lives under this package path.
|
|
70
|
+
if (isWildcard) {
|
|
71
|
+
const dirSuffix = '/' + segments.join('/') + '/';
|
|
72
|
+
for (const absPath of index.files.keys()) {
|
|
73
|
+
if (absPath.includes(dirSuffix)) {
|
|
74
|
+
return absPath;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build import/export relationship graphs
|
|
84
|
+
*/
|
|
85
|
+
function buildImportGraph(index) {
|
|
86
|
+
index.importGraph.clear();
|
|
87
|
+
index.exportGraph.clear();
|
|
88
|
+
|
|
89
|
+
// Pre-build directory→files map for Go package linking (O(1) lookup vs O(n) scan)
|
|
90
|
+
const dirToGoFiles = new Map();
|
|
91
|
+
// Pre-build filename→files map for Java import resolution (O(1) vs O(n) scan)
|
|
92
|
+
const javaFileIndex = new Map();
|
|
93
|
+
for (const [fp, fe] of index.files) {
|
|
94
|
+
if (langTraits(fe.language)?.packageScope === 'directory') {
|
|
95
|
+
const dir = path.dirname(fp);
|
|
96
|
+
if (!dirToGoFiles.has(dir)) dirToGoFiles.set(dir, []);
|
|
97
|
+
dirToGoFiles.get(dir).push(fp);
|
|
98
|
+
} else if (fe.language === 'java') {
|
|
99
|
+
const name = path.basename(fp, '.java');
|
|
100
|
+
if (!javaFileIndex.has(name)) javaFileIndex.set(name, []);
|
|
101
|
+
javaFileIndex.get(name).push(fp);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
106
|
+
const importedFiles = [];
|
|
107
|
+
const seenModules = new Set();
|
|
108
|
+
|
|
109
|
+
for (const importModule of fileEntry.imports) {
|
|
110
|
+
// Skip null modules (e.g., dynamic include! macros in Rust)
|
|
111
|
+
if (!importModule) continue;
|
|
112
|
+
|
|
113
|
+
// Deduplicate: same module imported multiple times in one file
|
|
114
|
+
// (e.g., lazy imports inside different functions)
|
|
115
|
+
if (seenModules.has(importModule)) continue;
|
|
116
|
+
seenModules.add(importModule);
|
|
117
|
+
|
|
118
|
+
let resolved = resolveImport(importModule, filePath, {
|
|
119
|
+
aliases: index.config.aliases,
|
|
120
|
+
language: fileEntry.language,
|
|
121
|
+
root: index.root
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Java package imports: resolve by progressive suffix matching
|
|
125
|
+
// Handles regular, static (com.pkg.Class.method), and wildcard (com.pkg.Class.*) imports
|
|
126
|
+
if (!resolved && fileEntry.language === 'java' && !importModule.startsWith('.')) {
|
|
127
|
+
resolved = _resolveJavaPackageImport(index, importModule, javaFileIndex);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (resolved && index.files.has(resolved)) {
|
|
131
|
+
// For Go, a package import means all files in that directory are dependencies
|
|
132
|
+
// (Go packages span multiple files in the same directory)
|
|
133
|
+
const filesToLink = [resolved];
|
|
134
|
+
if (langTraits(fileEntry.language)?.packageScope === 'directory') {
|
|
135
|
+
const pkgDir = path.dirname(resolved);
|
|
136
|
+
const dirFiles = dirToGoFiles.get(pkgDir) || [];
|
|
137
|
+
const importerIsTest = filePath.endsWith('_test.go');
|
|
138
|
+
for (const fp of dirFiles) {
|
|
139
|
+
if (fp !== resolved) {
|
|
140
|
+
if (!importerIsTest && fp.endsWith('_test.go')) continue;
|
|
141
|
+
filesToLink.push(fp);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const linkedFile of filesToLink) {
|
|
147
|
+
importedFiles.push(linkedFile);
|
|
148
|
+
if (!index.exportGraph.has(linkedFile)) {
|
|
149
|
+
index.exportGraph.set(linkedFile, []);
|
|
150
|
+
}
|
|
151
|
+
index.exportGraph.get(linkedFile).push(filePath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
index.importGraph.set(filePath, importedFiles);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build inheritance relationship graphs
|
|
162
|
+
*/
|
|
163
|
+
function buildInheritanceGraph(index) {
|
|
164
|
+
index.extendsGraph.clear();
|
|
165
|
+
index.extendedByGraph.clear();
|
|
166
|
+
|
|
167
|
+
// Collect all class/interface/struct names for alias resolution
|
|
168
|
+
const classNames = new Set();
|
|
169
|
+
for (const [, fileEntry] of index.files) {
|
|
170
|
+
for (const symbol of fileEntry.symbols) {
|
|
171
|
+
if (['class', 'interface', 'struct', 'trait', 'record'].includes(symbol.type)) {
|
|
172
|
+
classNames.add(symbol.name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
178
|
+
for (const symbol of fileEntry.symbols) {
|
|
179
|
+
if (!['class', 'interface', 'struct', 'trait', 'record'].includes(symbol.type)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (symbol.extends) {
|
|
184
|
+
// Parse comma-separated parents (Python MRO: "Flyable, Swimmable")
|
|
185
|
+
const parents = symbol.extends.split(',').map(s => s.trim()).filter(Boolean);
|
|
186
|
+
|
|
187
|
+
// Resolve aliased parent names via import aliases
|
|
188
|
+
// e.g., const { BaseHandler: Handler } = require('./base')
|
|
189
|
+
// class Child extends Handler → resolve Handler to BaseHandler
|
|
190
|
+
const resolvedParents = parents.map(parent => {
|
|
191
|
+
if (classNames.has(parent)) return parent;
|
|
192
|
+
if (fileEntry.importAliases) {
|
|
193
|
+
const alias = fileEntry.importAliases.find(a => a.local === parent);
|
|
194
|
+
if (alias && classNames.has(alias.original)) return alias.original;
|
|
195
|
+
}
|
|
196
|
+
return parent;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Store with file scope to avoid collisions when same class name
|
|
200
|
+
// appears in multiple files (F-002 fix)
|
|
201
|
+
if (!index.extendsGraph.has(symbol.name)) {
|
|
202
|
+
index.extendsGraph.set(symbol.name, []);
|
|
203
|
+
}
|
|
204
|
+
index.extendsGraph.get(symbol.name).push({
|
|
205
|
+
file: filePath,
|
|
206
|
+
parents: resolvedParents
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
for (const parent of resolvedParents) {
|
|
210
|
+
if (!index.extendedByGraph.has(parent)) {
|
|
211
|
+
index.extendedByGraph.set(parent, []);
|
|
212
|
+
}
|
|
213
|
+
index.extendedByGraph.get(parent).push({
|
|
214
|
+
name: symbol.name,
|
|
215
|
+
type: symbol.type,
|
|
216
|
+
file: filePath
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, _resolveJavaPackageImport };
|
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)
|
|
@@ -487,18 +488,7 @@ class ProjectIndex {
|
|
|
487
488
|
* Build directory→files index for O(1) same-package lookups.
|
|
488
489
|
* Replaces O(N) full-index scans in findCallers and countSymbolUsages.
|
|
489
490
|
*/
|
|
490
|
-
_buildDirIndex() {
|
|
491
|
-
this.dirToFiles = new Map();
|
|
492
|
-
for (const filePath of this.files.keys()) {
|
|
493
|
-
const dir = path.dirname(filePath);
|
|
494
|
-
let list = this.dirToFiles.get(dir);
|
|
495
|
-
if (!list) {
|
|
496
|
-
list = [];
|
|
497
|
-
this.dirToFiles.set(dir, list);
|
|
498
|
-
}
|
|
499
|
-
list.push(filePath);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
491
|
+
_buildDirIndex() { graphBuildModule.buildDirIndex(this); }
|
|
502
492
|
|
|
503
493
|
/**
|
|
504
494
|
* Build inverted call index: callee name -> Set<filePath>.
|
|
@@ -558,194 +548,18 @@ class ProjectIndex {
|
|
|
558
548
|
* Progressively strips trailing segments to find the class file.
|
|
559
549
|
*/
|
|
560
550
|
_resolveJavaPackageImport(importModule, javaFileIndex) {
|
|
561
|
-
|
|
562
|
-
// Strip wildcard suffix (e.g., "com.pkg.Class.*" -> "com.pkg.Class")
|
|
563
|
-
const mod = isWildcard ? importModule.slice(0, -2) : importModule;
|
|
564
|
-
const segments = mod.split('.');
|
|
565
|
-
|
|
566
|
-
// Try progressively shorter paths: full path, then strip last segment, etc.
|
|
567
|
-
// This handles static imports where path includes member name after class
|
|
568
|
-
if (javaFileIndex) {
|
|
569
|
-
// Fast path: use pre-built filename→files index (O(candidates) vs O(all files))
|
|
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;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
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;
|
|
551
|
+
return graphBuildModule._resolveJavaPackageImport(this, importModule, javaFileIndex);
|
|
606
552
|
}
|
|
607
553
|
|
|
608
554
|
/**
|
|
609
555
|
* Build import/export relationship graphs
|
|
610
556
|
*/
|
|
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
|
-
}
|
|
557
|
+
buildImportGraph() { graphBuildModule.buildImportGraph(this); }
|
|
685
558
|
|
|
686
559
|
/**
|
|
687
560
|
* Build inheritance relationship graphs
|
|
688
561
|
*/
|
|
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
|
-
}
|
|
562
|
+
buildInheritanceGraph() { graphBuildModule.buildInheritanceGraph(this); }
|
|
749
563
|
|
|
750
564
|
/**
|
|
751
565
|
* Get inheritance parents for a class, scoped by file to handle
|
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/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.22",
|
|
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",
|