ucn 3.2.0 → 3.3.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 +145 -43
- package/core/imports.js +153 -4
- package/core/output.js +129 -147
- package/core/project.js +365 -122
- package/languages/go.js +21 -10
- package/languages/java.js +25 -9
- 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 +967 -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,41 @@ 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 = [];
|
|
239
252
|
|
|
240
253
|
for (const importModule of fileEntry.imports) {
|
|
241
|
-
|
|
254
|
+
let resolved = resolveImport(importModule, filePath, {
|
|
242
255
|
aliases: this.config.aliases,
|
|
243
256
|
language: fileEntry.language,
|
|
244
257
|
root: this.root
|
|
245
258
|
});
|
|
246
259
|
|
|
260
|
+
// Java package imports: resolve by matching file path suffix
|
|
261
|
+
// e.g., "com.google.gson.Gson" -> find file ending in "com/google/gson/Gson.java"
|
|
262
|
+
if (!resolved && fileEntry.language === 'java' &&
|
|
263
|
+
!importModule.startsWith('.') && !importModule.endsWith('.*')) {
|
|
264
|
+
if (!javaSuffixMap) {
|
|
265
|
+
javaSuffixMap = new Map();
|
|
266
|
+
for (const [absPath, entry] of this.files) {
|
|
267
|
+
if (entry.language === 'java') {
|
|
268
|
+
javaSuffixMap.set(absPath, absPath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const fileSuffix = '/' + importModule.split('.').join('/') + '.java';
|
|
273
|
+
for (const absPath of javaSuffixMap.keys()) {
|
|
274
|
+
if (absPath.endsWith(fileSuffix)) {
|
|
275
|
+
resolved = absPath;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
247
281
|
if (resolved && this.files.has(resolved)) {
|
|
248
282
|
importedFiles.push(resolved);
|
|
249
283
|
|
|
@@ -287,6 +321,17 @@ class ProjectIndex {
|
|
|
287
321
|
}
|
|
288
322
|
}
|
|
289
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Count dynamic imports across indexed files
|
|
326
|
+
*/
|
|
327
|
+
getDynamicImportCount() {
|
|
328
|
+
let total = 0;
|
|
329
|
+
for (const fileEntry of this.files.values()) {
|
|
330
|
+
total += fileEntry.dynamicImports || 0;
|
|
331
|
+
}
|
|
332
|
+
return total;
|
|
333
|
+
}
|
|
334
|
+
|
|
290
335
|
// ========================================================================
|
|
291
336
|
// QUERY METHODS
|
|
292
337
|
// ========================================================================
|
|
@@ -367,6 +412,80 @@ class ProjectIndex {
|
|
|
367
412
|
* @param {object} options - { file, prefer, exact, exclude, in }
|
|
368
413
|
* @returns {Array} Matching symbols with usage counts
|
|
369
414
|
*/
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Resolve a symbol name to the best matching definition.
|
|
418
|
+
* Centralized selection logic used by all commands for consistency.
|
|
419
|
+
*
|
|
420
|
+
* Priority order:
|
|
421
|
+
* 1. Filter by --file if specified
|
|
422
|
+
* 2. Prefer class/struct/interface/type over functions/constructors
|
|
423
|
+
* 3. Prefer non-test file definitions over test files
|
|
424
|
+
* 4. Prefer higher usage count
|
|
425
|
+
*
|
|
426
|
+
* @param {string} name - Symbol name
|
|
427
|
+
* @param {object} [options] - { file }
|
|
428
|
+
* @returns {{ def: object|null, definitions: Array, warnings: Array }}
|
|
429
|
+
*/
|
|
430
|
+
resolveSymbol(name, options = {}) {
|
|
431
|
+
let definitions = this.symbols.get(name) || [];
|
|
432
|
+
if (definitions.length === 0) {
|
|
433
|
+
return { def: null, definitions: [], warnings: [] };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Filter by file if specified
|
|
437
|
+
if (options.file) {
|
|
438
|
+
const filtered = definitions.filter(d =>
|
|
439
|
+
d.relativePath && d.relativePath.includes(options.file)
|
|
440
|
+
);
|
|
441
|
+
if (filtered.length > 0) {
|
|
442
|
+
definitions = filtered;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Score each definition for selection
|
|
447
|
+
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
448
|
+
const scored = definitions.map(d => {
|
|
449
|
+
let score = 0;
|
|
450
|
+
const rp = d.relativePath || '';
|
|
451
|
+
// Prefer class/struct/interface types (+1000)
|
|
452
|
+
if (typeOrder.has(d.type)) score += 1000;
|
|
453
|
+
// Deprioritize test files (-500)
|
|
454
|
+
if (isTestFile(rp, detectLanguage(d.file))) {
|
|
455
|
+
score -= 500;
|
|
456
|
+
}
|
|
457
|
+
// Deprioritize examples/docs/vendor directories (-300)
|
|
458
|
+
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) {
|
|
459
|
+
score -= 300;
|
|
460
|
+
}
|
|
461
|
+
// Boost lib/src/core/internal directories (+200)
|
|
462
|
+
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) {
|
|
463
|
+
score += 200;
|
|
464
|
+
}
|
|
465
|
+
return { def: d, score };
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Sort by score descending, then by index order for stability
|
|
469
|
+
scored.sort((a, b) => b.score - a.score);
|
|
470
|
+
|
|
471
|
+
const def = scored[0].def;
|
|
472
|
+
|
|
473
|
+
// Build warnings
|
|
474
|
+
const warnings = [];
|
|
475
|
+
if (definitions.length > 1) {
|
|
476
|
+
warnings.push({
|
|
477
|
+
type: 'ambiguous',
|
|
478
|
+
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
|
|
479
|
+
alternatives: definitions.filter(d => d !== def).map(d => ({
|
|
480
|
+
file: d.relativePath,
|
|
481
|
+
line: d.startLine
|
|
482
|
+
}))
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { def, definitions, warnings };
|
|
487
|
+
}
|
|
488
|
+
|
|
370
489
|
find(name, options = {}) {
|
|
371
490
|
const matches = this.symbols.get(name) || [];
|
|
372
491
|
|
|
@@ -693,30 +812,10 @@ class ProjectIndex {
|
|
|
693
812
|
* Get context for a symbol (callers + callees)
|
|
694
813
|
*/
|
|
695
814
|
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
|
-
}
|
|
815
|
+
const resolved = this.resolveSymbol(name, { file: options.file });
|
|
816
|
+
let { def, definitions, warnings } = resolved;
|
|
817
|
+
if (!def) {
|
|
818
|
+
return null;
|
|
720
819
|
}
|
|
721
820
|
|
|
722
821
|
// Special handling for class/struct/interface types
|
|
@@ -738,25 +837,28 @@ class ProjectIndex {
|
|
|
738
837
|
receiver: m.receiver
|
|
739
838
|
})),
|
|
740
839
|
// Also include places where the type is used in function parameters/returns
|
|
741
|
-
callers: this.findCallers(name, { includeMethods: options.includeMethods })
|
|
840
|
+
callers: this.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain })
|
|
742
841
|
};
|
|
743
842
|
|
|
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
|
-
}];
|
|
843
|
+
if (warnings.length > 0) {
|
|
844
|
+
result.warnings = warnings;
|
|
753
845
|
}
|
|
754
846
|
|
|
755
847
|
return result;
|
|
756
848
|
}
|
|
757
849
|
|
|
758
|
-
const
|
|
759
|
-
const
|
|
850
|
+
const stats = { uncertain: 0 };
|
|
851
|
+
const callers = this.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
852
|
+
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
853
|
+
|
|
854
|
+
const filesInScope = new Set([def.file]);
|
|
855
|
+
callers.forEach(c => filesInScope.add(c.file));
|
|
856
|
+
callees.forEach(c => filesInScope.add(c.file));
|
|
857
|
+
let dynamicImports = 0;
|
|
858
|
+
for (const f of filesInScope) {
|
|
859
|
+
const fe = this.files.get(f);
|
|
860
|
+
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
861
|
+
}
|
|
760
862
|
|
|
761
863
|
const result = {
|
|
762
864
|
function: name,
|
|
@@ -766,19 +868,17 @@ class ProjectIndex {
|
|
|
766
868
|
params: def.params,
|
|
767
869
|
returnType: def.returnType,
|
|
768
870
|
callers,
|
|
769
|
-
callees
|
|
871
|
+
callees,
|
|
872
|
+
meta: {
|
|
873
|
+
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
874
|
+
skipped: 0,
|
|
875
|
+
dynamicImports,
|
|
876
|
+
uncertain: stats.uncertain
|
|
877
|
+
}
|
|
770
878
|
};
|
|
771
879
|
|
|
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
|
-
}];
|
|
880
|
+
if (warnings.length > 0) {
|
|
881
|
+
result.warnings = warnings;
|
|
782
882
|
}
|
|
783
883
|
|
|
784
884
|
return result;
|
|
@@ -859,6 +959,7 @@ class ProjectIndex {
|
|
|
859
959
|
*/
|
|
860
960
|
findCallers(name, options = {}) {
|
|
861
961
|
const callers = [];
|
|
962
|
+
const stats = options.stats;
|
|
862
963
|
|
|
863
964
|
// Get definition lines to exclude them
|
|
864
965
|
const definitions = this.symbols.get(name) || [];
|
|
@@ -879,6 +980,23 @@ class ProjectIndex {
|
|
|
879
980
|
// Skip if not matching our target name
|
|
880
981
|
if (call.name !== name) continue;
|
|
881
982
|
|
|
983
|
+
// Resolve binding within this file (without mutating cached call objects)
|
|
984
|
+
let bindingId = call.bindingId;
|
|
985
|
+
let isUncertain = call.uncertain;
|
|
986
|
+
if (!bindingId) {
|
|
987
|
+
const bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
|
|
988
|
+
if (bindings.length === 1) {
|
|
989
|
+
bindingId = bindings[0].id;
|
|
990
|
+
} else if (bindings.length !== 0) {
|
|
991
|
+
isUncertain = true;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (isUncertain && !options.includeUncertain) {
|
|
996
|
+
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
|
|
882
1000
|
// Smart method call handling
|
|
883
1001
|
if (call.isMethod) {
|
|
884
1002
|
// Always skip this/self/cls calls (internal state access, not function calls)
|
|
@@ -891,6 +1009,12 @@ class ProjectIndex {
|
|
|
891
1009
|
// Skip definition lines
|
|
892
1010
|
if (definitionLines.has(`${filePath}:${call.line}`)) continue;
|
|
893
1011
|
|
|
1012
|
+
// If we have a binding id on definition, require match when available
|
|
1013
|
+
const targetBindingIds = new Set(definitions.map(d => d.bindingId).filter(Boolean));
|
|
1014
|
+
if (targetBindingIds.size > 0 && bindingId && !targetBindingIds.has(bindingId)) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
894
1018
|
// Find the enclosing function (get full symbol info)
|
|
895
1019
|
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
896
1020
|
|
|
@@ -975,7 +1099,7 @@ class ProjectIndex {
|
|
|
975
1099
|
const fileEntry = this.files.get(def.file);
|
|
976
1100
|
const language = fileEntry?.language;
|
|
977
1101
|
|
|
978
|
-
const callees = new Map(); //
|
|
1102
|
+
const callees = new Map(); // key -> { name, bindingId, count }
|
|
979
1103
|
|
|
980
1104
|
for (const call of calls) {
|
|
981
1105
|
// Filter to calls within this function's scope using enclosingFunction
|
|
@@ -993,7 +1117,56 @@ class ProjectIndex {
|
|
|
993
1117
|
// Skip keywords and built-ins
|
|
994
1118
|
if (this.isKeyword(call.name, language)) continue;
|
|
995
1119
|
|
|
996
|
-
|
|
1120
|
+
// Resolve binding within this file (without mutating cached call objects)
|
|
1121
|
+
let calleeKey = call.bindingId || call.name;
|
|
1122
|
+
let bindingResolved = call.bindingId;
|
|
1123
|
+
let isUncertain = call.uncertain;
|
|
1124
|
+
if (!call.bindingId && fileEntry?.bindings) {
|
|
1125
|
+
const bindings = fileEntry.bindings.filter(b => b.name === call.name);
|
|
1126
|
+
if (bindings.length === 1) {
|
|
1127
|
+
bindingResolved = bindings[0].id;
|
|
1128
|
+
calleeKey = bindingResolved;
|
|
1129
|
+
} else if (bindings.length > 1) {
|
|
1130
|
+
if (call.name === def.name) {
|
|
1131
|
+
// Calling same-name function (e.g., Java overloads)
|
|
1132
|
+
// Add ALL other overloads as potential callees
|
|
1133
|
+
const otherBindings = bindings.filter(b =>
|
|
1134
|
+
b.startLine !== def.startLine
|
|
1135
|
+
);
|
|
1136
|
+
for (const ob of otherBindings) {
|
|
1137
|
+
const existing = callees.get(ob.id);
|
|
1138
|
+
if (existing) {
|
|
1139
|
+
existing.count += 1;
|
|
1140
|
+
} else {
|
|
1141
|
+
callees.set(ob.id, {
|
|
1142
|
+
name: call.name,
|
|
1143
|
+
bindingId: ob.id,
|
|
1144
|
+
count: 1
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
continue; // Already added all overloads, skip normal add
|
|
1149
|
+
} else {
|
|
1150
|
+
isUncertain = true;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (isUncertain && !options.includeUncertain) {
|
|
1156
|
+
if (options.stats) options.stats.uncertain = (options.stats.uncertain || 0) + 1;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const existing = callees.get(calleeKey);
|
|
1161
|
+
if (existing) {
|
|
1162
|
+
existing.count += 1;
|
|
1163
|
+
} else {
|
|
1164
|
+
callees.set(calleeKey, {
|
|
1165
|
+
name: call.name,
|
|
1166
|
+
bindingId: bindingResolved,
|
|
1167
|
+
count: 1
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
997
1170
|
}
|
|
998
1171
|
|
|
999
1172
|
// Look up each callee in the symbol table
|
|
@@ -1002,16 +1175,24 @@ class ProjectIndex {
|
|
|
1002
1175
|
const defDir = path.dirname(def.file);
|
|
1003
1176
|
const defReceiver = def.receiver;
|
|
1004
1177
|
|
|
1005
|
-
for (const
|
|
1178
|
+
for (const { name: calleeName, bindingId, count } of callees.values()) {
|
|
1006
1179
|
const symbols = this.symbols.get(calleeName);
|
|
1007
1180
|
if (symbols && symbols.length > 0) {
|
|
1008
1181
|
let callee = symbols[0];
|
|
1009
1182
|
|
|
1010
|
-
// If
|
|
1011
|
-
if (symbols.length > 1) {
|
|
1012
|
-
|
|
1183
|
+
// If we have a binding ID, find the exact matching symbol
|
|
1184
|
+
if (bindingId && symbols.length > 1) {
|
|
1185
|
+
const exactMatch = symbols.find(s => s.bindingId === bindingId);
|
|
1186
|
+
if (exactMatch) {
|
|
1187
|
+
callee = exactMatch;
|
|
1188
|
+
}
|
|
1189
|
+
} else if (symbols.length > 1) {
|
|
1190
|
+
// Priority 1: Same file, but different definition (for overloads)
|
|
1191
|
+
const sameFileDifferent = symbols.find(s => s.file === def.file && s.startLine !== def.startLine);
|
|
1013
1192
|
const sameFile = symbols.find(s => s.file === def.file);
|
|
1014
|
-
if (
|
|
1193
|
+
if (sameFileDifferent && calleeName === def.name) {
|
|
1194
|
+
callee = sameFileDifferent;
|
|
1195
|
+
} else if (sameFile) {
|
|
1015
1196
|
callee = sameFile;
|
|
1016
1197
|
} else {
|
|
1017
1198
|
// Priority 2: Same directory (package)
|
|
@@ -1059,18 +1240,27 @@ class ProjectIndex {
|
|
|
1059
1240
|
* Smart extraction: function + dependencies
|
|
1060
1241
|
*/
|
|
1061
1242
|
smart(name, options = {}) {
|
|
1062
|
-
const
|
|
1063
|
-
if (
|
|
1243
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
1244
|
+
if (!def) {
|
|
1064
1245
|
return null;
|
|
1065
1246
|
}
|
|
1066
|
-
|
|
1067
|
-
const def = definitions[0];
|
|
1068
1247
|
const code = this.extractCode(def);
|
|
1069
|
-
const
|
|
1248
|
+
const stats = { uncertain: 0 };
|
|
1249
|
+
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
1250
|
+
|
|
1251
|
+
const filesInScope = new Set([def.file]);
|
|
1252
|
+
callees.forEach(c => filesInScope.add(c.file));
|
|
1253
|
+
let dynamicImports = 0;
|
|
1254
|
+
for (const f of filesInScope) {
|
|
1255
|
+
const fe = this.files.get(f);
|
|
1256
|
+
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
1257
|
+
}
|
|
1070
1258
|
|
|
1071
|
-
// Extract code for each dependency, excluding the
|
|
1259
|
+
// Extract code for each dependency, excluding the exact same function
|
|
1260
|
+
// (but keeping same-name overloads, e.g. Java toJson(Object) vs toJson(Object, Class))
|
|
1261
|
+
const defBindingId = def.bindingId;
|
|
1072
1262
|
const dependencies = callees
|
|
1073
|
-
.filter(callee => callee.
|
|
1263
|
+
.filter(callee => callee.bindingId !== defBindingId)
|
|
1074
1264
|
.map(callee => ({
|
|
1075
1265
|
...callee,
|
|
1076
1266
|
code: this.extractCode(callee)
|
|
@@ -1102,7 +1292,13 @@ class ProjectIndex {
|
|
|
1102
1292
|
code
|
|
1103
1293
|
},
|
|
1104
1294
|
dependencies,
|
|
1105
|
-
types
|
|
1295
|
+
types,
|
|
1296
|
+
meta: {
|
|
1297
|
+
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
1298
|
+
skipped: 0,
|
|
1299
|
+
dynamicImports,
|
|
1300
|
+
uncertain: stats.uncertain
|
|
1301
|
+
}
|
|
1106
1302
|
};
|
|
1107
1303
|
}
|
|
1108
1304
|
|
|
@@ -2026,13 +2222,11 @@ class ProjectIndex {
|
|
|
2026
2222
|
* @param {string} name - Function name
|
|
2027
2223
|
* @returns {object} Related functions grouped by relationship type
|
|
2028
2224
|
*/
|
|
2029
|
-
related(name) {
|
|
2030
|
-
const
|
|
2031
|
-
if (!
|
|
2225
|
+
related(name, options = {}) {
|
|
2226
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2227
|
+
if (!def) {
|
|
2032
2228
|
return null;
|
|
2033
2229
|
}
|
|
2034
|
-
|
|
2035
|
-
const def = definitions[0];
|
|
2036
2230
|
const related = {
|
|
2037
2231
|
target: {
|
|
2038
2232
|
name: def.name,
|
|
@@ -2166,22 +2360,10 @@ class ProjectIndex {
|
|
|
2166
2360
|
const maxDepth = Math.max(0, rawDepth);
|
|
2167
2361
|
const direction = options.direction || 'down'; // 'down' = callees, 'up' = callers, 'both'
|
|
2168
2362
|
|
|
2169
|
-
|
|
2170
|
-
if (!
|
|
2363
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2364
|
+
if (!def) {
|
|
2171
2365
|
return null;
|
|
2172
2366
|
}
|
|
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
2367
|
const visited = new Set();
|
|
2186
2368
|
const defDir = path.dirname(def.file);
|
|
2187
2369
|
|
|
@@ -2251,22 +2433,10 @@ class ProjectIndex {
|
|
|
2251
2433
|
* @returns {object} Impact analysis
|
|
2252
2434
|
*/
|
|
2253
2435
|
impact(name, options = {}) {
|
|
2254
|
-
|
|
2255
|
-
if (!
|
|
2436
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2437
|
+
if (!def) {
|
|
2256
2438
|
return null;
|
|
2257
2439
|
}
|
|
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
2440
|
const usages = this.usages(name, { codeOnly: true });
|
|
2271
2441
|
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2272
2442
|
|
|
@@ -2747,13 +2917,11 @@ class ProjectIndex {
|
|
|
2747
2917
|
* @param {string} name - Function name
|
|
2748
2918
|
* @returns {object} Verification results with mismatches
|
|
2749
2919
|
*/
|
|
2750
|
-
verify(name) {
|
|
2751
|
-
const
|
|
2752
|
-
if (!
|
|
2920
|
+
verify(name, options = {}) {
|
|
2921
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2922
|
+
if (!def) {
|
|
2753
2923
|
return { found: false, function: name };
|
|
2754
2924
|
}
|
|
2755
|
-
|
|
2756
|
-
const def = definitions[0];
|
|
2757
2925
|
const expectedParamCount = def.paramsStructured?.length || 0;
|
|
2758
2926
|
const optionalCount = (def.paramsStructured || []).filter(p => p.optional || p.default !== undefined).length;
|
|
2759
2927
|
const minArgs = expectedParamCount - optionalCount;
|
|
@@ -2767,9 +2935,18 @@ class ProjectIndex {
|
|
|
2767
2935
|
const mismatches = [];
|
|
2768
2936
|
const uncertain = [];
|
|
2769
2937
|
|
|
2938
|
+
// If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
|
|
2939
|
+
// This prevents false positives where a standalone function name matches method calls
|
|
2940
|
+
const defIsMethod = def.isMethod || def.type === 'method' || def.className;
|
|
2941
|
+
|
|
2770
2942
|
for (const call of calls) {
|
|
2771
2943
|
const analysis = this.analyzeCallSite(call, name);
|
|
2772
2944
|
|
|
2945
|
+
// Skip method calls when verifying a non-method definition
|
|
2946
|
+
if (analysis.isMethodCall && !defIsMethod) {
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2773
2950
|
if (analysis.args === null) {
|
|
2774
2951
|
// Couldn't parse arguments
|
|
2775
2952
|
uncertain.push({
|
|
@@ -2841,7 +3018,7 @@ class ProjectIndex {
|
|
|
2841
3018
|
hasDefault: p.default !== undefined
|
|
2842
3019
|
})) || [],
|
|
2843
3020
|
expectedArgs: { min: minArgs, max: hasRest ? '∞' : expectedParamCount },
|
|
2844
|
-
totalCalls:
|
|
3021
|
+
totalCalls: valid.length + mismatches.length + uncertain.length,
|
|
2845
3022
|
valid: valid.length,
|
|
2846
3023
|
mismatches: mismatches.length,
|
|
2847
3024
|
uncertain: uncertain.length,
|
|
@@ -2882,8 +3059,23 @@ class ProjectIndex {
|
|
|
2882
3059
|
const callNode = this._findCallNode(tree.rootNode, callTypes, targetRow, funcName);
|
|
2883
3060
|
if (!callNode) return { args: null, argCount: 0 };
|
|
2884
3061
|
|
|
3062
|
+
// Check if this is a method call (obj.func()) vs a direct call (func())
|
|
3063
|
+
const funcNode = callNode.childForFieldName('function') ||
|
|
3064
|
+
callNode.childForFieldName('name');
|
|
3065
|
+
let isMethodCall = false;
|
|
3066
|
+
if (funcNode) {
|
|
3067
|
+
// member_expression (JS), attribute (Python), selector_expression (Go), field_expression (Rust)
|
|
3068
|
+
if (['member_expression', 'attribute', 'selector_expression', 'field_expression'].includes(funcNode.type)) {
|
|
3069
|
+
isMethodCall = true;
|
|
3070
|
+
}
|
|
3071
|
+
// Java method_invocation with object
|
|
3072
|
+
if (callNode.type === 'method_invocation' && callNode.childForFieldName('object')) {
|
|
3073
|
+
isMethodCall = true;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2885
3077
|
const argsNode = callNode.childForFieldName('arguments');
|
|
2886
|
-
if (!argsNode) return { args: [], argCount: 0 };
|
|
3078
|
+
if (!argsNode) return { args: [], argCount: 0, isMethodCall };
|
|
2887
3079
|
|
|
2888
3080
|
const args = [];
|
|
2889
3081
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
@@ -2894,7 +3086,8 @@ class ProjectIndex {
|
|
|
2894
3086
|
args,
|
|
2895
3087
|
argCount: args.length,
|
|
2896
3088
|
hasSpread: args.some(a => a.startsWith('...')),
|
|
2897
|
-
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a))
|
|
3089
|
+
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
|
|
3090
|
+
isMethodCall
|
|
2898
3091
|
};
|
|
2899
3092
|
} catch (e) {
|
|
2900
3093
|
return { args: null, argCount: 0 };
|
|
@@ -3000,9 +3193,12 @@ class ProjectIndex {
|
|
|
3000
3193
|
};
|
|
3001
3194
|
}
|
|
3002
3195
|
|
|
3003
|
-
// Use
|
|
3004
|
-
const
|
|
3005
|
-
const
|
|
3196
|
+
// Use resolveSymbol for consistent primary selection (prefers non-test files)
|
|
3197
|
+
const { def: resolved } = this.resolveSymbol(name, { file: options.file });
|
|
3198
|
+
const primary = resolved || definitions[0];
|
|
3199
|
+
const others = definitions.filter(d =>
|
|
3200
|
+
d.relativePath !== primary.relativePath || d.startLine !== primary.startLine
|
|
3201
|
+
);
|
|
3006
3202
|
|
|
3007
3203
|
// Use the actual symbol name (may differ from query if fuzzy matched)
|
|
3008
3204
|
const symbolName = primary.name;
|
|
@@ -3268,42 +3464,89 @@ class ProjectIndex {
|
|
|
3268
3464
|
/**
|
|
3269
3465
|
* Get TOC for all files
|
|
3270
3466
|
*/
|
|
3271
|
-
getToc() {
|
|
3467
|
+
getToc(options = {}) {
|
|
3272
3468
|
const files = [];
|
|
3273
3469
|
let totalFunctions = 0;
|
|
3274
3470
|
let totalClasses = 0;
|
|
3275
3471
|
let totalState = 0;
|
|
3276
3472
|
let totalLines = 0;
|
|
3473
|
+
let totalDynamic = 0;
|
|
3474
|
+
let totalTests = 0;
|
|
3277
3475
|
|
|
3278
3476
|
for (const [filePath, fileEntry] of this.files) {
|
|
3279
|
-
|
|
3477
|
+
let functions = fileEntry.symbols.filter(s => s.type === 'function');
|
|
3280
3478
|
const classes = fileEntry.symbols.filter(s =>
|
|
3281
3479
|
['class', 'interface', 'type', 'enum', 'struct', 'trait', 'impl'].includes(s.type)
|
|
3282
3480
|
);
|
|
3283
3481
|
const state = fileEntry.symbols.filter(s => s.type === 'state');
|
|
3284
3482
|
|
|
3483
|
+
if (options.topLevel) {
|
|
3484
|
+
functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3285
3487
|
totalFunctions += functions.length;
|
|
3286
3488
|
totalClasses += classes.length;
|
|
3287
3489
|
totalState += state.length;
|
|
3288
3490
|
totalLines += fileEntry.lines;
|
|
3491
|
+
totalDynamic += fileEntry.dynamicImports || 0;
|
|
3492
|
+
if (isTestFile(filePath)) totalTests += 1;
|
|
3289
3493
|
|
|
3290
|
-
|
|
3494
|
+
const entry = {
|
|
3291
3495
|
file: fileEntry.relativePath,
|
|
3292
3496
|
language: fileEntry.language,
|
|
3293
3497
|
lines: fileEntry.lines,
|
|
3294
|
-
functions,
|
|
3295
|
-
classes,
|
|
3296
|
-
state
|
|
3297
|
-
}
|
|
3498
|
+
functions: functions.length,
|
|
3499
|
+
classes: classes.length,
|
|
3500
|
+
state: state.length
|
|
3501
|
+
};
|
|
3502
|
+
|
|
3503
|
+
if (options.detailed) {
|
|
3504
|
+
entry.symbols = { functions, classes, state };
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
files.push(entry);
|
|
3298
3508
|
}
|
|
3299
3509
|
|
|
3510
|
+
// Hints: top files by function count and lines
|
|
3511
|
+
const topFunctionFiles = [...files]
|
|
3512
|
+
.sort((a, b) => b.functions - a.functions || b.lines - a.lines)
|
|
3513
|
+
.filter(f => f.functions > 0)
|
|
3514
|
+
.slice(0, 3)
|
|
3515
|
+
.map(f => ({ file: f.file, functions: f.functions }));
|
|
3516
|
+
|
|
3517
|
+
const topLineFiles = [...files]
|
|
3518
|
+
.sort((a, b) => b.lines - a.lines)
|
|
3519
|
+
.slice(0, 3)
|
|
3520
|
+
.map(f => ({ file: f.file, lines: f.lines }));
|
|
3521
|
+
|
|
3522
|
+
// Entry point candidates
|
|
3523
|
+
const entryPattern = /(main|index|server|app)\.(js|jsx|ts|tsx|py|go|rs|java)$/i;
|
|
3524
|
+
const entryFiles = files
|
|
3525
|
+
.filter(f => entryPattern.test(f.file))
|
|
3526
|
+
.slice(0, 5)
|
|
3527
|
+
.map(f => f.file);
|
|
3528
|
+
|
|
3300
3529
|
return {
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3530
|
+
meta: {
|
|
3531
|
+
complete: totalDynamic === 0,
|
|
3532
|
+
skipped: 0,
|
|
3533
|
+
dynamicImports: totalDynamic,
|
|
3534
|
+
uncertain: 0
|
|
3535
|
+
},
|
|
3536
|
+
totals: {
|
|
3537
|
+
files: files.length,
|
|
3538
|
+
lines: totalLines,
|
|
3539
|
+
functions: totalFunctions,
|
|
3540
|
+
classes: totalClasses,
|
|
3541
|
+
state: totalState,
|
|
3542
|
+
testFiles: totalTests
|
|
3543
|
+
},
|
|
3544
|
+
summary: {
|
|
3545
|
+
topFunctionFiles,
|
|
3546
|
+
topLineFiles,
|
|
3547
|
+
entryFiles
|
|
3548
|
+
},
|
|
3549
|
+
files
|
|
3307
3550
|
};
|
|
3308
3551
|
}
|
|
3309
3552
|
|