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/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
- const resolved = resolveImport(importModule, filePath, {
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
- if (lowerPath.includes(pattern.toLowerCase())) {
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
- let definitions = this.symbols.get(name) || [];
697
- if (definitions.length === 0) {
698
- return { function: name, file: null, callers: [], callees: [] };
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 (definitions.length > 1) {
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 callers = this.findCallers(name, { includeMethods: options.includeMethods });
759
- const callees = this.findCallees(def, { includeMethods: options.includeMethods });
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
- // Add disambiguation warning if multiple definitions exist
773
- if (definitions.length > 1) {
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(); // name -> count
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
- callees.set(call.name, (callees.get(call.name) || 0) + 1);
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 [calleeName, count] of callees) {
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 multiple definitions, try to find the best match
1011
- if (symbols.length > 1) {
1012
- // Priority 1: Same file
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 (sameFile) {
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 definitions = this.symbols.get(name) || [];
1063
- if (definitions.length === 0) {
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 callees = this.findCallees(def, { includeMethods: options.includeMethods });
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 main function itself
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.name !== name) // Don't include self
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
- const targetRelative = path.relative(this.root, targetPath);
1418
- const targetBasename = path.basename(targetPath, path.extname(targetPath));
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
- const isPythonMagicMethod = lang === 'python' && /^__\w+__$/.test(name);
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
- const isEntryPoint = isGoEntryPoint || isJavaEntryPoint ||
1798
- isPythonMagicMethod || isRustEntryPoint;
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 definitions = this.symbols.get(name);
2031
- if (!definitions || definitions.length === 0) {
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
- let definitions = this.symbols.get(name);
2170
- if (!definitions || definitions.length === 0) {
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
- let definitions = this.symbols.get(name);
2255
- if (!definitions || definitions.length === 0) {
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 definitions = this.symbols.get(name);
2752
- if (!definitions || definitions.length === 0) {
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
- const def = definitions[0];
2757
- const expectedParamCount = def.paramsStructured?.length || 0;
2758
- const optionalCount = (def.paramsStructured || []).filter(p => p.optional || p.default !== undefined).length;
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 = (def.paramsStructured || []).some(p => p.rest);
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: def.paramsStructured?.map(p => ({
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: calls.length,
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 the definition with highest usage count (primary implementation)
3004
- const primary = definitions[0];
3005
- const others = definitions.slice(1);
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
- const functions = fileEntry.symbols.filter(s => s.type === 'function');
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
- files.push({
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
- totalFiles: files.length,
3302
- totalLines,
3303
- totalFunctions,
3304
- totalClasses,
3305
- totalState,
3306
- byFile: files
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