ucn 3.7.45 → 3.7.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,18 @@ function findCallers(index, name, options = {}) {
102
130
  definitionLines.add(`${def.file}:${def.startLine}`);
103
131
  }
104
132
 
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
+
105
139
  for (const [filePath, fileEntry] of index.files) {
140
+ // Early exit when maxResults is reached
141
+ if (maxResults && pendingCount >= maxResults) break;
106
142
  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');
143
+ const calls = getCachedCalls(index, filePath);
144
+ if (!calls) continue;
112
145
 
113
146
  for (const call of calls) {
114
147
  // Skip if not matching our target name (also check alias resolution)
@@ -120,31 +153,53 @@ function findCallers(index, name, options = {}) {
120
153
  if (call.isPotentialCallback) {
121
154
  const syms = definitions;
122
155
  if (!syms || syms.length === 0) continue;
156
+
157
+ // Go unexported visibility: lowercase functions are package-private.
158
+ // Only allow callers from the same package directory.
159
+ if (fileEntry.language === 'go' && /^[a-z]/.test(name)) {
160
+ const targetDefs = options.targetDefinitions || definitions;
161
+ const targetPkgDirs = new Set(
162
+ targetDefs.filter(d => d.file).map(d => path.dirname(d.file))
163
+ );
164
+ if (targetPkgDirs.size > 0 && !targetPkgDirs.has(path.dirname(filePath))) {
165
+ continue;
166
+ }
167
+ }
168
+
169
+ // Go/Java/Rust receiver disambiguation for callbacks (e.g. dc.worker)
170
+ if (call.isMethod && call.receiver &&
171
+ (fileEntry.language === 'go' || fileEntry.language === 'java' || fileEntry.language === 'rust')) {
172
+ const targetDefs = options.targetDefinitions || definitions;
173
+ const targetTypes = new Set();
174
+ for (const td of targetDefs) {
175
+ if (td.className) targetTypes.add(td.className);
176
+ if (td.receiver) targetTypes.add(td.receiver.replace(/^\*/, ''));
177
+ }
178
+ if (targetTypes.size > 0 && call.receiverType) {
179
+ if (!targetTypes.has(call.receiverType)) continue;
180
+ }
181
+ }
182
+
123
183
  // Find the enclosing function
124
184
  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
185
+ if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
186
+ pendingByFile.get(filePath).push({
187
+ call, fileEntry, callerSymbol,
188
+ isMethod: false, isFunctionReference: true, receiver: undefined
136
189
  });
190
+ pendingCount++;
137
191
  continue;
138
192
  }
139
193
 
140
194
  // Resolve binding within this file (without mutating cached call objects)
141
195
  let bindingId = call.bindingId;
142
196
  let isUncertain = call.uncertain;
143
- // Skip binding resolution for method calls with non-self/this/cls receivers:
197
+ // Skip binding resolution for calls with non-self/this/cls receivers:
144
198
  // e.g., analyzer.analyze_instrument() should NOT resolve to a local
145
199
  // standalone function def `analyze_instrument` — they're different symbols.
200
+ // Also skip for Go package-qualified calls (isMethod:false but has receiver like 'cli')
146
201
  const selfReceivers = new Set(['self', 'cls', 'this', 'super']);
147
- const skipLocalBinding = call.isMethod && call.receiver && !selfReceivers.has(call.receiver);
202
+ const skipLocalBinding = call.receiver && !selfReceivers.has(call.receiver);
148
203
  if (!bindingId && !skipLocalBinding) {
149
204
  let bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
150
205
  // For Go, also check sibling files in same directory (same package scope)
@@ -306,34 +361,78 @@ function findCallers(index, name, options = {}) {
306
361
  continue;
307
362
  }
308
363
 
364
+ // Go unexported visibility: lowercase functions are package-private.
365
+ // Only allow callers from the same package directory.
366
+ if (fileEntry.language === 'go' && /^[a-z]/.test(name)) {
367
+ const targetPkgDirs = new Set(
368
+ targetDefs.filter(d => d.file).map(d => path.dirname(d.file))
369
+ );
370
+ if (targetPkgDirs.size > 0 && !targetPkgDirs.has(path.dirname(filePath))) {
371
+ continue;
372
+ }
373
+ }
374
+
375
+ // Go/Java/Rust: method vs non-method cross-matching filter.
376
+ // Prevents t.Errorf() (method call) from matching standalone func Errorf,
377
+ // and cli.Run() (package call, isMethod:false) from matching DeploymentController.Run.
378
+ if (!bindingId && !resolvedBySameClass &&
379
+ (fileEntry.language === 'go' || fileEntry.language === 'java' || fileEntry.language === 'rust')) {
380
+ const targetHasClass = targetDefs.some(d => d.className);
381
+ if (call.isMethod && !targetHasClass) {
382
+ // Method call but target is a standalone function — skip
383
+ continue;
384
+ }
385
+ if (!call.isMethod && targetHasClass) {
386
+ // Non-method call but target is a class method — skip
387
+ continue;
388
+ }
389
+ }
390
+
309
391
  // 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.
392
+ // When the target definition has a class/receiver type, filter callers
393
+ // whose receiverType is known to be a different type.
394
+ // Go uses inferred receiverType; Java/Rust fall back to variable name matching.
315
395
  if (call.isMethod && call.receiver && !resolvedBySameClass && !bindingId &&
316
- options.targetDefinitions && definitions.length > 1 &&
317
396
  (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
397
+ // Build target type set from both className (Java) and receiver (Go/Rust)
398
+ const targetTypes = new Set();
399
+ for (const td of targetDefs) {
400
+ if (td.className) targetTypes.add(td.className);
401
+ if (td.receiver) targetTypes.add(td.receiver.replace(/^\*/, ''));
402
+ }
403
+ if (targetTypes.size > 0) {
404
+ // Use inferred receiverType when available (Go/Java/Rust parameter type tracking)
405
+ const knownType = call.receiverType;
406
+ if (knownType) {
407
+ const matchesTarget = targetTypes.has(knownType);
408
+ if (!matchesTarget) {
409
+ // Known type doesn't match target skip directly
331
410
  isUncertain = true;
332
411
  if (!options.includeUncertain) {
333
412
  if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
334
413
  continue;
335
414
  }
336
415
  }
416
+ } else if (options.targetDefinitions && definitions.length > 1) {
417
+ // No inferred type — fall back to receiver variable name matching
418
+ // only when we have multiple definitions to disambiguate
419
+ const receiverLower = call.receiver.toLowerCase();
420
+ const matchesTarget = [...targetTypes].some(cn => cn.toLowerCase() === receiverLower);
421
+ if (!matchesTarget) {
422
+ const nonTargetClasses = new Set();
423
+ for (const d of definitions) {
424
+ const t = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
425
+ if (t && !targetTypes.has(t)) nonTargetClasses.add(t);
426
+ }
427
+ const matchesOther = [...nonTargetClasses].some(cn => cn.toLowerCase() === receiverLower);
428
+ if (matchesOther) {
429
+ isUncertain = true;
430
+ if (!options.includeUncertain) {
431
+ if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
432
+ continue;
433
+ }
434
+ }
435
+ }
337
436
  }
338
437
  }
339
438
  }
@@ -341,22 +440,43 @@ function findCallers(index, name, options = {}) {
341
440
  // Find the enclosing function (get full symbol info)
342
441
  const callerSymbol = index.findEnclosingFunction(filePath, call.line, true);
343
442
 
443
+ if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
444
+ pendingByFile.get(filePath).push({
445
+ call, fileEntry, callerSymbol,
446
+ isMethod: call.isMethod || false, isFunctionReference: false,
447
+ receiver: call.receiver,
448
+ receiverType: call.receiverType
449
+ });
450
+ pendingCount++;
451
+ }
452
+ } catch (e) {
453
+ // Expected: minified files exceed tree-sitter buffer, binary files fail to parse.
454
+ // These are not actionable errors — silently skip.
455
+ }
456
+ }
457
+
458
+ // Phase 2: Read content only for files with matching calls (eliminates ~98% of file reads)
459
+ for (const [filePath, pending] of pendingByFile) {
460
+ try {
461
+ const content = fs.readFileSync(filePath, 'utf-8');
462
+ for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType } of pending) {
344
463
  callers.push({
345
464
  file: filePath,
346
465
  relativePath: fileEntry.relativePath,
347
466
  line: call.line,
348
- content: lines[call.line - 1] || '',
467
+ content: getLine(content, call.line),
349
468
  callerName: callerSymbol ? callerSymbol.name : null,
350
469
  callerFile: callerSymbol ? filePath : null,
351
470
  callerStartLine: callerSymbol ? callerSymbol.startLine : null,
352
471
  callerEndLine: callerSymbol ? callerSymbol.endLine : null,
353
- isMethod: call.isMethod || false,
354
- receiver: call.receiver
472
+ isMethod,
473
+ ...(isFunctionReference && { isFunctionReference: true }),
474
+ ...(receiver !== undefined && { receiver }),
475
+ ...(receiverType && { receiverType })
355
476
  });
356
477
  }
357
478
  } catch (e) {
358
- // Expected: minified files exceed tree-sitter buffer, binary files fail to parse.
359
- // These are not actionable errors — silently skip.
479
+ // File may have been deleted between Phase 1 and Phase 2
360
480
  }
361
481
  }
362
482
 
@@ -374,6 +494,9 @@ function findCallers(index, name, options = {}) {
374
494
  function findCallees(index, def, options = {}) {
375
495
  index._beginOp();
376
496
  try {
497
+ // Lazy-load callsCache from disk if not already populated
498
+ if (index.loadCallsCache) index.loadCallsCache();
499
+
377
500
  try {
378
501
  // Get all calls from the file's cache (now includes enclosingFunction)
379
502
  const calls = getCachedCalls(index, def.file);
@@ -403,6 +526,8 @@ function findCallees(index, def, options = {}) {
403
526
  let localTypes = null;
404
527
  if (language === 'python' || language === 'javascript') {
405
528
  localTypes = _buildLocalTypeMap(index, def, calls);
529
+ } else if (language === 'go' || language === 'java' || language === 'rust') {
530
+ localTypes = _buildTypedLocalTypeMap(index, def, calls);
406
531
  }
407
532
 
408
533
  for (const call of calls) {
@@ -434,11 +559,17 @@ function findCallees(index, def, options = {}) {
434
559
  } else if (localTypes && localTypes.has(call.receiver)) {
435
560
  // Resolve method calls on locally-constructed objects:
436
561
  // bt = Backtester(...); bt.run_backtest() → Backtester.run_backtest
437
- const className = localTypes.get(call.receiver);
562
+ // Go: f.Run() where f is *Framework → Framework.Run (receiver match)
563
+ const typeName = localTypes.get(call.receiver);
438
564
  const symbols = index.symbols.get(call.name);
439
- const match = symbols?.find(s => s.className === className);
565
+ const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
566
+ (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
567
+ const match = symbols?.find(s =>
568
+ isCallable(s) && (
569
+ s.className === typeName ||
570
+ (s.receiver && s.receiver.replace(/^\*/, '') === typeName)));
440
571
  if (match) {
441
- const key = match.bindingId || `${className}.${call.name}`;
572
+ const key = match.bindingId || `${typeName}.${call.name}`;
442
573
  const existing = callees.get(key);
443
574
  if (existing) {
444
575
  existing.count += 1;
@@ -447,6 +578,64 @@ function findCallees(index, def, options = {}) {
447
578
  }
448
579
  }
449
580
  continue;
581
+ } else if (call.receiverType && (language === 'go' || language === 'java' || language === 'rust')) {
582
+ // Use parser-inferred receiverType for method resolution
583
+ // e.g., f.RunFilter() where f is *Framework → resolve to Framework.RunFilter
584
+ const typeName = call.receiverType;
585
+ const symbols = index.symbols.get(call.name);
586
+ const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
587
+ (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
588
+ const match = symbols?.find(s =>
589
+ isCallableRT(s) && (
590
+ (s.receiver && s.receiver.replace(/^\*/, '') === typeName) ||
591
+ s.className === typeName));
592
+ if (match) {
593
+ const key = match.bindingId || `${typeName}.${call.name}`;
594
+ const existing = callees.get(key);
595
+ if (existing) {
596
+ existing.count += 1;
597
+ } else {
598
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1 });
599
+ }
600
+ continue;
601
+ }
602
+ // No match found with inferred type — fall through to include as unresolved
603
+ } else if (language === 'go' && call.receiver) {
604
+ // Go package-qualified calls: klog.Infof(), wait.UntilWithContext()
605
+ // Check if receiver is an import alias and resolve to correct package
606
+ const goImports = fileEntry?.imports || [];
607
+ // Find import whose package name matches the receiver
608
+ const importModule = goImports.find(mod => {
609
+ const pkgName = mod.split('/').pop();
610
+ return pkgName === call.receiver;
611
+ });
612
+ if (importModule) {
613
+ // Receiver is an import alias — resolve to definitions from that package
614
+ const symbols = index.symbols.get(call.name);
615
+ if (symbols) {
616
+ // Match by checking if the definition's directory path matches the import path suffix
617
+ const importParts = importModule.split('/');
618
+ const match = symbols.find(s => {
619
+ const sDir = path.dirname(s.relativePath || path.relative(index.root, s.file));
620
+ // Try matching progressively shorter suffixes of the import path
621
+ for (let i = importParts.length - 1; i >= 0; i--) {
622
+ const suffix = importParts.slice(i).join('/');
623
+ if (sDir === suffix || sDir.endsWith('/' + suffix)) return true;
624
+ }
625
+ return false;
626
+ });
627
+ if (match) {
628
+ const key = match.bindingId || `${call.receiver}.${call.name}`;
629
+ const existing = callees.get(key);
630
+ if (existing) {
631
+ existing.count += 1;
632
+ } else {
633
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1 });
634
+ }
635
+ continue;
636
+ }
637
+ }
638
+ }
450
639
  } else if (language !== 'go' && language !== 'java' && language !== 'rust' && !options.includeMethods) {
451
640
  continue;
452
641
  }
@@ -544,7 +733,23 @@ function findCallees(index, def, options = {}) {
544
733
  // (cross-type ambiguity, e.g. TypeA.Length vs TypeB.Length).
545
734
  const defs = index.symbols.get(call.name);
546
735
  if (defs && defs.length > 1) {
547
- isUncertain = true;
736
+ // Go: if receiverType is known, check if it matches exactly one def
737
+ // This resolves ambiguity like Framework.Run vs Scheduler.Run
738
+ const rType = call.receiverType || localTypes?.get(call.receiver);
739
+ if (rType && (language === 'go' || language === 'java' || language === 'rust')) {
740
+ const matchingDef = defs.find(d =>
741
+ d.className === rType ||
742
+ (d.receiver && d.receiver.replace(/^\*/, '') === rType));
743
+ if (matchingDef) {
744
+ // Resolved to specific type — not uncertain
745
+ calleeKey = matchingDef.bindingId || `${rType}.${call.name}`;
746
+ bindingResolved = matchingDef.bindingId;
747
+ } else {
748
+ isUncertain = true;
749
+ }
750
+ } else {
751
+ isUncertain = true;
752
+ }
548
753
  }
549
754
  }
550
755
  }
@@ -769,10 +974,10 @@ function findCallees(index, def, options = {}) {
769
974
  callee = sameDir;
770
975
  } else {
771
976
  // 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));
977
+ // from any of the candidate callee files (using importGraph)
978
+ const callerImportedFiles = index.importGraph.get(def.file) || [];
979
+ const importedFileSet = new Set(callerImportedFiles);
980
+ const importedCallee = symbols.find(s => importedFileSet.has(s.file));
776
981
  if (importedCallee) {
777
982
  callee = importedCallee;
778
983
  } else if (defReceiver) {
@@ -835,6 +1040,18 @@ function findCallees(index, def, options = {}) {
835
1040
  }
836
1041
  }
837
1042
 
1043
+ // Skip non-callable types (interface, struct, type) as callees.
1044
+ // These appear when local variables shadow symbol names
1045
+ // (e.g., `for _, handler := range handlers { handler(r) }` —
1046
+ // handler is a local var, not the handler interface type).
1047
+ // Exception: function-typed fields (e.g., syncHandler func(...))
1048
+ // are callable via Go dependency injection patterns.
1049
+ if (!bindingId && NON_CALLABLE_TYPES.has(callee.type)) {
1050
+ const isFuncField = callee.type === 'field' && callee.fieldType &&
1051
+ /^func\b/.test(callee.fieldType);
1052
+ if (!isFuncField) continue;
1053
+ }
1054
+
838
1055
  // Skip test-file callees when caller is production code and
839
1056
  // there's no binding (import) evidence linking them
840
1057
  if (!callerIsTest && !bindingId) {
@@ -950,6 +1167,53 @@ function _buildLocalTypeMap(index, def, calls) {
950
1167
  return localTypes.size > 0 ? localTypes : null;
951
1168
  }
952
1169
 
1170
+ /**
1171
+ * Build a local variable type map for typed languages (Go, Java, Rust)
1172
+ * using parser-inferred receiverType from call objects.
1173
+ * Go also resolves New*() constructor patterns.
1174
+ * @param {object} index - ProjectIndex instance
1175
+ * @param {object} def - Function definition with file, startLine, endLine
1176
+ * @param {Array} calls - Cached call sites for the file
1177
+ */
1178
+ function _buildTypedLocalTypeMap(index, def, calls) {
1179
+ const localTypes = new Map();
1180
+
1181
+ for (const call of calls) {
1182
+ if (call.line < def.startLine || call.line > def.endLine) continue;
1183
+
1184
+ // Collect receiverType from method calls (inferred by parser from params/receivers)
1185
+ if (call.isMethod && call.receiver && call.receiverType) {
1186
+ localTypes.set(call.receiver, call.receiverType);
1187
+ }
1188
+
1189
+ // Collect types from constructor calls: x := NewFoo() → x maps to Foo
1190
+ // Handles: x := NewFoo(), x, err := NewFoo(), x := pkg.NewFoo(), x, err := pkg.NewFoo()
1191
+ const newName = call.isMethod ? call.name : call.name;
1192
+ if (/^New[A-Z]/.test(newName) && !call.isPotentialCallback) {
1193
+ let content;
1194
+ try {
1195
+ content = index._readFile(def.file);
1196
+ } catch { continue; }
1197
+ const lines = content.split('\n');
1198
+ const sourceLine = lines[call.line - 1];
1199
+ if (!sourceLine) continue;
1200
+ // Match: x := [pkg.]NewFoo( or x, err := [pkg.]NewFoo( or x, _ := [pkg.]NewFoo(
1201
+ const assignMatch = sourceLine.match(
1202
+ /(\w+)(?:\s*,\s*\w+)?\s*:=\s*(?:\w+\.)?(\w+)\s*\(/
1203
+ );
1204
+ if (assignMatch && /^New[A-Z]/.test(assignMatch[2])) {
1205
+ // NewFoo → Foo, NewFooBar → FooBar
1206
+ const typeName = assignMatch[2].slice(3);
1207
+ if (typeName && /^[A-Z]/.test(typeName)) {
1208
+ localTypes.set(assignMatch[1], typeName);
1209
+ }
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ return localTypes.size > 0 ? localTypes : null;
1215
+ }
1216
+
953
1217
  /**
954
1218
  * Check if a function is used as a callback anywhere in the codebase
955
1219
  * @param {object} index - ProjectIndex instance
package/core/deadcode.js CHANGED
@@ -9,12 +9,13 @@ const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../
9
9
  const { isTestFile } = require('./discovery');
10
10
 
11
11
  /**
12
- * Build a usage index for all identifiers in the codebase (optimized for deadcode)
12
+ * Build a usage index for identifiers in the codebase (optimized for deadcode)
13
13
  * Scans all files ONCE and builds a reverse index: name -> [usages]
14
14
  * @param {object} index - ProjectIndex instance
15
+ * @param {Set<string>} [filterNames] - If provided, only track these names (reduces memory)
15
16
  * @returns {Map<string, Array>} Usage index
16
17
  */
17
- function buildUsageIndex(index) {
18
+ function buildUsageIndex(index, filterNames) {
18
19
  const usageIndex = new Map(); // name -> [{file, line}]
19
20
 
20
21
  for (const [filePath, fileEntry] of index.files) {
@@ -72,6 +73,8 @@ function buildUsageIndex(index) {
72
73
  }
73
74
  }
74
75
  // Member expression property: obj.Separator — not a standalone reference
76
+ // EXCEPTION: If the selector/member expression is part of a call_expression,
77
+ // this IS a method call (e.g., dc.syncDeployment()) and should count as usage.
75
78
  if (parentType === 'member_expression' ||
76
79
  parentType === 'field_expression' ||
77
80
  parentType === 'member_access_expression' ||
@@ -82,15 +85,31 @@ function buildUsageIndex(index) {
82
85
  // by checking if it's NOT the object (left side)
83
86
  const firstChild = node.parent.child(0);
84
87
  if (firstChild !== node) {
85
- // This is the property part skip it for deadcode counting
86
- for (let i = 0; i < node.childCount; i++) {
87
- traverse(node.child(i));
88
+ // Check if this member expression is part of a call or
89
+ // used as a function reference (callback argument)
90
+ const grandparent = node.parent.parent;
91
+ const isCall = grandparent &&
92
+ (grandparent.type === 'call_expression' ||
93
+ grandparent.type === 'argument_list' ||
94
+ grandparent.type === 'arguments');
95
+ if (!isCall) {
96
+ // Pure field access — skip for deadcode counting
97
+ for (let i = 0; i < node.childCount; i++) {
98
+ traverse(node.child(i));
99
+ }
100
+ return;
88
101
  }
89
- return;
102
+ // Method call or callback — fall through to count as usage
90
103
  }
91
104
  }
92
105
  }
93
106
  const name = node.text;
107
+ if (filterNames && !filterNames.has(name)) {
108
+ for (let i = 0; i < node.childCount; i++) {
109
+ traverse(node.child(i));
110
+ }
111
+ return;
112
+ }
94
113
  if (!usageIndex.has(name)) {
95
114
  usageIndex.set(name, []);
96
115
  }
@@ -144,19 +163,21 @@ function deadcode(index, options = {}) {
144
163
  let excludedDecorated = 0;
145
164
  let excludedExported = 0;
146
165
 
147
- // Build usage index once (instead of per-symbol)
148
- const usageIndex = buildUsageIndex(index);
166
+ // Collect callable symbol names to reduce usage index scope
167
+ const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
168
+ const callableNames = new Set();
169
+ for (const [symbolName, symbols] of index.symbols) {
170
+ if (symbols.some(s => callableTypes.includes(s.type))) {
171
+ callableNames.add(symbolName);
172
+ }
173
+ }
174
+
175
+ // Build usage index once (instead of per-symbol), filtered to callable names only
176
+ const usageIndex = buildUsageIndex(index, callableNames);
149
177
 
150
178
  for (const [name, symbols] of index.symbols) {
151
179
  for (const symbol of symbols) {
152
- // Skip non-function/class types
153
- // Include various method types from different languages:
154
- // - function: standalone functions
155
- // - class, struct, interface: type definitions (skip them in deadcode)
156
- // - method: class methods
157
- // - static, public, abstract: Java method modifiers used as types
158
- // - constructor: constructors
159
- const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
180
+ // Skip non-function/class types (callableTypes defined above)
160
181
  if (!callableTypes.includes(symbol.type)) {
161
182
  continue;
162
183
  }
@@ -174,6 +195,11 @@ function deadcode(index, options = {}) {
174
195
  continue;
175
196
  }
176
197
 
198
+ // Apply file filter (scopes deadcode to matching files)
199
+ if (options.file && !symbol.relativePath.includes(options.file)) {
200
+ continue;
201
+ }
202
+
177
203
  // Apply exclude and in filters
178
204
  if ((options.exclude && options.exclude.length > 0) || options.in) {
179
205
  if (!index.matchesFilters(symbol.relativePath, { exclude: options.exclude, in: options.in })) {
package/core/discovery.js CHANGED
@@ -191,7 +191,7 @@ function expandGlob(pattern, options = {}) {
191
191
  const root = path.resolve(options.root || process.cwd());
192
192
  const ignores = options.ignores || DEFAULT_IGNORES;
193
193
  const maxDepth = options.maxDepth || 20;
194
- const maxFiles = options.maxFiles || 10000;
194
+ const maxFiles = options.maxFiles || 50000;
195
195
  const followSymlinks = options.followSymlinks !== false; // default true
196
196
 
197
197
  // Handle home directory expansion