ucn 3.2.0 → 3.4.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.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/README.md +6 -2
- package/cli/index.js +146 -44
- package/core/imports.js +162 -4
- package/core/output.js +129 -147
- package/core/project.js +412 -133
- package/languages/go.js +21 -10
- package/languages/java.js +29 -10
- package/languages/javascript.js +56 -37
- package/languages/python.js +39 -10
- package/languages/rust.js +36 -8
- package/package.json +1 -1
- package/test/parser.test.js +1217 -7
- package/test/reliability-test-prompt.md +58 -0
package/core/project.js
CHANGED
|
@@ -139,7 +139,7 @@ class ProjectIndex {
|
|
|
139
139
|
if (!language) return;
|
|
140
140
|
|
|
141
141
|
const parsed = parseFile(filePath);
|
|
142
|
-
const { imports } = extractImports(content, language);
|
|
142
|
+
const { imports, dynamicCount } = extractImports(content, language);
|
|
143
143
|
const { exports } = extractExports(content, language);
|
|
144
144
|
|
|
145
145
|
const fileEntry = {
|
|
@@ -152,8 +152,10 @@ class ProjectIndex {
|
|
|
152
152
|
size: stat.size,
|
|
153
153
|
imports: imports.map(i => i.module),
|
|
154
154
|
exports: exports.map(e => e.name),
|
|
155
|
-
symbols: []
|
|
155
|
+
symbols: [],
|
|
156
|
+
bindings: []
|
|
156
157
|
};
|
|
158
|
+
fileEntry.dynamicImports = dynamicCount || 0;
|
|
157
159
|
|
|
158
160
|
// Add symbols
|
|
159
161
|
const addSymbol = (item, type) => {
|
|
@@ -169,6 +171,7 @@ class ProjectIndex {
|
|
|
169
171
|
returnType: item.returnType,
|
|
170
172
|
modifiers: item.modifiers,
|
|
171
173
|
docstring: item.docstring,
|
|
174
|
+
bindingId: `${fileEntry.relativePath}:${type}:${item.startLine}`,
|
|
172
175
|
...(item.extends && { extends: item.extends }),
|
|
173
176
|
...(item.implements && { implements: item.implements }),
|
|
174
177
|
...(item.indent !== undefined && { indent: item.indent }),
|
|
@@ -179,6 +182,12 @@ class ProjectIndex {
|
|
|
179
182
|
...(item.memberType && { memberType: item.memberType })
|
|
180
183
|
};
|
|
181
184
|
fileEntry.symbols.push(symbol);
|
|
185
|
+
fileEntry.bindings.push({
|
|
186
|
+
id: symbol.bindingId,
|
|
187
|
+
name: symbol.name,
|
|
188
|
+
type: symbol.type,
|
|
189
|
+
startLine: symbol.startLine
|
|
190
|
+
});
|
|
182
191
|
|
|
183
192
|
if (!this.symbols.has(item.name)) {
|
|
184
193
|
this.symbols.set(item.name, []);
|
|
@@ -234,16 +243,47 @@ class ProjectIndex {
|
|
|
234
243
|
this.importGraph.clear();
|
|
235
244
|
this.exportGraph.clear();
|
|
236
245
|
|
|
246
|
+
// Build Java suffix lookup for package import resolution
|
|
247
|
+
// Maps "com/google/gson/Gson.java" -> absolute path
|
|
248
|
+
let javaSuffixMap = null;
|
|
249
|
+
|
|
237
250
|
for (const [filePath, fileEntry] of this.files) {
|
|
238
251
|
const importedFiles = [];
|
|
252
|
+
const seenModules = new Set();
|
|
239
253
|
|
|
240
254
|
for (const importModule of fileEntry.imports) {
|
|
241
|
-
|
|
255
|
+
// Deduplicate: same module imported multiple times in one file
|
|
256
|
+
// (e.g., lazy imports inside different functions)
|
|
257
|
+
if (seenModules.has(importModule)) continue;
|
|
258
|
+
seenModules.add(importModule);
|
|
259
|
+
|
|
260
|
+
let resolved = resolveImport(importModule, filePath, {
|
|
242
261
|
aliases: this.config.aliases,
|
|
243
262
|
language: fileEntry.language,
|
|
244
263
|
root: this.root
|
|
245
264
|
});
|
|
246
265
|
|
|
266
|
+
// Java package imports: resolve by matching file path suffix
|
|
267
|
+
// e.g., "com.google.gson.Gson" -> find file ending in "com/google/gson/Gson.java"
|
|
268
|
+
if (!resolved && fileEntry.language === 'java' &&
|
|
269
|
+
!importModule.startsWith('.') && !importModule.endsWith('.*')) {
|
|
270
|
+
if (!javaSuffixMap) {
|
|
271
|
+
javaSuffixMap = new Map();
|
|
272
|
+
for (const [absPath, entry] of this.files) {
|
|
273
|
+
if (entry.language === 'java') {
|
|
274
|
+
javaSuffixMap.set(absPath, absPath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const fileSuffix = '/' + importModule.split('.').join('/') + '.java';
|
|
279
|
+
for (const absPath of javaSuffixMap.keys()) {
|
|
280
|
+
if (absPath.endsWith(fileSuffix)) {
|
|
281
|
+
resolved = absPath;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
247
287
|
if (resolved && this.files.has(resolved)) {
|
|
248
288
|
importedFiles.push(resolved);
|
|
249
289
|
|
|
@@ -287,6 +327,17 @@ class ProjectIndex {
|
|
|
287
327
|
}
|
|
288
328
|
}
|
|
289
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Count dynamic imports across indexed files
|
|
332
|
+
*/
|
|
333
|
+
getDynamicImportCount() {
|
|
334
|
+
let total = 0;
|
|
335
|
+
for (const fileEntry of this.files.values()) {
|
|
336
|
+
total += fileEntry.dynamicImports || 0;
|
|
337
|
+
}
|
|
338
|
+
return total;
|
|
339
|
+
}
|
|
340
|
+
|
|
290
341
|
// ========================================================================
|
|
291
342
|
// QUERY METHODS
|
|
292
343
|
// ========================================================================
|
|
@@ -299,10 +350,14 @@ class ProjectIndex {
|
|
|
299
350
|
*/
|
|
300
351
|
matchesFilters(filePath, filters = {}) {
|
|
301
352
|
// Check exclusions (patterns like 'test', 'mock', 'spec')
|
|
353
|
+
// Uses path-segment boundary matching to avoid false positives
|
|
354
|
+
// (e.g. 'test' should NOT match 'backtester', but should match 'tests/', 'test_foo', '_test.')
|
|
302
355
|
if (filters.exclude && filters.exclude.length > 0) {
|
|
303
356
|
const lowerPath = filePath.toLowerCase();
|
|
304
357
|
for (const pattern of filters.exclude) {
|
|
305
|
-
|
|
358
|
+
const escaped = pattern.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
359
|
+
const regex = new RegExp(`(^|/)${escaped}|[_.\\-]${escaped}([_.\\-/]|$)`);
|
|
360
|
+
if (regex.test(lowerPath)) {
|
|
306
361
|
return false;
|
|
307
362
|
}
|
|
308
363
|
}
|
|
@@ -367,6 +422,80 @@ class ProjectIndex {
|
|
|
367
422
|
* @param {object} options - { file, prefer, exact, exclude, in }
|
|
368
423
|
* @returns {Array} Matching symbols with usage counts
|
|
369
424
|
*/
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Resolve a symbol name to the best matching definition.
|
|
428
|
+
* Centralized selection logic used by all commands for consistency.
|
|
429
|
+
*
|
|
430
|
+
* Priority order:
|
|
431
|
+
* 1. Filter by --file if specified
|
|
432
|
+
* 2. Prefer class/struct/interface/type over functions/constructors
|
|
433
|
+
* 3. Prefer non-test file definitions over test files
|
|
434
|
+
* 4. Prefer higher usage count
|
|
435
|
+
*
|
|
436
|
+
* @param {string} name - Symbol name
|
|
437
|
+
* @param {object} [options] - { file }
|
|
438
|
+
* @returns {{ def: object|null, definitions: Array, warnings: Array }}
|
|
439
|
+
*/
|
|
440
|
+
resolveSymbol(name, options = {}) {
|
|
441
|
+
let definitions = this.symbols.get(name) || [];
|
|
442
|
+
if (definitions.length === 0) {
|
|
443
|
+
return { def: null, definitions: [], warnings: [] };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Filter by file if specified
|
|
447
|
+
if (options.file) {
|
|
448
|
+
const filtered = definitions.filter(d =>
|
|
449
|
+
d.relativePath && d.relativePath.includes(options.file)
|
|
450
|
+
);
|
|
451
|
+
if (filtered.length > 0) {
|
|
452
|
+
definitions = filtered;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Score each definition for selection
|
|
457
|
+
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
458
|
+
const scored = definitions.map(d => {
|
|
459
|
+
let score = 0;
|
|
460
|
+
const rp = d.relativePath || '';
|
|
461
|
+
// Prefer class/struct/interface types (+1000)
|
|
462
|
+
if (typeOrder.has(d.type)) score += 1000;
|
|
463
|
+
// Deprioritize test files (-500)
|
|
464
|
+
if (isTestFile(rp, detectLanguage(d.file))) {
|
|
465
|
+
score -= 500;
|
|
466
|
+
}
|
|
467
|
+
// Deprioritize examples/docs/vendor directories (-300)
|
|
468
|
+
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) {
|
|
469
|
+
score -= 300;
|
|
470
|
+
}
|
|
471
|
+
// Boost lib/src/core/internal directories (+200)
|
|
472
|
+
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) {
|
|
473
|
+
score += 200;
|
|
474
|
+
}
|
|
475
|
+
return { def: d, score };
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Sort by score descending, then by index order for stability
|
|
479
|
+
scored.sort((a, b) => b.score - a.score);
|
|
480
|
+
|
|
481
|
+
const def = scored[0].def;
|
|
482
|
+
|
|
483
|
+
// Build warnings
|
|
484
|
+
const warnings = [];
|
|
485
|
+
if (definitions.length > 1) {
|
|
486
|
+
warnings.push({
|
|
487
|
+
type: 'ambiguous',
|
|
488
|
+
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
|
|
489
|
+
alternatives: definitions.filter(d => d !== def).map(d => ({
|
|
490
|
+
file: d.relativePath,
|
|
491
|
+
line: d.startLine
|
|
492
|
+
}))
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return { def, definitions, warnings };
|
|
497
|
+
}
|
|
498
|
+
|
|
370
499
|
find(name, options = {}) {
|
|
371
500
|
const matches = this.symbols.get(name) || [];
|
|
372
501
|
|
|
@@ -693,30 +822,10 @@ class ProjectIndex {
|
|
|
693
822
|
* Get context for a symbol (callers + callees)
|
|
694
823
|
*/
|
|
695
824
|
context(name, options = {}) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
// Filter by file if specified
|
|
702
|
-
if (options.file) {
|
|
703
|
-
const filtered = definitions.filter(d =>
|
|
704
|
-
d.relativePath && d.relativePath.includes(options.file)
|
|
705
|
-
);
|
|
706
|
-
if (filtered.length > 0) {
|
|
707
|
-
definitions = filtered;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Prefer class/struct/interface definitions over functions/methods/constructors
|
|
712
|
-
// This ensures context('ClassName') finds the class, not a constructor with same name
|
|
713
|
-
const typeOrder = ['class', 'struct', 'interface', 'type', 'impl'];
|
|
714
|
-
let def = definitions[0];
|
|
715
|
-
for (const d of definitions) {
|
|
716
|
-
if (typeOrder.includes(d.type)) {
|
|
717
|
-
def = d;
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
825
|
+
const resolved = this.resolveSymbol(name, { file: options.file });
|
|
826
|
+
let { def, definitions, warnings } = resolved;
|
|
827
|
+
if (!def) {
|
|
828
|
+
return null;
|
|
720
829
|
}
|
|
721
830
|
|
|
722
831
|
// Special handling for class/struct/interface types
|
|
@@ -738,25 +847,28 @@ class ProjectIndex {
|
|
|
738
847
|
receiver: m.receiver
|
|
739
848
|
})),
|
|
740
849
|
// Also include places where the type is used in function parameters/returns
|
|
741
|
-
callers: this.findCallers(name, { includeMethods: options.includeMethods })
|
|
850
|
+
callers: this.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain })
|
|
742
851
|
};
|
|
743
852
|
|
|
744
|
-
if (
|
|
745
|
-
result.warnings =
|
|
746
|
-
type: 'ambiguous',
|
|
747
|
-
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
|
|
748
|
-
alternatives: definitions.slice(1).map(d => ({
|
|
749
|
-
file: d.relativePath,
|
|
750
|
-
line: d.startLine
|
|
751
|
-
}))
|
|
752
|
-
}];
|
|
853
|
+
if (warnings.length > 0) {
|
|
854
|
+
result.warnings = warnings;
|
|
753
855
|
}
|
|
754
856
|
|
|
755
857
|
return result;
|
|
756
858
|
}
|
|
757
859
|
|
|
758
|
-
const
|
|
759
|
-
const
|
|
860
|
+
const stats = { uncertain: 0 };
|
|
861
|
+
const callers = this.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
862
|
+
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
863
|
+
|
|
864
|
+
const filesInScope = new Set([def.file]);
|
|
865
|
+
callers.forEach(c => filesInScope.add(c.file));
|
|
866
|
+
callees.forEach(c => filesInScope.add(c.file));
|
|
867
|
+
let dynamicImports = 0;
|
|
868
|
+
for (const f of filesInScope) {
|
|
869
|
+
const fe = this.files.get(f);
|
|
870
|
+
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
871
|
+
}
|
|
760
872
|
|
|
761
873
|
const result = {
|
|
762
874
|
function: name,
|
|
@@ -766,19 +878,17 @@ class ProjectIndex {
|
|
|
766
878
|
params: def.params,
|
|
767
879
|
returnType: def.returnType,
|
|
768
880
|
callers,
|
|
769
|
-
callees
|
|
881
|
+
callees,
|
|
882
|
+
meta: {
|
|
883
|
+
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
884
|
+
skipped: 0,
|
|
885
|
+
dynamicImports,
|
|
886
|
+
uncertain: stats.uncertain
|
|
887
|
+
}
|
|
770
888
|
};
|
|
771
889
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
result.warnings = [{
|
|
775
|
-
type: 'ambiguous',
|
|
776
|
-
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
|
|
777
|
-
alternatives: definitions.slice(1).map(d => ({
|
|
778
|
-
file: d.relativePath,
|
|
779
|
-
line: d.startLine
|
|
780
|
-
}))
|
|
781
|
-
}];
|
|
890
|
+
if (warnings.length > 0) {
|
|
891
|
+
result.warnings = warnings;
|
|
782
892
|
}
|
|
783
893
|
|
|
784
894
|
return result;
|
|
@@ -859,6 +969,7 @@ class ProjectIndex {
|
|
|
859
969
|
*/
|
|
860
970
|
findCallers(name, options = {}) {
|
|
861
971
|
const callers = [];
|
|
972
|
+
const stats = options.stats;
|
|
862
973
|
|
|
863
974
|
// Get definition lines to exclude them
|
|
864
975
|
const definitions = this.symbols.get(name) || [];
|
|
@@ -879,6 +990,23 @@ class ProjectIndex {
|
|
|
879
990
|
// Skip if not matching our target name
|
|
880
991
|
if (call.name !== name) continue;
|
|
881
992
|
|
|
993
|
+
// Resolve binding within this file (without mutating cached call objects)
|
|
994
|
+
let bindingId = call.bindingId;
|
|
995
|
+
let isUncertain = call.uncertain;
|
|
996
|
+
if (!bindingId) {
|
|
997
|
+
const bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
|
|
998
|
+
if (bindings.length === 1) {
|
|
999
|
+
bindingId = bindings[0].id;
|
|
1000
|
+
} else if (bindings.length !== 0) {
|
|
1001
|
+
isUncertain = true;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (isUncertain && !options.includeUncertain) {
|
|
1006
|
+
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
882
1010
|
// Smart method call handling
|
|
883
1011
|
if (call.isMethod) {
|
|
884
1012
|
// Always skip this/self/cls calls (internal state access, not function calls)
|
|
@@ -891,6 +1019,12 @@ class ProjectIndex {
|
|
|
891
1019
|
// Skip definition lines
|
|
892
1020
|
if (definitionLines.has(`${filePath}:${call.line}`)) continue;
|
|
893
1021
|
|
|
1022
|
+
// If we have a binding id on definition, require match when available
|
|
1023
|
+
const targetBindingIds = new Set(definitions.map(d => d.bindingId).filter(Boolean));
|
|
1024
|
+
if (targetBindingIds.size > 0 && bindingId && !targetBindingIds.has(bindingId)) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
894
1028
|
// Find the enclosing function (get full symbol info)
|
|
895
1029
|
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
896
1030
|
|
|
@@ -975,7 +1109,7 @@ class ProjectIndex {
|
|
|
975
1109
|
const fileEntry = this.files.get(def.file);
|
|
976
1110
|
const language = fileEntry?.language;
|
|
977
1111
|
|
|
978
|
-
const callees = new Map(); //
|
|
1112
|
+
const callees = new Map(); // key -> { name, bindingId, count }
|
|
979
1113
|
|
|
980
1114
|
for (const call of calls) {
|
|
981
1115
|
// Filter to calls within this function's scope using enclosingFunction
|
|
@@ -993,7 +1127,56 @@ class ProjectIndex {
|
|
|
993
1127
|
// Skip keywords and built-ins
|
|
994
1128
|
if (this.isKeyword(call.name, language)) continue;
|
|
995
1129
|
|
|
996
|
-
|
|
1130
|
+
// Resolve binding within this file (without mutating cached call objects)
|
|
1131
|
+
let calleeKey = call.bindingId || call.name;
|
|
1132
|
+
let bindingResolved = call.bindingId;
|
|
1133
|
+
let isUncertain = call.uncertain;
|
|
1134
|
+
if (!call.bindingId && fileEntry?.bindings) {
|
|
1135
|
+
const bindings = fileEntry.bindings.filter(b => b.name === call.name);
|
|
1136
|
+
if (bindings.length === 1) {
|
|
1137
|
+
bindingResolved = bindings[0].id;
|
|
1138
|
+
calleeKey = bindingResolved;
|
|
1139
|
+
} else if (bindings.length > 1) {
|
|
1140
|
+
if (call.name === def.name) {
|
|
1141
|
+
// Calling same-name function (e.g., Java overloads)
|
|
1142
|
+
// Add ALL other overloads as potential callees
|
|
1143
|
+
const otherBindings = bindings.filter(b =>
|
|
1144
|
+
b.startLine !== def.startLine
|
|
1145
|
+
);
|
|
1146
|
+
for (const ob of otherBindings) {
|
|
1147
|
+
const existing = callees.get(ob.id);
|
|
1148
|
+
if (existing) {
|
|
1149
|
+
existing.count += 1;
|
|
1150
|
+
} else {
|
|
1151
|
+
callees.set(ob.id, {
|
|
1152
|
+
name: call.name,
|
|
1153
|
+
bindingId: ob.id,
|
|
1154
|
+
count: 1
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
continue; // Already added all overloads, skip normal add
|
|
1159
|
+
} else {
|
|
1160
|
+
isUncertain = true;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (isUncertain && !options.includeUncertain) {
|
|
1166
|
+
if (options.stats) options.stats.uncertain = (options.stats.uncertain || 0) + 1;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const existing = callees.get(calleeKey);
|
|
1171
|
+
if (existing) {
|
|
1172
|
+
existing.count += 1;
|
|
1173
|
+
} else {
|
|
1174
|
+
callees.set(calleeKey, {
|
|
1175
|
+
name: call.name,
|
|
1176
|
+
bindingId: bindingResolved,
|
|
1177
|
+
count: 1
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
997
1180
|
}
|
|
998
1181
|
|
|
999
1182
|
// Look up each callee in the symbol table
|
|
@@ -1002,16 +1185,24 @@ class ProjectIndex {
|
|
|
1002
1185
|
const defDir = path.dirname(def.file);
|
|
1003
1186
|
const defReceiver = def.receiver;
|
|
1004
1187
|
|
|
1005
|
-
for (const
|
|
1188
|
+
for (const { name: calleeName, bindingId, count } of callees.values()) {
|
|
1006
1189
|
const symbols = this.symbols.get(calleeName);
|
|
1007
1190
|
if (symbols && symbols.length > 0) {
|
|
1008
1191
|
let callee = symbols[0];
|
|
1009
1192
|
|
|
1010
|
-
// If
|
|
1011
|
-
if (symbols.length > 1) {
|
|
1012
|
-
|
|
1193
|
+
// If we have a binding ID, find the exact matching symbol
|
|
1194
|
+
if (bindingId && symbols.length > 1) {
|
|
1195
|
+
const exactMatch = symbols.find(s => s.bindingId === bindingId);
|
|
1196
|
+
if (exactMatch) {
|
|
1197
|
+
callee = exactMatch;
|
|
1198
|
+
}
|
|
1199
|
+
} else if (symbols.length > 1) {
|
|
1200
|
+
// Priority 1: Same file, but different definition (for overloads)
|
|
1201
|
+
const sameFileDifferent = symbols.find(s => s.file === def.file && s.startLine !== def.startLine);
|
|
1013
1202
|
const sameFile = symbols.find(s => s.file === def.file);
|
|
1014
|
-
if (
|
|
1203
|
+
if (sameFileDifferent && calleeName === def.name) {
|
|
1204
|
+
callee = sameFileDifferent;
|
|
1205
|
+
} else if (sameFile) {
|
|
1015
1206
|
callee = sameFile;
|
|
1016
1207
|
} else {
|
|
1017
1208
|
// Priority 2: Same directory (package)
|
|
@@ -1059,18 +1250,27 @@ class ProjectIndex {
|
|
|
1059
1250
|
* Smart extraction: function + dependencies
|
|
1060
1251
|
*/
|
|
1061
1252
|
smart(name, options = {}) {
|
|
1062
|
-
const
|
|
1063
|
-
if (
|
|
1253
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
1254
|
+
if (!def) {
|
|
1064
1255
|
return null;
|
|
1065
1256
|
}
|
|
1066
|
-
|
|
1067
|
-
const def = definitions[0];
|
|
1068
1257
|
const code = this.extractCode(def);
|
|
1069
|
-
const
|
|
1258
|
+
const stats = { uncertain: 0 };
|
|
1259
|
+
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
1260
|
+
|
|
1261
|
+
const filesInScope = new Set([def.file]);
|
|
1262
|
+
callees.forEach(c => filesInScope.add(c.file));
|
|
1263
|
+
let dynamicImports = 0;
|
|
1264
|
+
for (const f of filesInScope) {
|
|
1265
|
+
const fe = this.files.get(f);
|
|
1266
|
+
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
1267
|
+
}
|
|
1070
1268
|
|
|
1071
|
-
// Extract code for each dependency, excluding the
|
|
1269
|
+
// Extract code for each dependency, excluding the exact same function
|
|
1270
|
+
// (but keeping same-name overloads, e.g. Java toJson(Object) vs toJson(Object, Class))
|
|
1271
|
+
const defBindingId = def.bindingId;
|
|
1072
1272
|
const dependencies = callees
|
|
1073
|
-
.filter(callee => callee.
|
|
1273
|
+
.filter(callee => callee.bindingId !== defBindingId)
|
|
1074
1274
|
.map(callee => ({
|
|
1075
1275
|
...callee,
|
|
1076
1276
|
code: this.extractCode(callee)
|
|
@@ -1102,7 +1302,13 @@ class ProjectIndex {
|
|
|
1102
1302
|
code
|
|
1103
1303
|
},
|
|
1104
1304
|
dependencies,
|
|
1105
|
-
types
|
|
1305
|
+
types,
|
|
1306
|
+
meta: {
|
|
1307
|
+
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
1308
|
+
skipped: 0,
|
|
1309
|
+
dynamicImports,
|
|
1310
|
+
uncertain: stats.uncertain
|
|
1311
|
+
}
|
|
1106
1312
|
};
|
|
1107
1313
|
}
|
|
1108
1314
|
|
|
@@ -1414,8 +1620,13 @@ class ProjectIndex {
|
|
|
1414
1620
|
try {
|
|
1415
1621
|
const content = fs.readFileSync(importerPath, 'utf-8');
|
|
1416
1622
|
const lines = content.split('\n');
|
|
1417
|
-
|
|
1418
|
-
|
|
1623
|
+
let targetBasename = path.basename(targetPath, path.extname(targetPath));
|
|
1624
|
+
|
|
1625
|
+
// For __init__.py, search for the package name (parent dir)
|
|
1626
|
+
// e.g., "from tools import X" → search for "tools" not "__init__"
|
|
1627
|
+
if (targetBasename === '__init__') {
|
|
1628
|
+
targetBasename = path.basename(path.dirname(targetPath));
|
|
1629
|
+
}
|
|
1419
1630
|
|
|
1420
1631
|
for (let i = 0; i < lines.length; i++) {
|
|
1421
1632
|
if (lines[i].includes(targetBasename) &&
|
|
@@ -1788,14 +1999,24 @@ class ProjectIndex {
|
|
|
1788
1999
|
mods.includes('public') && mods.includes('static');
|
|
1789
2000
|
|
|
1790
2001
|
// Python: Magic/dunder methods are called by the interpreter, not user code
|
|
1791
|
-
|
|
2002
|
+
// test_* functions/methods are called by pytest/unittest via reflection
|
|
2003
|
+
const isPythonEntryPoint = lang === 'python' &&
|
|
2004
|
+
(/^__\w+__$/.test(name) || /^test_/.test(name));
|
|
1792
2005
|
|
|
1793
2006
|
// Rust: main() is entry point, #[test] functions are called by test runner
|
|
1794
2007
|
const isRustEntryPoint = lang === 'rust' &&
|
|
1795
2008
|
(name === 'main' || mods.includes('test'));
|
|
1796
2009
|
|
|
1797
|
-
|
|
1798
|
-
|
|
2010
|
+
// Go: Test*, Benchmark*, Example* functions are called by go test
|
|
2011
|
+
const isGoTestFunc = lang === 'go' &&
|
|
2012
|
+
/^(Test|Benchmark|Example)[A-Z]/.test(name);
|
|
2013
|
+
|
|
2014
|
+
// Java: @Test annotated methods are called by JUnit
|
|
2015
|
+
const isJavaTestMethod = lang === 'java' && mods.includes('test');
|
|
2016
|
+
|
|
2017
|
+
const isEntryPoint = isGoEntryPoint || isGoTestFunc ||
|
|
2018
|
+
isJavaEntryPoint || isJavaTestMethod ||
|
|
2019
|
+
isPythonEntryPoint || isRustEntryPoint;
|
|
1799
2020
|
|
|
1800
2021
|
const isExported = fileEntry && (
|
|
1801
2022
|
fileEntry.exports.includes(name) ||
|
|
@@ -2026,13 +2247,11 @@ class ProjectIndex {
|
|
|
2026
2247
|
* @param {string} name - Function name
|
|
2027
2248
|
* @returns {object} Related functions grouped by relationship type
|
|
2028
2249
|
*/
|
|
2029
|
-
related(name) {
|
|
2030
|
-
const
|
|
2031
|
-
if (!
|
|
2250
|
+
related(name, options = {}) {
|
|
2251
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2252
|
+
if (!def) {
|
|
2032
2253
|
return null;
|
|
2033
2254
|
}
|
|
2034
|
-
|
|
2035
|
-
const def = definitions[0];
|
|
2036
2255
|
const related = {
|
|
2037
2256
|
target: {
|
|
2038
2257
|
name: def.name,
|
|
@@ -2166,22 +2385,10 @@ class ProjectIndex {
|
|
|
2166
2385
|
const maxDepth = Math.max(0, rawDepth);
|
|
2167
2386
|
const direction = options.direction || 'down'; // 'down' = callees, 'up' = callers, 'both'
|
|
2168
2387
|
|
|
2169
|
-
|
|
2170
|
-
if (!
|
|
2388
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2389
|
+
if (!def) {
|
|
2171
2390
|
return null;
|
|
2172
2391
|
}
|
|
2173
|
-
|
|
2174
|
-
// Filter by file if specified
|
|
2175
|
-
if (options.file) {
|
|
2176
|
-
const filtered = definitions.filter(d =>
|
|
2177
|
-
d.relativePath && d.relativePath.includes(options.file)
|
|
2178
|
-
);
|
|
2179
|
-
if (filtered.length > 0) {
|
|
2180
|
-
definitions = filtered;
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
const def = definitions[0];
|
|
2185
2392
|
const visited = new Set();
|
|
2186
2393
|
const defDir = path.dirname(def.file);
|
|
2187
2394
|
|
|
@@ -2251,22 +2458,10 @@ class ProjectIndex {
|
|
|
2251
2458
|
* @returns {object} Impact analysis
|
|
2252
2459
|
*/
|
|
2253
2460
|
impact(name, options = {}) {
|
|
2254
|
-
|
|
2255
|
-
if (!
|
|
2461
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2462
|
+
if (!def) {
|
|
2256
2463
|
return null;
|
|
2257
2464
|
}
|
|
2258
|
-
|
|
2259
|
-
// Filter by file if specified
|
|
2260
|
-
if (options.file) {
|
|
2261
|
-
const filtered = definitions.filter(d =>
|
|
2262
|
-
d.relativePath && d.relativePath.includes(options.file)
|
|
2263
|
-
);
|
|
2264
|
-
if (filtered.length > 0) {
|
|
2265
|
-
definitions = filtered;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
const def = definitions[0];
|
|
2270
2465
|
const usages = this.usages(name, { codeOnly: true });
|
|
2271
2466
|
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2272
2467
|
|
|
@@ -2747,17 +2942,26 @@ class ProjectIndex {
|
|
|
2747
2942
|
* @param {string} name - Function name
|
|
2748
2943
|
* @returns {object} Verification results with mismatches
|
|
2749
2944
|
*/
|
|
2750
|
-
verify(name) {
|
|
2751
|
-
const
|
|
2752
|
-
if (!
|
|
2945
|
+
verify(name, options = {}) {
|
|
2946
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2947
|
+
if (!def) {
|
|
2753
2948
|
return { found: false, function: name };
|
|
2754
2949
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
const
|
|
2758
|
-
const
|
|
2950
|
+
// For Python/Rust methods, exclude self/cls from parameter count
|
|
2951
|
+
// (callers don't pass self/cls explicitly: obj.method(a, b) not obj.method(obj, a, b))
|
|
2952
|
+
const fileEntry = this.files.get(def.file);
|
|
2953
|
+
const lang = fileEntry?.language;
|
|
2954
|
+
let params = def.paramsStructured || [];
|
|
2955
|
+
if ((lang === 'python' || lang === 'rust') && params.length > 0) {
|
|
2956
|
+
const firstName = params[0].name;
|
|
2957
|
+
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
2958
|
+
params = params.slice(1);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
const expectedParamCount = params.length;
|
|
2962
|
+
const optionalCount = params.filter(p => p.optional || p.default !== undefined).length;
|
|
2759
2963
|
const minArgs = expectedParamCount - optionalCount;
|
|
2760
|
-
const hasRest =
|
|
2964
|
+
const hasRest = params.some(p => p.rest);
|
|
2761
2965
|
|
|
2762
2966
|
// Get all call sites
|
|
2763
2967
|
const usages = this.usages(name, { codeOnly: true });
|
|
@@ -2767,9 +2971,18 @@ class ProjectIndex {
|
|
|
2767
2971
|
const mismatches = [];
|
|
2768
2972
|
const uncertain = [];
|
|
2769
2973
|
|
|
2974
|
+
// If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
|
|
2975
|
+
// This prevents false positives where a standalone function name matches method calls
|
|
2976
|
+
const defIsMethod = def.isMethod || def.type === 'method' || def.className;
|
|
2977
|
+
|
|
2770
2978
|
for (const call of calls) {
|
|
2771
2979
|
const analysis = this.analyzeCallSite(call, name);
|
|
2772
2980
|
|
|
2981
|
+
// Skip method calls when verifying a non-method definition
|
|
2982
|
+
if (analysis.isMethodCall && !defIsMethod) {
|
|
2983
|
+
continue;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2773
2986
|
if (analysis.args === null) {
|
|
2774
2987
|
// Couldn't parse arguments
|
|
2775
2988
|
uncertain.push({
|
|
@@ -2835,13 +3048,13 @@ class ProjectIndex {
|
|
|
2835
3048
|
file: def.relativePath,
|
|
2836
3049
|
startLine: def.startLine,
|
|
2837
3050
|
signature: this.formatSignature(def),
|
|
2838
|
-
params:
|
|
3051
|
+
params: params.map(p => ({
|
|
2839
3052
|
name: p.name,
|
|
2840
3053
|
optional: p.optional || p.default !== undefined,
|
|
2841
3054
|
hasDefault: p.default !== undefined
|
|
2842
|
-
}))
|
|
3055
|
+
})),
|
|
2843
3056
|
expectedArgs: { min: minArgs, max: hasRest ? '∞' : expectedParamCount },
|
|
2844
|
-
totalCalls:
|
|
3057
|
+
totalCalls: valid.length + mismatches.length + uncertain.length,
|
|
2845
3058
|
valid: valid.length,
|
|
2846
3059
|
mismatches: mismatches.length,
|
|
2847
3060
|
uncertain: uncertain.length,
|
|
@@ -2882,8 +3095,23 @@ class ProjectIndex {
|
|
|
2882
3095
|
const callNode = this._findCallNode(tree.rootNode, callTypes, targetRow, funcName);
|
|
2883
3096
|
if (!callNode) return { args: null, argCount: 0 };
|
|
2884
3097
|
|
|
3098
|
+
// Check if this is a method call (obj.func()) vs a direct call (func())
|
|
3099
|
+
const funcNode = callNode.childForFieldName('function') ||
|
|
3100
|
+
callNode.childForFieldName('name');
|
|
3101
|
+
let isMethodCall = false;
|
|
3102
|
+
if (funcNode) {
|
|
3103
|
+
// member_expression (JS), attribute (Python), selector_expression (Go), field_expression (Rust)
|
|
3104
|
+
if (['member_expression', 'attribute', 'selector_expression', 'field_expression'].includes(funcNode.type)) {
|
|
3105
|
+
isMethodCall = true;
|
|
3106
|
+
}
|
|
3107
|
+
// Java method_invocation with object
|
|
3108
|
+
if (callNode.type === 'method_invocation' && callNode.childForFieldName('object')) {
|
|
3109
|
+
isMethodCall = true;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
|
|
2885
3113
|
const argsNode = callNode.childForFieldName('arguments');
|
|
2886
|
-
if (!argsNode) return { args: [], argCount: 0 };
|
|
3114
|
+
if (!argsNode) return { args: [], argCount: 0, isMethodCall };
|
|
2887
3115
|
|
|
2888
3116
|
const args = [];
|
|
2889
3117
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
@@ -2894,7 +3122,8 @@ class ProjectIndex {
|
|
|
2894
3122
|
args,
|
|
2895
3123
|
argCount: args.length,
|
|
2896
3124
|
hasSpread: args.some(a => a.startsWith('...')),
|
|
2897
|
-
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a))
|
|
3125
|
+
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
|
|
3126
|
+
isMethodCall
|
|
2898
3127
|
};
|
|
2899
3128
|
} catch (e) {
|
|
2900
3129
|
return { args: null, argCount: 0 };
|
|
@@ -3000,9 +3229,12 @@ class ProjectIndex {
|
|
|
3000
3229
|
};
|
|
3001
3230
|
}
|
|
3002
3231
|
|
|
3003
|
-
// Use
|
|
3004
|
-
const
|
|
3005
|
-
const
|
|
3232
|
+
// Use resolveSymbol for consistent primary selection (prefers non-test files)
|
|
3233
|
+
const { def: resolved } = this.resolveSymbol(name, { file: options.file });
|
|
3234
|
+
const primary = resolved || definitions[0];
|
|
3235
|
+
const others = definitions.filter(d =>
|
|
3236
|
+
d.relativePath !== primary.relativePath || d.startLine !== primary.startLine
|
|
3237
|
+
);
|
|
3006
3238
|
|
|
3007
3239
|
// Use the actual symbol name (may differ from query if fuzzy matched)
|
|
3008
3240
|
const symbolName = primary.name;
|
|
@@ -3268,42 +3500,89 @@ class ProjectIndex {
|
|
|
3268
3500
|
/**
|
|
3269
3501
|
* Get TOC for all files
|
|
3270
3502
|
*/
|
|
3271
|
-
getToc() {
|
|
3503
|
+
getToc(options = {}) {
|
|
3272
3504
|
const files = [];
|
|
3273
3505
|
let totalFunctions = 0;
|
|
3274
3506
|
let totalClasses = 0;
|
|
3275
3507
|
let totalState = 0;
|
|
3276
3508
|
let totalLines = 0;
|
|
3509
|
+
let totalDynamic = 0;
|
|
3510
|
+
let totalTests = 0;
|
|
3277
3511
|
|
|
3278
3512
|
for (const [filePath, fileEntry] of this.files) {
|
|
3279
|
-
|
|
3513
|
+
let functions = fileEntry.symbols.filter(s => s.type === 'function');
|
|
3280
3514
|
const classes = fileEntry.symbols.filter(s =>
|
|
3281
3515
|
['class', 'interface', 'type', 'enum', 'struct', 'trait', 'impl'].includes(s.type)
|
|
3282
3516
|
);
|
|
3283
3517
|
const state = fileEntry.symbols.filter(s => s.type === 'state');
|
|
3284
3518
|
|
|
3519
|
+
if (options.topLevel) {
|
|
3520
|
+
functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3285
3523
|
totalFunctions += functions.length;
|
|
3286
3524
|
totalClasses += classes.length;
|
|
3287
3525
|
totalState += state.length;
|
|
3288
3526
|
totalLines += fileEntry.lines;
|
|
3527
|
+
totalDynamic += fileEntry.dynamicImports || 0;
|
|
3528
|
+
if (isTestFile(filePath)) totalTests += 1;
|
|
3289
3529
|
|
|
3290
|
-
|
|
3530
|
+
const entry = {
|
|
3291
3531
|
file: fileEntry.relativePath,
|
|
3292
3532
|
language: fileEntry.language,
|
|
3293
3533
|
lines: fileEntry.lines,
|
|
3294
|
-
functions,
|
|
3295
|
-
classes,
|
|
3296
|
-
state
|
|
3297
|
-
}
|
|
3534
|
+
functions: functions.length,
|
|
3535
|
+
classes: classes.length,
|
|
3536
|
+
state: state.length
|
|
3537
|
+
};
|
|
3538
|
+
|
|
3539
|
+
if (options.detailed) {
|
|
3540
|
+
entry.symbols = { functions, classes, state };
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
files.push(entry);
|
|
3298
3544
|
}
|
|
3299
3545
|
|
|
3546
|
+
// Hints: top files by function count and lines
|
|
3547
|
+
const topFunctionFiles = [...files]
|
|
3548
|
+
.sort((a, b) => b.functions - a.functions || b.lines - a.lines)
|
|
3549
|
+
.filter(f => f.functions > 0)
|
|
3550
|
+
.slice(0, 3)
|
|
3551
|
+
.map(f => ({ file: f.file, functions: f.functions }));
|
|
3552
|
+
|
|
3553
|
+
const topLineFiles = [...files]
|
|
3554
|
+
.sort((a, b) => b.lines - a.lines)
|
|
3555
|
+
.slice(0, 3)
|
|
3556
|
+
.map(f => ({ file: f.file, lines: f.lines }));
|
|
3557
|
+
|
|
3558
|
+
// Entry point candidates
|
|
3559
|
+
const entryPattern = /(main|index|server|app)\.(js|jsx|ts|tsx|py|go|rs|java)$/i;
|
|
3560
|
+
const entryFiles = files
|
|
3561
|
+
.filter(f => entryPattern.test(f.file))
|
|
3562
|
+
.slice(0, 5)
|
|
3563
|
+
.map(f => f.file);
|
|
3564
|
+
|
|
3300
3565
|
return {
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3566
|
+
meta: {
|
|
3567
|
+
complete: totalDynamic === 0,
|
|
3568
|
+
skipped: 0,
|
|
3569
|
+
dynamicImports: totalDynamic,
|
|
3570
|
+
uncertain: 0
|
|
3571
|
+
},
|
|
3572
|
+
totals: {
|
|
3573
|
+
files: files.length,
|
|
3574
|
+
lines: totalLines,
|
|
3575
|
+
functions: totalFunctions,
|
|
3576
|
+
classes: totalClasses,
|
|
3577
|
+
state: totalState,
|
|
3578
|
+
testFiles: totalTests
|
|
3579
|
+
},
|
|
3580
|
+
summary: {
|
|
3581
|
+
topFunctionFiles,
|
|
3582
|
+
topLineFiles,
|
|
3583
|
+
entryFiles
|
|
3584
|
+
},
|
|
3585
|
+
files
|
|
3307
3586
|
};
|
|
3308
3587
|
}
|
|
3309
3588
|
|