ucn 3.8.20 → 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 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 = new Set(['toc', 'fn', 'class', 'find', 'usages', 'search', 'lines', 'typedef', 'api']);
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 callers of a function using AST-based detection
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 functions called by a function using AST-based detection
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
- * Check if a function is used as a callback anywhere in the codebase
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
- const isWildcard = importModule.endsWith('.*');
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
- const text = receiverNode.text;
49
- // Match named receiver: (r *Router) or (r Router[T])
50
- const namedMatch = text.match(/\(\s*\w+\s+(\*?\w+(?:\[[\w,\s]+\])?)\s*\)/);
51
- if (namedMatch) return namedMatch[1];
52
- // Match unnamed receiver: (Router) or (*Router) or (Router[T])
53
- const unnamedMatch = text.match(/\(\s*(\*?\w+(?:\[[\w,\s]+\])?)\s*\)/);
54
- if (unnamedMatch) return unnamedMatch[1];
55
- return text.replace(/^\(|\)$/g, '').trim();
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.20",
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",