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/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
- const resolved = resolveImport(importModule, filePath, {
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
- 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
- }
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 (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
- }];
843
+ if (warnings.length > 0) {
844
+ result.warnings = warnings;
753
845
  }
754
846
 
755
847
  return result;
756
848
  }
757
849
 
758
- const callers = this.findCallers(name, { includeMethods: options.includeMethods });
759
- const callees = this.findCallees(def, { includeMethods: options.includeMethods });
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
- // 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
- }];
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(); // name -> count
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
- callees.set(call.name, (callees.get(call.name) || 0) + 1);
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 [calleeName, count] of callees) {
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 multiple definitions, try to find the best match
1011
- if (symbols.length > 1) {
1012
- // Priority 1: Same file
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 (sameFile) {
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 definitions = this.symbols.get(name) || [];
1063
- if (definitions.length === 0) {
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 callees = this.findCallees(def, { includeMethods: options.includeMethods });
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 main function itself
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.name !== name) // Don't include self
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 definitions = this.symbols.get(name);
2031
- if (!definitions || definitions.length === 0) {
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
- let definitions = this.symbols.get(name);
2170
- if (!definitions || definitions.length === 0) {
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
- let definitions = this.symbols.get(name);
2255
- if (!definitions || definitions.length === 0) {
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 definitions = this.symbols.get(name);
2752
- if (!definitions || definitions.length === 0) {
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: calls.length,
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 the definition with highest usage count (primary implementation)
3004
- const primary = definitions[0];
3005
- const others = definitions.slice(1);
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
- const functions = fileEntry.symbols.filter(s => s.type === 'function');
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
- files.push({
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
- totalFiles: files.length,
3302
- totalLines,
3303
- totalFunctions,
3304
- totalClasses,
3305
- totalState,
3306
- byFile: files
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