ucn 3.7.46 → 3.8.0

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/core/callers.js CHANGED
@@ -12,6 +12,22 @@ const { detectLanguage, getParser, getLanguageModule } = require('../languages')
12
12
  const { isTestFile } = require('./discovery');
13
13
  const { NON_CALLABLE_TYPES } = require('./shared');
14
14
 
15
+ /**
16
+ * Extract a single line from content without splitting the entire string.
17
+ * @param {string} content - Full file content
18
+ * @param {number} lineNum - 1-indexed line number
19
+ * @returns {string} The line content
20
+ */
21
+ function getLine(content, lineNum) {
22
+ let start = 0;
23
+ for (let i = 1; i < lineNum; i++) {
24
+ start = content.indexOf('\n', start) + 1;
25
+ if (start === 0) return ''; // past end
26
+ }
27
+ const end = content.indexOf('\n', start);
28
+ return end === -1 ? content.slice(start) : content.slice(start, end);
29
+ }
30
+
15
31
  /**
16
32
  * Get cached call sites for a file, with mtime/hash validation
17
33
  * Uses mtime for fast cache validation, falls back to hash if mtime matches but content changed
@@ -63,7 +79,16 @@ function getCachedCalls(index, filePath, options = {}) {
63
79
  if (!langModule.findCallsInCode) return null;
64
80
 
65
81
  const parser = getParser(language);
66
- const calls = langModule.findCallsInCode(content, parser);
82
+ // Pass import alias names to Go parser for package vs method call disambiguation
83
+ // importNames contains resolved alias names (e.g., 'utilversion' for renamed imports)
84
+ const callOpts = {};
85
+ if (language === 'go') {
86
+ const fileEntry = index.files.get(filePath);
87
+ if (fileEntry?.importNames) {
88
+ callOpts.imports = fileEntry.importNames;
89
+ }
90
+ }
91
+ const calls = langModule.findCallsInCode(content, parser, callOpts);
67
92
 
68
93
  index.callsCache.set(filePath, {
69
94
  mtime,
@@ -92,6 +117,9 @@ function getCachedCalls(index, filePath, options = {}) {
92
117
  function findCallers(index, name, options = {}) {
93
118
  index._beginOp();
94
119
  try {
120
+ // Lazy-load callsCache from disk if not already populated
121
+ if (index.loadCallsCache) index.loadCallsCache();
122
+
95
123
  const callers = [];
96
124
  const stats = options.stats;
97
125
 
@@ -102,13 +130,24 @@ function findCallers(index, name, options = {}) {
102
130
  definitionLines.add(`${def.file}:${def.startLine}`);
103
131
  }
104
132
 
105
- for (const [filePath, fileEntry] of index.files) {
133
+ // Phase 1: Find matching calls without reading file content.
134
+ // Collect pending callers keyed by file — content is read only in Phase 2.
135
+ const pendingByFile = new Map(); // filePath -> [{ call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver }]
136
+ let pendingCount = 0;
137
+ const maxResults = options.maxResults;
138
+
139
+ // Use inverted callee index to skip files that don't contain calls to this name
140
+ const calleeFiles = index.getCalleeFiles(name);
141
+ const fileIterator = calleeFiles
142
+ ? [...calleeFiles].map(fp => [fp, index.files.get(fp)]).filter(([, fe]) => fe)
143
+ : index.files;
144
+
145
+ for (const [filePath, fileEntry] of fileIterator) {
146
+ // Early exit when maxResults is reached
147
+ if (maxResults && pendingCount >= maxResults) break;
106
148
  try {
107
- const result = getCachedCalls(index, filePath, { includeContent: true });
108
- if (!result) continue;
109
-
110
- const { calls, content } = result;
111
- const lines = content.split('\n');
149
+ const calls = getCachedCalls(index, filePath);
150
+ if (!calls) continue;
112
151
 
113
152
  for (const call of calls) {
114
153
  // Skip if not matching our target name (also check alias resolution)
@@ -120,31 +159,53 @@ function findCallers(index, name, options = {}) {
120
159
  if (call.isPotentialCallback) {
121
160
  const syms = definitions;
122
161
  if (!syms || syms.length === 0) continue;
162
+
163
+ // Go unexported visibility: lowercase functions are package-private.
164
+ // Only allow callers from the same package directory.
165
+ if (fileEntry.language === 'go' && /^[a-z]/.test(name)) {
166
+ const targetDefs = options.targetDefinitions || definitions;
167
+ const targetPkgDirs = new Set(
168
+ targetDefs.filter(d => d.file).map(d => path.dirname(d.file))
169
+ );
170
+ if (targetPkgDirs.size > 0 && !targetPkgDirs.has(path.dirname(filePath))) {
171
+ continue;
172
+ }
173
+ }
174
+
175
+ // Go/Java/Rust receiver disambiguation for callbacks (e.g. dc.worker)
176
+ if (call.isMethod && call.receiver &&
177
+ (fileEntry.language === 'go' || fileEntry.language === 'java' || fileEntry.language === 'rust')) {
178
+ const targetDefs = options.targetDefinitions || definitions;
179
+ const targetTypes = new Set();
180
+ for (const td of targetDefs) {
181
+ if (td.className) targetTypes.add(td.className);
182
+ if (td.receiver) targetTypes.add(td.receiver.replace(/^\*/, ''));
183
+ }
184
+ if (targetTypes.size > 0 && call.receiverType) {
185
+ if (!targetTypes.has(call.receiverType)) continue;
186
+ }
187
+ }
188
+
123
189
  // Find the enclosing function
124
190
  const callerSymbol = index.findEnclosingFunction(filePath, call.line, true);
125
- callers.push({
126
- file: filePath,
127
- relativePath: fileEntry.relativePath,
128
- line: call.line,
129
- content: lines[call.line - 1] || '',
130
- callerName: callerSymbol ? callerSymbol.name : null,
131
- callerFile: callerSymbol ? filePath : null,
132
- callerStartLine: callerSymbol ? callerSymbol.startLine : null,
133
- callerEndLine: callerSymbol ? callerSymbol.endLine : null,
134
- isMethod: false,
135
- isFunctionReference: true
191
+ if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
192
+ pendingByFile.get(filePath).push({
193
+ call, fileEntry, callerSymbol,
194
+ isMethod: false, isFunctionReference: true, receiver: undefined
136
195
  });
196
+ pendingCount++;
137
197
  continue;
138
198
  }
139
199
 
140
200
  // Resolve binding within this file (without mutating cached call objects)
141
201
  let bindingId = call.bindingId;
142
202
  let isUncertain = call.uncertain;
143
- // Skip binding resolution for method calls with non-self/this/cls receivers:
203
+ // Skip binding resolution for calls with non-self/this/cls receivers:
144
204
  // e.g., analyzer.analyze_instrument() should NOT resolve to a local
145
205
  // standalone function def `analyze_instrument` — they're different symbols.
206
+ // Also skip for Go package-qualified calls (isMethod:false but has receiver like 'cli')
146
207
  const selfReceivers = new Set(['self', 'cls', 'this', 'super']);
147
- const skipLocalBinding = call.isMethod && call.receiver && !selfReceivers.has(call.receiver);
208
+ const skipLocalBinding = call.receiver && !selfReceivers.has(call.receiver);
148
209
  if (!bindingId && !skipLocalBinding) {
149
210
  let bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
150
211
  // For Go, also check sibling files in same directory (same package scope)
@@ -306,34 +367,106 @@ function findCallers(index, name, options = {}) {
306
367
  continue;
307
368
  }
308
369
 
370
+ // Import-graph disambiguation for JS/TS/Python: when multiple definitions of
371
+ // the same name exist and this call has no bindingId, check whether the calling
372
+ // file imports from the target definition's file. Skips false positives like
373
+ // user_b importing from b.js being reported as a caller of a.js:process.
374
+ // Go/Java/Rust are excluded — they use package/module scoping, not file imports.
375
+ if (!bindingId && options.targetDefinitions && definitions.length > 1 &&
376
+ fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
377
+ const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
378
+ if (targetFiles.size > 0 && !targetFiles.has(filePath)) {
379
+ const imports = index.importGraph.get(filePath) || [];
380
+ const importsTarget = imports.some(imp => targetFiles.has(imp));
381
+ if (!importsTarget) {
382
+ // Check one level of re-exports (barrel files)
383
+ let foundViaReexport = false;
384
+ for (const imp of imports) {
385
+ const transImports = index.importGraph.get(imp) || [];
386
+ if (transImports.some(ti => targetFiles.has(ti))) {
387
+ foundViaReexport = true;
388
+ break;
389
+ }
390
+ }
391
+ if (!foundViaReexport) continue;
392
+ }
393
+ }
394
+ }
395
+
396
+ // Go unexported visibility: lowercase functions are package-private.
397
+ // Only allow callers from the same package directory.
398
+ if (fileEntry.language === 'go' && /^[a-z]/.test(name)) {
399
+ const targetPkgDirs = new Set(
400
+ targetDefs.filter(d => d.file).map(d => path.dirname(d.file))
401
+ );
402
+ if (targetPkgDirs.size > 0 && !targetPkgDirs.has(path.dirname(filePath))) {
403
+ continue;
404
+ }
405
+ }
406
+
407
+ // Go/Java/Rust: method vs non-method cross-matching filter.
408
+ // Prevents t.Errorf() (method call) from matching standalone func Errorf,
409
+ // and cli.Run() (package call, isMethod:false) from matching DeploymentController.Run.
410
+ // Rust path calls (module::func(), Type::new()) bypass this filter — they're
411
+ // scoped_identifier calls that can target both standalone functions and impl methods.
412
+ if (!bindingId && !resolvedBySameClass && !call.isPathCall &&
413
+ (fileEntry.language === 'go' || fileEntry.language === 'java' || fileEntry.language === 'rust')) {
414
+ const targetHasClass = targetDefs.some(d => d.className);
415
+ if (call.isMethod && !targetHasClass) {
416
+ // Method call but target is a standalone function — skip
417
+ continue;
418
+ }
419
+ if (!call.isMethod && targetHasClass) {
420
+ // Non-method call but target is a class method — skip
421
+ continue;
422
+ }
423
+ }
424
+
309
425
  // Java/Go/Rust receiver-class disambiguation:
310
- // When targetDefinitions narrows to specific class(es) and the call has a
311
- // receiver (e.g. javascriptFileService.createDataFile()), check if the
312
- // receiver name better matches a non-target class definition.
313
- // This prevents false positives like reporting obj.save() as a caller of
314
- // TargetClass.save() when obj is clearly a different type.
426
+ // When the target definition has a class/receiver type, filter callers
427
+ // whose receiverType is known to be a different type.
428
+ // Go uses inferred receiverType; Java/Rust fall back to variable name matching.
315
429
  if (call.isMethod && call.receiver && !resolvedBySameClass && !bindingId &&
316
- options.targetDefinitions && definitions.length > 1 &&
317
430
  (fileEntry.language === 'java' || fileEntry.language === 'go' || fileEntry.language === 'rust')) {
318
- const targetClassNames = new Set(targetDefs.map(d => d.className).filter(Boolean));
319
- if (targetClassNames.size > 0) {
320
- const receiverLower = call.receiver.toLowerCase();
321
- // Check if receiver matches any target class (camelCase convention)
322
- const matchesTarget = [...targetClassNames].some(cn => cn.toLowerCase() === receiverLower);
323
- if (!matchesTarget) {
324
- // Check if receiver matches a non-target class instead
325
- const nonTargetClasses = definitions
326
- .filter(d => d.className && !targetClassNames.has(d.className))
327
- .map(d => d.className);
328
- const matchesOther = nonTargetClasses.some(cn => cn.toLowerCase() === receiverLower);
329
- if (matchesOther) {
330
- // Receiver clearly belongs to a different class
431
+ // Build target type set from both className (Java) and receiver (Go/Rust)
432
+ const targetTypes = new Set();
433
+ for (const td of targetDefs) {
434
+ if (td.className) targetTypes.add(td.className);
435
+ if (td.receiver) targetTypes.add(td.receiver.replace(/^\*/, ''));
436
+ }
437
+ if (targetTypes.size > 0) {
438
+ // Use inferred receiverType when available (Go/Java/Rust parameter type tracking)
439
+ const knownType = call.receiverType;
440
+ if (knownType) {
441
+ const matchesTarget = targetTypes.has(knownType);
442
+ if (!matchesTarget) {
443
+ // Known type doesn't match target skip directly
331
444
  isUncertain = true;
332
445
  if (!options.includeUncertain) {
333
446
  if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
334
447
  continue;
335
448
  }
336
449
  }
450
+ } else if (options.targetDefinitions && definitions.length > 1) {
451
+ // No inferred type — fall back to receiver variable name matching
452
+ // only when we have multiple definitions to disambiguate
453
+ const receiverLower = call.receiver.toLowerCase();
454
+ const matchesTarget = [...targetTypes].some(cn => cn.toLowerCase() === receiverLower);
455
+ if (!matchesTarget) {
456
+ const nonTargetClasses = new Set();
457
+ for (const d of definitions) {
458
+ const t = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
459
+ if (t && !targetTypes.has(t)) nonTargetClasses.add(t);
460
+ }
461
+ const matchesOther = [...nonTargetClasses].some(cn => cn.toLowerCase() === receiverLower);
462
+ if (matchesOther) {
463
+ isUncertain = true;
464
+ if (!options.includeUncertain) {
465
+ if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
466
+ continue;
467
+ }
468
+ }
469
+ }
337
470
  }
338
471
  }
339
472
  }
@@ -341,22 +474,43 @@ function findCallers(index, name, options = {}) {
341
474
  // Find the enclosing function (get full symbol info)
342
475
  const callerSymbol = index.findEnclosingFunction(filePath, call.line, true);
343
476
 
477
+ if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
478
+ pendingByFile.get(filePath).push({
479
+ call, fileEntry, callerSymbol,
480
+ isMethod: call.isMethod || false, isFunctionReference: false,
481
+ receiver: call.receiver,
482
+ receiverType: call.receiverType
483
+ });
484
+ pendingCount++;
485
+ }
486
+ } catch (e) {
487
+ // Expected: minified files exceed tree-sitter buffer, binary files fail to parse.
488
+ // These are not actionable errors — silently skip.
489
+ }
490
+ }
491
+
492
+ // Phase 2: Read content only for files with matching calls (eliminates ~98% of file reads)
493
+ for (const [filePath, pending] of pendingByFile) {
494
+ try {
495
+ const content = fs.readFileSync(filePath, 'utf-8');
496
+ for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType } of pending) {
344
497
  callers.push({
345
498
  file: filePath,
346
499
  relativePath: fileEntry.relativePath,
347
500
  line: call.line,
348
- content: lines[call.line - 1] || '',
501
+ content: getLine(content, call.line),
349
502
  callerName: callerSymbol ? callerSymbol.name : null,
350
503
  callerFile: callerSymbol ? filePath : null,
351
504
  callerStartLine: callerSymbol ? callerSymbol.startLine : null,
352
505
  callerEndLine: callerSymbol ? callerSymbol.endLine : null,
353
- isMethod: call.isMethod || false,
354
- receiver: call.receiver
506
+ isMethod,
507
+ ...(isFunctionReference && { isFunctionReference: true }),
508
+ ...(receiver !== undefined && { receiver }),
509
+ ...(receiverType && { receiverType })
355
510
  });
356
511
  }
357
512
  } catch (e) {
358
- // Expected: minified files exceed tree-sitter buffer, binary files fail to parse.
359
- // These are not actionable errors — silently skip.
513
+ // File may have been deleted between Phase 1 and Phase 2
360
514
  }
361
515
  }
362
516
 
@@ -374,6 +528,9 @@ function findCallers(index, name, options = {}) {
374
528
  function findCallees(index, def, options = {}) {
375
529
  index._beginOp();
376
530
  try {
531
+ // Lazy-load callsCache from disk if not already populated
532
+ if (index.loadCallsCache) index.loadCallsCache();
533
+
377
534
  try {
378
535
  // Get all calls from the file's cache (now includes enclosingFunction)
379
536
  const calls = getCachedCalls(index, def.file);
@@ -403,6 +560,8 @@ function findCallees(index, def, options = {}) {
403
560
  let localTypes = null;
404
561
  if (language === 'python' || language === 'javascript') {
405
562
  localTypes = _buildLocalTypeMap(index, def, calls);
563
+ } else if (language === 'go' || language === 'java' || language === 'rust') {
564
+ localTypes = _buildTypedLocalTypeMap(index, def, calls);
406
565
  }
407
566
 
408
567
  for (const call of calls) {
@@ -434,11 +593,17 @@ function findCallees(index, def, options = {}) {
434
593
  } else if (localTypes && localTypes.has(call.receiver)) {
435
594
  // Resolve method calls on locally-constructed objects:
436
595
  // bt = Backtester(...); bt.run_backtest() → Backtester.run_backtest
437
- const className = localTypes.get(call.receiver);
596
+ // Go: f.Run() where f is *Framework → Framework.Run (receiver match)
597
+ const typeName = localTypes.get(call.receiver);
438
598
  const symbols = index.symbols.get(call.name);
439
- const match = symbols?.find(s => s.className === className);
599
+ const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
600
+ (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
601
+ const match = symbols?.find(s =>
602
+ isCallable(s) && (
603
+ s.className === typeName ||
604
+ (s.receiver && s.receiver.replace(/^\*/, '') === typeName)));
440
605
  if (match) {
441
- const key = match.bindingId || `${className}.${call.name}`;
606
+ const key = match.bindingId || `${typeName}.${call.name}`;
442
607
  const existing = callees.get(key);
443
608
  if (existing) {
444
609
  existing.count += 1;
@@ -447,6 +612,64 @@ function findCallees(index, def, options = {}) {
447
612
  }
448
613
  }
449
614
  continue;
615
+ } else if (call.receiverType && (language === 'go' || language === 'java' || language === 'rust')) {
616
+ // Use parser-inferred receiverType for method resolution
617
+ // e.g., f.RunFilter() where f is *Framework → resolve to Framework.RunFilter
618
+ const typeName = call.receiverType;
619
+ const symbols = index.symbols.get(call.name);
620
+ const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
621
+ (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
622
+ const match = symbols?.find(s =>
623
+ isCallableRT(s) && (
624
+ (s.receiver && s.receiver.replace(/^\*/, '') === typeName) ||
625
+ s.className === typeName));
626
+ if (match) {
627
+ const key = match.bindingId || `${typeName}.${call.name}`;
628
+ const existing = callees.get(key);
629
+ if (existing) {
630
+ existing.count += 1;
631
+ } else {
632
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1 });
633
+ }
634
+ continue;
635
+ }
636
+ // No match found with inferred type — fall through to include as unresolved
637
+ } else if (language === 'go' && call.receiver) {
638
+ // Go package-qualified calls: klog.Infof(), wait.UntilWithContext()
639
+ // Check if receiver is an import alias and resolve to correct package
640
+ const goImports = fileEntry?.imports || [];
641
+ // Find import whose package name matches the receiver
642
+ const importModule = goImports.find(mod => {
643
+ const pkgName = mod.split('/').pop();
644
+ return pkgName === call.receiver;
645
+ });
646
+ if (importModule) {
647
+ // Receiver is an import alias — resolve to definitions from that package
648
+ const symbols = index.symbols.get(call.name);
649
+ if (symbols) {
650
+ // Match by checking if the definition's directory path matches the import path suffix
651
+ const importParts = importModule.split('/');
652
+ const match = symbols.find(s => {
653
+ const sDir = path.dirname(s.relativePath || path.relative(index.root, s.file));
654
+ // Try matching progressively shorter suffixes of the import path
655
+ for (let i = importParts.length - 1; i >= 0; i--) {
656
+ const suffix = importParts.slice(i).join('/');
657
+ if (sDir === suffix || sDir.endsWith('/' + suffix)) return true;
658
+ }
659
+ return false;
660
+ });
661
+ if (match) {
662
+ const key = match.bindingId || `${call.receiver}.${call.name}`;
663
+ const existing = callees.get(key);
664
+ if (existing) {
665
+ existing.count += 1;
666
+ } else {
667
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1 });
668
+ }
669
+ continue;
670
+ }
671
+ }
672
+ }
450
673
  } else if (language !== 'go' && language !== 'java' && language !== 'rust' && !options.includeMethods) {
451
674
  continue;
452
675
  }
@@ -544,7 +767,23 @@ function findCallees(index, def, options = {}) {
544
767
  // (cross-type ambiguity, e.g. TypeA.Length vs TypeB.Length).
545
768
  const defs = index.symbols.get(call.name);
546
769
  if (defs && defs.length > 1) {
547
- isUncertain = true;
770
+ // Go: if receiverType is known, check if it matches exactly one def
771
+ // This resolves ambiguity like Framework.Run vs Scheduler.Run
772
+ const rType = call.receiverType || localTypes?.get(call.receiver);
773
+ if (rType && (language === 'go' || language === 'java' || language === 'rust')) {
774
+ const matchingDef = defs.find(d =>
775
+ d.className === rType ||
776
+ (d.receiver && d.receiver.replace(/^\*/, '') === rType));
777
+ if (matchingDef) {
778
+ // Resolved to specific type — not uncertain
779
+ calleeKey = matchingDef.bindingId || `${rType}.${call.name}`;
780
+ bindingResolved = matchingDef.bindingId;
781
+ } else {
782
+ isUncertain = true;
783
+ }
784
+ } else {
785
+ isUncertain = true;
786
+ }
548
787
  }
549
788
  }
550
789
  }
@@ -769,10 +1008,10 @@ function findCallees(index, def, options = {}) {
769
1008
  callee = sameDir;
770
1009
  } else {
771
1010
  // Priority 2.5: Imported file — check if the caller's file imports
772
- // from any of the candidate callee files
773
- const callerImports = fileEntry?.imports || [];
774
- const importedFiles = new Set(callerImports.map(imp => imp.resolvedPath).filter(Boolean));
775
- const importedCallee = symbols.find(s => importedFiles.has(s.file));
1011
+ // from any of the candidate callee files (using importGraph)
1012
+ const callerImportedFiles = index.importGraph.get(def.file) || [];
1013
+ const importedFileSet = new Set(callerImportedFiles);
1014
+ const importedCallee = symbols.find(s => importedFileSet.has(s.file));
776
1015
  if (importedCallee) {
777
1016
  callee = importedCallee;
778
1017
  } else if (defReceiver) {
@@ -835,6 +1074,18 @@ function findCallees(index, def, options = {}) {
835
1074
  }
836
1075
  }
837
1076
 
1077
+ // Skip non-callable types (interface, struct, type) as callees.
1078
+ // These appear when local variables shadow symbol names
1079
+ // (e.g., `for _, handler := range handlers { handler(r) }` —
1080
+ // handler is a local var, not the handler interface type).
1081
+ // Exception: function-typed fields (e.g., syncHandler func(...))
1082
+ // are callable via Go dependency injection patterns.
1083
+ if (!bindingId && NON_CALLABLE_TYPES.has(callee.type)) {
1084
+ const isFuncField = callee.type === 'field' && callee.fieldType &&
1085
+ /^func\b/.test(callee.fieldType);
1086
+ if (!isFuncField) continue;
1087
+ }
1088
+
838
1089
  // Skip test-file callees when caller is production code and
839
1090
  // there's no binding (import) evidence linking them
840
1091
  if (!callerIsTest && !bindingId) {
@@ -950,6 +1201,53 @@ function _buildLocalTypeMap(index, def, calls) {
950
1201
  return localTypes.size > 0 ? localTypes : null;
951
1202
  }
952
1203
 
1204
+ /**
1205
+ * Build a local variable type map for typed languages (Go, Java, Rust)
1206
+ * using parser-inferred receiverType from call objects.
1207
+ * Go also resolves New*() constructor patterns.
1208
+ * @param {object} index - ProjectIndex instance
1209
+ * @param {object} def - Function definition with file, startLine, endLine
1210
+ * @param {Array} calls - Cached call sites for the file
1211
+ */
1212
+ function _buildTypedLocalTypeMap(index, def, calls) {
1213
+ const localTypes = new Map();
1214
+
1215
+ for (const call of calls) {
1216
+ if (call.line < def.startLine || call.line > def.endLine) continue;
1217
+
1218
+ // Collect receiverType from method calls (inferred by parser from params/receivers)
1219
+ if (call.isMethod && call.receiver && call.receiverType) {
1220
+ localTypes.set(call.receiver, call.receiverType);
1221
+ }
1222
+
1223
+ // Collect types from constructor calls: x := NewFoo() → x maps to Foo
1224
+ // Handles: x := NewFoo(), x, err := NewFoo(), x := pkg.NewFoo(), x, err := pkg.NewFoo()
1225
+ const newName = call.isMethod ? call.name : call.name;
1226
+ if (/^New[A-Z]/.test(newName) && !call.isPotentialCallback) {
1227
+ let content;
1228
+ try {
1229
+ content = index._readFile(def.file);
1230
+ } catch { continue; }
1231
+ const lines = content.split('\n');
1232
+ const sourceLine = lines[call.line - 1];
1233
+ if (!sourceLine) continue;
1234
+ // Match: x := [pkg.]NewFoo( or x, err := [pkg.]NewFoo( or x, _ := [pkg.]NewFoo(
1235
+ const assignMatch = sourceLine.match(
1236
+ /(\w+)(?:\s*,\s*\w+)?\s*:=\s*(?:\w+\.)?(\w+)\s*\(/
1237
+ );
1238
+ if (assignMatch && /^New[A-Z]/.test(assignMatch[2])) {
1239
+ // NewFoo → Foo, NewFooBar → FooBar
1240
+ const typeName = assignMatch[2].slice(3);
1241
+ if (typeName && /^[A-Z]/.test(typeName)) {
1242
+ localTypes.set(assignMatch[1], typeName);
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+
1248
+ return localTypes.size > 0 ? localTypes : null;
1249
+ }
1250
+
953
1251
  /**
954
1252
  * Check if a function is used as a callback anywhere in the codebase
955
1253
  * @param {object} index - ProjectIndex instance