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/.claude/skills/ucn/SKILL.md +34 -4
- package/README.md +16 -13
- package/cli/index.js +112 -15
- package/core/cache.js +176 -51
- package/core/callers.js +350 -52
- package/core/deadcode.js +143 -17
- package/core/discovery.js +1 -1
- package/core/execute.js +245 -11
- package/core/output.js +423 -4
- package/core/project.js +1204 -94
- package/core/registry.js +18 -7
- package/core/shared.js +1 -1
- package/core/stacktrace.js +31 -2
- package/core/verify.js +11 -0
- package/languages/go.js +338 -24
- package/languages/index.js +20 -1
- package/languages/java.js +145 -6
- package/languages/javascript.js +199 -8
- package/languages/python.js +8 -2
- package/languages/rust.js +168 -8
- package/mcp/server.js +79 -17
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
if (!
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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.
|
|
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
|
|
311
|
-
//
|
|
312
|
-
//
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
//
|
|
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:
|
|
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
|
|
354
|
-
|
|
506
|
+
isMethod,
|
|
507
|
+
...(isFunctionReference && { isFunctionReference: true }),
|
|
508
|
+
...(receiver !== undefined && { receiver }),
|
|
509
|
+
...(receiverType && { receiverType })
|
|
355
510
|
});
|
|
356
511
|
}
|
|
357
512
|
} catch (e) {
|
|
358
|
-
//
|
|
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
|
-
|
|
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
|
|
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 || `${
|
|
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
|
-
|
|
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
|
|
774
|
-
const
|
|
775
|
-
const importedCallee = symbols.find(s =>
|
|
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
|