ucn 3.8.3 → 3.8.4

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.
@@ -0,0 +1 @@
1
+ {"sessionId":"5e2a28ce-7f65-4369-a922-8d1d902f9d8f","pid":18086,"acquiredAt":1773363188494}
package/cli/index.js CHANGED
@@ -169,7 +169,9 @@ if (unknownFlags.length > 0) {
169
169
  const VALUE_FLAGS = new Set([
170
170
  '--file', '--depth', '--top', '--context', '--direction',
171
171
  '--add-param', '--remove-param', '--rename-to', '--default',
172
- '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name'
172
+ '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name',
173
+ '--type', '--param', '--receiver', '--returns', '--decorator',
174
+ '--limit', '--max-files', '--min-confidence', '--stack'
173
175
  ]);
174
176
 
175
177
  // Remove flags from args, then add args after -- (which are all positional)
@@ -1246,7 +1248,7 @@ Flags can be added per-command: context myFunc --include-methods
1246
1248
  const tokens = input.split(/\s+/);
1247
1249
  const command = tokens[0];
1248
1250
  // Flags that take a space-separated value (--flag value)
1249
- const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack']);
1251
+ const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name']);
1250
1252
  const flagTokens = [];
1251
1253
  const argTokens = [];
1252
1254
  const skipNext = new Set();
@@ -1456,7 +1458,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1456
1458
  }
1457
1459
 
1458
1460
  case 'graph': {
1459
- const { ok, result, error } = execute(index, 'graph', { file: arg, ...iflags });
1461
+ const { ok, result, error } = execute(index, 'graph', { file: arg || iflags.file, direction: iflags.direction, depth: iflags.depth, all: iflags.all });
1460
1462
  if (!ok) { console.log(error); return; }
1461
1463
  const graphDepth = iflags.depth ? parseInt(iflags.depth) : 2;
1462
1464
  console.log(output.formatGraph(result, { showAll: iflags.all || !!iflags.depth, maxDepth: graphDepth, file: arg }));
package/core/callers.js CHANGED
@@ -456,6 +456,15 @@ function findCallers(index, name, options = {}) {
456
456
  const receiverLower = call.receiver.toLowerCase();
457
457
  const matchesTarget = [...targetTypes].some(cn => cn.toLowerCase() === receiverLower);
458
458
  if (!matchesTarget) {
459
+ // Rust/Go path calls (Type::method() / pkg.Method()): receiver IS the type name
460
+ // If it doesn't match target, it's definitely a different type — filter it
461
+ if (call.isPathCall && /^[A-Z]/.test(call.receiver)) {
462
+ isUncertain = true;
463
+ if (!options.includeUncertain) {
464
+ if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
465
+ continue;
466
+ }
467
+ }
459
468
  const nonTargetClasses = new Set();
460
469
  for (const d of definitions) {
461
470
  const t = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
@@ -915,7 +924,8 @@ function findCallees(index, def, options = {}) {
915
924
  callees.set(calleeKey, {
916
925
  name: effectiveName,
917
926
  bindingId: bindingResolved,
918
- count: 1
927
+ count: 1,
928
+ ...(call.isConstructor && { isConstructor: true })
919
929
  });
920
930
  }
921
931
  }
@@ -1023,7 +1033,7 @@ function findCallees(index, def, options = {}) {
1023
1033
  // Pre-compute import graph for callee confidence scoring
1024
1034
  const callerImportSet = new Set(index.importGraph.get(def.file) || []);
1025
1035
 
1026
- for (const { name: calleeName, bindingId, count } of callees.values()) {
1036
+ for (const { name: calleeName, bindingId, count, isConstructor } of callees.values()) {
1027
1037
  const symbols = index.symbols.get(calleeName);
1028
1038
  if (symbols && symbols.length > 0) {
1029
1039
  let callee = symbols[0];
@@ -1122,7 +1132,8 @@ function findCallees(index, def, options = {}) {
1122
1132
  if (!bindingId && NON_CALLABLE_TYPES.has(callee.type)) {
1123
1133
  const isFuncField = callee.type === 'field' && callee.fieldType &&
1124
1134
  /^func\b/.test(callee.fieldType);
1125
- if (!isFuncField) continue;
1135
+ // Constructor calls (new Foo()) are always callable regardless of type
1136
+ if (!isFuncField && !isConstructor) continue;
1126
1137
  }
1127
1138
 
1128
1139
  // Skip test-file callees when caller is production code and
package/core/deadcode.js CHANGED
@@ -328,9 +328,8 @@ function deadcode(index, options = {}) {
328
328
 
329
329
  // Rust: trait impl methods are invoked via trait dispatch, not direct calls
330
330
  // They can never be "dead" - the trait contract requires them to exist
331
- // className for trait impls contains " for " (e.g., "PartialEq for Glob")
332
331
  const isRustTraitImpl = lang === 'rust' && symbol.isMethod &&
333
- symbol.className && symbol.className.includes(' for ');
332
+ symbol.className && symbol.traitImpl;
334
333
 
335
334
  // Go: Test*, Benchmark*, Example* functions are called by go test
336
335
  const isGoTestFunc = lang === 'go' &&
package/core/execute.js CHANGED
@@ -393,36 +393,25 @@ const HANDLERS = {
393
393
  if (err) return { ok: false, error: err };
394
394
  applyClassMethodSyntax(p);
395
395
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
396
+ const fileErr = checkFilePatternMatch(index, p.file);
397
+ if (fileErr) return { ok: false, error: fileErr };
396
398
  const result = index.usages(p.name, {
397
399
  codeOnly: p.codeOnly || false,
398
400
  context: num(p.context, 0),
399
401
  className: p.className,
402
+ file: p.file,
400
403
  exclude,
401
404
  in: p.in,
402
405
  });
403
- // Apply limit to total usages across files
406
+ // Apply limit to total usages (result is a flat array)
404
407
  const limit = num(p.limit, undefined);
405
408
  let note;
406
- if (limit && limit > 0 && result.files) {
407
- let total = result.files.reduce((s, f) => s + f.usages.length, 0);
408
- if (total > limit) {
409
- let remaining = limit;
410
- const truncated = [];
411
- for (const f of result.files) {
412
- if (remaining <= 0) break;
413
- if (f.usages.length <= remaining) {
414
- truncated.push(f);
415
- remaining -= f.usages.length;
416
- } else {
417
- truncated.push({ ...f, usages: f.usages.slice(0, remaining) });
418
- remaining = 0;
419
- }
420
- }
421
- result.files = truncated;
422
- note = limitNote(limit, total);
423
- }
409
+ let limited = result;
410
+ if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
411
+ note = limitNote(limit, result.length);
412
+ limited = result.slice(0, limit);
424
413
  }
425
- return { ok: true, result, note };
414
+ return { ok: true, result: limited, note };
426
415
  },
427
416
 
428
417
  toc: (index, p) => {
@@ -432,34 +421,44 @@ const HANDLERS = {
432
421
  all: p.all,
433
422
  top: num(p.top, undefined),
434
423
  file: p.file,
424
+ exclude: p.exclude,
435
425
  });
436
- // Apply limit to detailed toc entries
426
+ // Apply limit to detailed toc entries (symbols are in f.symbols.functions/classes arrays)
437
427
  const limit = num(p.limit, undefined);
438
428
  let note;
439
429
  if (limit && limit > 0 && p.detailed && result.files) {
440
- let totalEntries = result.files.reduce((s, f) => s + (f.functions?.length || 0) + (f.classes?.length || 0), 0);
430
+ let totalEntries = result.files.reduce((s, f) => {
431
+ const syms = f.symbols || {};
432
+ return s + (syms.functions?.length || 0) + (syms.classes?.length || 0);
433
+ }, 0);
441
434
  if (totalEntries > limit) {
442
435
  let remaining = limit;
443
436
  for (const f of result.files) {
437
+ const syms = f.symbols || {};
444
438
  if (remaining <= 0) {
445
- f.functions = [];
446
- f.classes = [];
439
+ if (syms.functions) syms.functions = [];
440
+ if (syms.classes) syms.classes = [];
441
+ f.functions = 0;
442
+ f.classes = 0;
447
443
  continue;
448
444
  }
449
- const fns = f.functions?.length || 0;
450
- const cls = f.classes?.length || 0;
445
+ const fns = syms.functions?.length || 0;
446
+ const cls = syms.classes?.length || 0;
451
447
  if (fns + cls <= remaining) {
452
448
  remaining -= fns + cls;
453
449
  } else {
454
- if (f.functions && remaining > 0) {
455
- f.functions = f.functions.slice(0, remaining);
456
- remaining -= f.functions.length;
450
+ if (syms.functions && remaining > 0) {
451
+ syms.functions = syms.functions.slice(0, remaining);
452
+ remaining -= syms.functions.length;
453
+ f.functions = syms.functions.length;
457
454
  }
458
- if (f.classes && remaining > 0) {
459
- f.classes = f.classes.slice(0, remaining);
460
- remaining -= f.classes.length;
461
- } else if (f.classes) {
462
- f.classes = [];
455
+ if (syms.classes && remaining > 0) {
456
+ syms.classes = syms.classes.slice(0, remaining);
457
+ remaining -= syms.classes.length;
458
+ f.classes = syms.classes.length;
459
+ } else if (syms.classes) {
460
+ syms.classes = [];
461
+ f.classes = 0;
463
462
  }
464
463
  }
465
464
  }
@@ -549,7 +548,7 @@ const HANDLERS = {
549
548
  deadcode: (index, p) => {
550
549
  const fileErr = checkFilePatternMatch(index, p.file);
551
550
  if (fileErr) return { ok: false, error: fileErr };
552
- const result = index.deadcode({
551
+ let result = index.deadcode({
553
552
  includeExported: p.includeExported || false,
554
553
  includeDecorated: p.includeDecorated || false,
555
554
  includeTests: p.includeTests || false,
@@ -557,15 +556,16 @@ const HANDLERS = {
557
556
  in: p.in,
558
557
  file: p.file,
559
558
  });
560
- // Apply limit to dead code results
559
+ // Apply limit to dead code results (result is an array with custom properties)
561
560
  const limit = num(p.limit, undefined);
562
561
  let note;
563
- if (limit && limit > 0 && result.dead) {
564
- const { items, total, limited } = applyLimit(result.dead, limit);
565
- if (limited) {
566
- note = limitNote(limit, total);
567
- result.dead = items;
568
- }
562
+ if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
563
+ note = limitNote(limit, result.length);
564
+ const sliced = result.slice(0, limit);
565
+ // Preserve custom properties (excludedExported, excludedDecorated) from deadcode()
566
+ if (result.excludedExported != null) sliced.excludedExported = result.excludedExported;
567
+ if (result.excludedDecorated != null) sliced.excludedDecorated = result.excludedDecorated;
568
+ result = sliced;
569
569
  }
570
570
  return { ok: true, result, note };
571
571
  },
@@ -866,7 +866,7 @@ const HANDLERS = {
866
866
  const err = requireName(p.name);
867
867
  if (err) return { ok: false, error: err };
868
868
  applyClassMethodSyntax(p);
869
- const result = index.typedef(p.name, { exact: p.exact || false, className: p.className });
869
+ const result = index.typedef(p.name, { exact: p.exact || false, className: p.className, file: p.file });
870
870
  return { ok: true, result };
871
871
  },
872
872
 
package/core/output.js CHANGED
@@ -392,7 +392,7 @@ function formatSearchJson(results, term) {
392
392
  const meta = results.meta;
393
393
  const obj = {
394
394
  term,
395
- totalMatches: results.reduce((sum, r) => sum + r.matches.length, 0),
395
+ totalMatches: (meta && meta.totalMatches != null) ? meta.totalMatches : results.reduce((sum, r) => sum + r.matches.length, 0),
396
396
  files: results.map(r => ({
397
397
  file: r.file,
398
398
  matchCount: r.matches.length,
@@ -407,6 +407,7 @@ function formatSearchJson(results, term) {
407
407
  obj.filesSkipped = meta.filesSkipped;
408
408
  obj.totalFiles = meta.totalFiles;
409
409
  if (meta.regexFallback) obj.regexFallback = meta.regexFallback;
410
+ if (meta.truncatedMatches > 0) obj.truncatedMatches = meta.truncatedMatches;
410
411
  }
411
412
  return JSON.stringify(obj, null, 2);
412
413
  }
@@ -839,7 +840,7 @@ function formatTrace(trace, options = {}) {
839
840
  }
840
841
 
841
842
  if (trace.includeMethods === false) {
842
- const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded (--include-methods=false). Remove flag to include them (default).';
843
+ const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded — use --include-methods to include them';
843
844
  lines.push(`\n${methodsHint}`);
844
845
  }
845
846
 
@@ -1634,7 +1635,7 @@ function formatAbout(about, options = {}) {
1634
1635
  }
1635
1636
 
1636
1637
  if (about.includeMethods === false) {
1637
- const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded (--include-methods=false). Remove flag to include them (default).';
1638
+ const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded — use --include-methods to include them';
1638
1639
  lines.push(`\n${methodsHint}`);
1639
1640
  }
1640
1641
 
@@ -2322,7 +2323,7 @@ function formatCircularDeps(result) {
2322
2323
  lines.push(`Filtered to cycles involving: ${result.fileFilter}`);
2323
2324
  }
2324
2325
 
2325
- const scannedCount = result.filesWithImports || result.totalFiles;
2326
+ const scannedCount = result.filesWithImports != null ? result.filesWithImports : result.totalFiles;
2326
2327
 
2327
2328
  if (result.cycles.length === 0) {
2328
2329
  lines.push('');
@@ -2413,7 +2414,7 @@ function formatSearch(results, term) {
2413
2414
  }
2414
2415
 
2415
2416
  if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
2416
- lines.push(`\nNote: ${meta.filesSkipped} file${meta.filesSkipped === 1 ? '' : 's'} excluded by filters (test files hidden by default; use include_tests=true to include).`);
2417
+ lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (use include_tests=true to include).`);
2417
2418
  }
2418
2419
 
2419
2420
  return lines.join('\n');
package/core/project.js CHANGED
@@ -365,7 +365,9 @@ class ProjectIndex {
365
365
  ...(item.memberType && { memberType: item.memberType }),
366
366
  ...(item.fieldType && { fieldType: item.fieldType }),
367
367
  ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
368
- ...(item.nameLine && { nameLine: item.nameLine })
368
+ ...(item.nameLine && { nameLine: item.nameLine }),
369
+ ...(item.traitImpl && { traitImpl: true }),
370
+ ...(item.isSignature && { isSignature: true })
369
371
  };
370
372
  fileEntry.symbols.push(symbol);
371
373
  fileEntry.bindings.push({
@@ -396,7 +398,7 @@ class ProjectIndex {
396
398
  if (cls.members) {
397
399
  for (const m of cls.members) {
398
400
  const memberType = m.memberType || 'method';
399
- addSymbol({ ...m, className: cls.name }, memberType);
401
+ addSymbol({ ...m, className: cls.name, ...(cls.traitName && { traitImpl: true }) }, memberType);
400
402
  }
401
403
  }
402
404
  }
@@ -780,6 +782,8 @@ class ProjectIndex {
780
782
  // Check inclusion (directory or file path)
781
783
  if (filters.in) {
782
784
  const inPattern = filters.in.replace(/\/$/, ''); // strip trailing slash
785
+ // '.' means current directory = entire project, always matches
786
+ if (inPattern === '.') return true;
783
787
  // Detect if pattern looks like a file path (has an extension)
784
788
  const looksLikeFile = /\.\w+$/.test(inPattern);
785
789
  if (looksLikeFile) {
@@ -908,6 +912,13 @@ class ProjectIndex {
908
912
  if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) {
909
913
  score += 200;
910
914
  }
915
+ // Deprioritize type-only overload signatures (TypeScript function_signature)
916
+ if (d.isSignature) score -= 200;
917
+ // Prefer larger function bodies (implementation over overload signature)
918
+ // Only for functions/methods — not for class-level types (struct vs impl)
919
+ if (d.startLine && d.endLine && d.type === 'function') {
920
+ score += Math.min(d.endLine - d.startLine, 100);
921
+ }
911
922
  return { def: d, score };
912
923
  });
913
924
 
@@ -1257,11 +1268,17 @@ class ProjectIndex {
1257
1268
  try {
1258
1269
  const usages = [];
1259
1270
 
1271
+ // Resolve file pattern for --file filter
1272
+ const fileFilter = options.file ? this.resolveFilePathForQuery(options.file) : null;
1273
+
1260
1274
  // Get definitions (filtered)
1261
1275
  let allDefinitions = this.symbols.get(name) || [];
1262
1276
  if (options.className) {
1263
1277
  allDefinitions = allDefinitions.filter(d => d.className === options.className);
1264
1278
  }
1279
+ if (fileFilter) {
1280
+ allDefinitions = allDefinitions.filter(d => d.file === fileFilter);
1281
+ }
1265
1282
  const definitions = options.exclude || options.in
1266
1283
  ? allDefinitions.filter(d => this.matchesFilters(d.relativePath, options))
1267
1284
  : allDefinitions;
@@ -1278,6 +1295,10 @@ class ProjectIndex {
1278
1295
 
1279
1296
  // Scan all files for usages
1280
1297
  for (const [filePath, fileEntry] of this.files) {
1298
+ // Apply --file filter
1299
+ if (fileFilter && filePath !== fileFilter) {
1300
+ continue;
1301
+ }
1281
1302
  // Apply filters
1282
1303
  if (!this.matchesFilters(fileEntry.relativePath, options)) {
1283
1304
  continue;
@@ -2247,7 +2268,7 @@ class ProjectIndex {
2247
2268
  * @returns {Array} Matching type definitions
2248
2269
  */
2249
2270
  typedef(name, options = {}) {
2250
- const typeKinds = ['type', 'interface', 'enum', 'struct', 'trait', 'class'];
2271
+ const typeKinds = ['type', 'interface', 'enum', 'struct', 'trait', 'class', 'record'];
2251
2272
  const matches = this.find(name, options);
2252
2273
 
2253
2274
  return matches.filter(m => typeKinds.includes(m.type)).map(m => ({
@@ -2277,6 +2298,15 @@ class ProjectIndex {
2277
2298
  for (const [filePath, fileEntry] of this.files) {
2278
2299
  if (isTestFile(fileEntry.relativePath, fileEntry.language)) {
2279
2300
  testFiles.push({ path: filePath, entry: fileEntry });
2301
+ } else if (fileEntry.language === 'rust') {
2302
+ // Rust idiomatically puts tests in #[cfg(test)] modules inside source files.
2303
+ // Check if file has any symbols with 'test' modifier (#[test] attribute).
2304
+ const hasInlineTests = fileEntry.symbols?.some(s =>
2305
+ s.modifiers?.includes('test')
2306
+ );
2307
+ if (hasInlineTests) {
2308
+ testFiles.push({ path: filePath, entry: fileEntry });
2309
+ }
2280
2310
  }
2281
2311
  }
2282
2312
 
@@ -2592,6 +2622,75 @@ class ProjectIndex {
2592
2622
  }
2593
2623
  }
2594
2624
 
2625
+ // Python __all__ re-exports: names listed in __all__ that come from imports
2626
+ // e.g. __init__.py: `from .utils import helper` + `__all__ = ["helper"]`
2627
+ // `helper` is in fileEntry.exports but not in fileEntry.symbols
2628
+ if (fileEntry.language === 'python' && fileEntry.exports.length > 0) {
2629
+ const matchedNames = new Set(results.map(r => r.name));
2630
+ const unmatched = fileEntry.exports.filter(name => !matchedNames.has(name));
2631
+ if (unmatched.length > 0) {
2632
+ // Re-extract raw imports to get name→module mapping (not stored in fileEntry)
2633
+ const { extractImports, resolveImport } = require('./imports');
2634
+ try {
2635
+ const content = this._readFile(absPath);
2636
+ const { imports: rawImports } = extractImports(content, 'python');
2637
+ // Build name→module map from raw imports
2638
+ const nameToModule = new Map();
2639
+ for (const imp of rawImports) {
2640
+ if (imp.names) {
2641
+ for (const name of imp.names) {
2642
+ if (name !== '*') nameToModule.set(name, imp.module);
2643
+ }
2644
+ }
2645
+ }
2646
+ for (const name of unmatched) {
2647
+ const sourceModule = nameToModule.get(name);
2648
+ if (!sourceModule) continue;
2649
+ const resolvedSrc = resolveImport(sourceModule, absPath, {
2650
+ language: 'python',
2651
+ root: this.root,
2652
+ extensions: this.extensions
2653
+ });
2654
+ if (!resolvedSrc) continue;
2655
+ const sourceEntry = this.files.get(resolvedSrc);
2656
+ const srcSymbol = sourceEntry && sourceEntry.symbols.find(s => s.name === name);
2657
+ if (srcSymbol) {
2658
+ matchedNames.add(name);
2659
+ results.push({
2660
+ name,
2661
+ type: srcSymbol.type,
2662
+ file: fileEntry.relativePath,
2663
+ startLine: srcSymbol.startLine,
2664
+ endLine: srcSymbol.endLine,
2665
+ params: srcSymbol.params,
2666
+ returnType: srcSymbol.returnType,
2667
+ signature: this.formatSignature(srcSymbol),
2668
+ reExportedFrom: sourceEntry.relativePath
2669
+ });
2670
+ } else {
2671
+ // Source not indexed or symbol not found — still list it
2672
+ matchedNames.add(name);
2673
+ results.push({
2674
+ name,
2675
+ type: 're-export',
2676
+ file: fileEntry.relativePath,
2677
+ startLine: undefined,
2678
+ endLine: undefined,
2679
+ params: undefined,
2680
+ returnType: null,
2681
+ signature: `re-export ${name} from '${sourceModule}'`,
2682
+ reExportedFrom: resolvedSrc
2683
+ ? (sourceEntry ? sourceEntry.relativePath : resolvedSrc)
2684
+ : sourceModule
2685
+ });
2686
+ }
2687
+ }
2688
+ } catch (_) {
2689
+ // File read failure — skip Python re-export resolution
2690
+ }
2691
+ }
2692
+ }
2693
+
2595
2694
  return results;
2596
2695
  }
2597
2696
 
@@ -3181,8 +3280,8 @@ class ProjectIndex {
3181
3280
  const content = this._readFile(c.callerFile);
3182
3281
  const lines = content.split('\n');
3183
3282
  const line = lines[call.line - 1] || '';
3184
- // Match "var = ClassName(...)"
3185
- const m = line.match(/^\s*(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/);
3283
+ // Match "var = ClassName(...)" or "var = new ClassName(...)" or "Type var = new ClassName<>(...)"
3284
+ const m = line.match(/(\w+)\s*=\s*(?:await\s+)?(?:new\s+)?(\w+)\s*(?:<[^>]*>)?\s*\(/);
3186
3285
  if (m && m[2] === call.name) {
3187
3286
  localTypes.set(m[1], call.name);
3188
3287
  }
@@ -3198,6 +3297,31 @@ class ProjectIndex {
3198
3297
  }
3199
3298
  }
3200
3299
  }
3300
+ // Check class field declarations for receiver type: private DataService service
3301
+ if (c.callerFile) {
3302
+ const callerEnclosing = this.findEnclosingFunction(c.callerFile, c.line, true);
3303
+ if (callerEnclosing?.className) {
3304
+ const classSyms = this.symbols.get(callerEnclosing.className);
3305
+ if (classSyms) {
3306
+ const classDef = classSyms.find(s => s.type === 'class' || s.type === 'struct' || s.type === 'interface');
3307
+ if (classDef) {
3308
+ const content = this._readFile(c.callerFile);
3309
+ const lines = content.split('\n');
3310
+ // Scan class body for field declarations matching the receiver
3311
+ for (let li = classDef.startLine - 1; li < (classDef.endLine || classDef.startLine + 50) && li < lines.length; li++) {
3312
+ const line = lines[li];
3313
+ // Match Java/TS field: [modifiers] TypeName<...> receiverName [= ...]
3314
+ const fieldMatch = line.match(new RegExp(`\\b(\\w+)(?:<[^>]*>)?\\s+${r.replace(/[.*+?^${}()|[\]\\]/g, '\\\\$&')}\\s*[;=]`));
3315
+ if (fieldMatch) {
3316
+ const fieldType = fieldMatch[1];
3317
+ if (fieldType === targetClassName) return true;
3318
+ break;
3319
+ }
3320
+ }
3321
+ }
3322
+ }
3323
+ }
3324
+ }
3201
3325
  // Check parameter type annotations: def foo(tracker: SourceTracker) → tracker.record()
3202
3326
  if (c.callerFile && c.callerStartLine) {
3203
3327
  const callerSymbol = this.findEnclosingFunction(c.callerFile, c.line, true);
@@ -3357,7 +3481,7 @@ class ProjectIndex {
3357
3481
  const classNames = [...new Set(allDefs
3358
3482
  .filter(d => d.className && d.className !== def.className)
3359
3483
  .map(d => d.className))];
3360
- if (classNames.length > 0) {
3484
+ if (classNames.length > 0 && !options.className && !options.file) {
3361
3485
  scopeWarning = {
3362
3486
  targetClass: def.className || '(unknown)',
3363
3487
  otherClasses: classNames,
@@ -3678,6 +3802,19 @@ class ProjectIndex {
3678
3802
  node.entryPoint = true;
3679
3803
  entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
3680
3804
  }
3805
+ } else if (currentDepth > 0) {
3806
+ // At depth limit: check if this node is an entry point
3807
+ const callers = this.findCallers(funcDef.name, {
3808
+ includeMethods,
3809
+ includeUncertain,
3810
+ targetDefinitions: funcDef.bindingId ? [funcDef] : undefined,
3811
+ });
3812
+ const hasCallers = callers.some(c => c.callerName &&
3813
+ (exclude.length === 0 || this.matchesFilters(c.relativePath, { exclude })));
3814
+ if (!hasCallers) {
3815
+ node.entryPoint = true;
3816
+ entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
3817
+ }
3681
3818
  }
3682
3819
 
3683
3820
  return node;
@@ -3764,7 +3901,12 @@ class ProjectIndex {
3764
3901
  const excludeArr = exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : [];
3765
3902
  const results = [];
3766
3903
  for (const [filePath, fileEntry] of this.files) {
3767
- if (!isTestFile(fileEntry.relativePath, fileEntry.language)) continue;
3904
+ let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
3905
+ // Rust inline #[cfg(test)] modules: source files with #[test]-marked symbols
3906
+ if (!isTest && fileEntry.language === 'rust') {
3907
+ isTest = fileEntry.symbols?.some(s => s.modifiers?.includes('test'));
3908
+ }
3909
+ if (!isTest) continue;
3768
3910
  if (excludeArr.length > 0 && !this.matchesFilters(fileEntry.relativePath, { exclude: excludeArr })) continue;
3769
3911
  try {
3770
3912
  const content = this._readFile(filePath);
@@ -4082,6 +4224,7 @@ class ProjectIndex {
4082
4224
  const results = [];
4083
4225
  let filesScanned = 0;
4084
4226
  let filesSkipped = 0;
4227
+ let filesFilteredByFlag = 0;
4085
4228
  const regexFlags = options.caseSensitive ? 'g' : 'gi';
4086
4229
  const useRegex = options.regex !== false; // Default: regex ON
4087
4230
  let regex;
@@ -4103,7 +4246,7 @@ class ProjectIndex {
4103
4246
  if (options.file) {
4104
4247
  const fp = fileEntry.relativePath;
4105
4248
  if (!fp.includes(options.file) && !fp.endsWith(options.file)) {
4106
- filesSkipped++;
4249
+ filesFilteredByFlag++;
4107
4250
  continue;
4108
4251
  }
4109
4252
  }
@@ -4228,7 +4371,7 @@ class ProjectIndex {
4228
4371
  results.push(...truncated);
4229
4372
  }
4230
4373
 
4231
- results.meta = { filesScanned, filesSkipped, totalFiles: this.files.size, regexFallback, totalMatches, truncatedMatches };
4374
+ results.meta = { filesScanned, filesSkipped, filesFilteredByFlag, totalFiles: this.files.size, regexFallback, totalMatches, truncatedMatches };
4232
4375
  return results;
4233
4376
  } finally { this._endOp(); }
4234
4377
  }
@@ -4330,7 +4473,7 @@ class ProjectIndex {
4330
4473
  }
4331
4474
  } else {
4332
4475
  // Search symbols (functions, classes, methods, types)
4333
- const functionTypes = new Set(['function', 'constructor', 'method', 'arrow', 'static', 'classmethod']);
4476
+ const functionTypes = new Set(['function', 'constructor', 'method', 'arrow', 'static', 'classmethod', 'abstract']);
4334
4477
  const classTypes = new Set(['class', 'struct', 'interface', 'impl', 'trait']);
4335
4478
  const typeTypes = new Set(['type', 'enum', 'interface', 'trait']);
4336
4479
  const methodTypes = new Set(['method', 'constructor']);
@@ -4361,6 +4504,11 @@ class ProjectIndex {
4361
4504
  if (!hasMatch) continue;
4362
4505
  }
4363
4506
 
4507
+ // Receiver filter: match className for methods
4508
+ if (receiver) {
4509
+ if (!def.className || !matchesSubstring(def.className, receiver, options.caseSensitive)) continue;
4510
+ }
4511
+
4364
4512
  // Return type filter
4365
4513
  if (returnType) {
4366
4514
  if (!def.returnType || !matchesSubstring(def.returnType, returnType, options.caseSensitive)) continue;
@@ -4546,6 +4694,9 @@ class ProjectIndex {
4546
4694
 
4547
4695
  for (const [filePath, fileEntry] of this.files) {
4548
4696
  if (fileFilter && !fileFilter.has(filePath)) continue;
4697
+ if (options.exclude && options.exclude.length > 0) {
4698
+ if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude })) continue;
4699
+ }
4549
4700
  let functions = fileEntry.symbols.filter(s =>
4550
4701
  s.type === 'function' || s.type === 'method' || s.type === 'static' ||
4551
4702
  s.type === 'constructor' || s.type === 'public' || s.type === 'abstract' ||
package/core/shared.js CHANGED
@@ -19,6 +19,8 @@ function pickBestDefinition(matches) {
19
19
  if (isTestFile(rp, detectLanguage(m.file))) score -= 500;
20
20
  if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
21
21
  if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
22
+ // Deprioritize type-only overload signatures (TypeScript function_signature)
23
+ if (m.isSignature) score -= 200;
22
24
  // Tiebreaker: prefer larger function bodies (more important/complex)
23
25
  if (m.startLine && m.endLine) {
24
26
  score += Math.min(m.endLine - m.startLine, 100);
package/core/verify.js CHANGED
@@ -29,11 +29,17 @@ function findCallNode(node, callTypes, targetRow, funcName) {
29
29
  }
30
30
  } else {
31
31
  // Check if this call is for our target function
32
- const funcNode = node.childForFieldName('function') ||
32
+ let funcNode = node.childForFieldName('function') ||
33
33
  node.childForFieldName('name'); // Java method_invocation uses 'name'
34
+ // Unwrap turbofish/generic_function: process::<T>() wraps the function in generic_function
35
+ if (funcNode && funcNode.type === 'generic_function') {
36
+ funcNode = funcNode.childForFieldName('function') || funcNode.namedChild(0);
37
+ }
34
38
  if (funcNode) {
35
39
  const funcText = funcNode.type === 'member_expression' || funcNode.type === 'selector_expression' || funcNode.type === 'field_expression' || funcNode.type === 'attribute'
36
40
  ? (funcNode.childForFieldName('property') || funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
41
+ : funcNode.type === 'scoped_identifier'
42
+ ? (funcNode.childForFieldName('name') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
37
43
  : funcNode.text;
38
44
  if (funcText === funcName) return node;
39
45
  }
@@ -190,7 +196,7 @@ function verify(index, name, options = {}) {
190
196
  let params = def.paramsStructured || [];
191
197
  if ((lang === 'python' || lang === 'rust') && params.length > 0) {
192
198
  const firstName = params[0].name;
193
- if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
199
+ if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self' || firstName === 'mut self') {
194
200
  params = params.slice(1);
195
201
  }
196
202
  }
@@ -420,7 +426,7 @@ function verify(index, name, options = {}) {
420
426
  const classNames = [...new Set(allDefs
421
427
  .filter(d => d.className && d.className !== def.className)
422
428
  .map(d => d.className))];
423
- if (classNames.length > 0) {
429
+ if (classNames.length > 0 && !options.className && !options.file) {
424
430
  scopeWarning = {
425
431
  targetClass: def.className || '(unknown)',
426
432
  otherClasses: classNames,
@@ -494,31 +500,39 @@ function plan(index, name, options = {}) {
494
500
  ...(options.defaultValue && { default: options.defaultValue })
495
501
  };
496
502
 
497
- // When adding a required param (no default), insert before the first
498
- // optional param to avoid producing invalid signatures in Python/TS
499
- // (required params must precede optional ones).
500
- if (!options.defaultValue) {
501
- const firstOptIdx = newParams.findIndex(p => p.optional || p.default !== undefined);
502
- if (firstOptIdx !== -1) {
503
- // Also skip self/cls/&self/&mut self at position 0
504
- const insertIdx = Math.max(firstOptIdx,
505
- (newParams.length > 0 && ['self', 'cls', '&self', '&mut self'].includes(newParams[0].name)) ? 1 : 0);
503
+ // When adding a param, insert before rest params (*args/**kwargs) and
504
+ // before optional params (required must precede optional in Python/TS).
505
+ {
506
+ const selfNames = ['self', 'cls', '&self', '&mut self', 'mut self'];
507
+ const minIdx = (newParams.length > 0 && selfNames.includes(newParams[0].name)) ? 1 : 0;
508
+ const firstRestIdx = newParams.findIndex(p => p.rest || (p.name && (p.name.startsWith('*') || p.name.startsWith('...'))));
509
+ if (firstRestIdx !== -1) {
510
+ // Always insert before rest params (*args, **kwargs, ...rest)
511
+ const insertIdx = Math.max(firstRestIdx, minIdx);
506
512
  newParams.splice(insertIdx, 0, newParam);
513
+ } else if (!options.defaultValue) {
514
+ const firstOptIdx = newParams.findIndex(p => p.optional || p.default !== undefined);
515
+ if (firstOptIdx !== -1) {
516
+ const insertIdx = Math.max(firstOptIdx, minIdx);
517
+ newParams.splice(insertIdx, 0, newParam);
518
+ } else {
519
+ newParams.push(newParam);
520
+ }
507
521
  } else {
508
522
  newParams.push(newParam);
509
523
  }
510
- } else {
511
- newParams.push(newParam);
512
524
  }
513
525
 
514
526
  // Generate new signature
515
527
  const paramsList = newParams.map(p => {
516
- let str = p.name;
528
+ let str = (p.rest && !p.name.startsWith('*')) ? `...${p.name}` : p.name;
529
+ if (p.optional && !p.default) str += '?';
517
530
  if (p.type) str += `: ${p.type}`;
518
531
  if (p.default) str += ` = ${p.default}`;
519
532
  return str;
520
533
  }).join(', ');
521
- newSignature = `${name}(${paramsList})`;
534
+ const asyncPrefix = (def.async || def.isAsync || def.modifiers?.includes('async')) ? 'async ' : '';
535
+ newSignature = `${asyncPrefix}${name}(${paramsList})`;
522
536
  if (def.returnType) newSignature += `: ${def.returnType}`;
523
537
 
524
538
  // Describe changes needed at each call site
@@ -540,7 +554,13 @@ function plan(index, name, options = {}) {
540
554
 
541
555
  if (options.removeParam) {
542
556
  operation = 'remove-param';
543
- const paramIndex = currentParams.findIndex(p => p.name === options.removeParam);
557
+ // Normalize self-parameter lookup: 'self' matches '&self', '&mut self', 'mut self'
558
+ let removeTarget = options.removeParam;
559
+ let paramIndex = currentParams.findIndex(p => p.name === removeTarget);
560
+ if (paramIndex === -1 && removeTarget === 'self') {
561
+ paramIndex = currentParams.findIndex(p => /^&?(?:mut )?self$/.test(p.name));
562
+ if (paramIndex !== -1) removeTarget = currentParams[paramIndex].name;
563
+ }
544
564
  if (paramIndex === -1) {
545
565
  return {
546
566
  found: true,
@@ -549,16 +569,18 @@ function plan(index, name, options = {}) {
549
569
  };
550
570
  }
551
571
 
552
- newParams = currentParams.filter(p => p.name !== options.removeParam);
572
+ newParams = currentParams.filter(p => p.name !== removeTarget);
553
573
 
554
574
  // Generate new signature
555
575
  const paramsList = newParams.map(p => {
556
- let str = p.name;
576
+ let str = (p.rest && !p.name.startsWith('*')) ? `...${p.name}` : p.name;
577
+ if (p.optional && !p.default) str += '?';
557
578
  if (p.type) str += `: ${p.type}`;
558
579
  if (p.default) str += ` = ${p.default}`;
559
580
  return str;
560
581
  }).join(', ');
561
- newSignature = `${name}(${paramsList})`;
582
+ const asyncPrefix = (def.async || def.isAsync || def.modifiers?.includes('async')) ? 'async ' : '';
583
+ newSignature = `${asyncPrefix}${name}(${paramsList})`;
562
584
  if (def.returnType) newSignature += `: ${def.returnType}`;
563
585
 
564
586
  // For Python/Rust methods, self/cls/&self/&mut self is in paramsStructured
@@ -568,7 +590,7 @@ function plan(index, name, options = {}) {
568
590
  let selfOffset = 0;
569
591
  if ((lang === 'python' || lang === 'rust') && currentParams.length > 0) {
570
592
  const firstName = currentParams[0].name;
571
- if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
593
+ if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self' || firstName === 'mut self') {
572
594
  selfOffset = 1;
573
595
  }
574
596
  }
@@ -643,11 +665,21 @@ function plan(index, name, options = {}) {
643
665
  operation,
644
666
  before: {
645
667
  signature: currentSignature,
646
- params: currentParams.map(p => p.name)
668
+ params: currentParams.map(p => {
669
+ let n = p.name;
670
+ if (p.optional && !p.default) n += '?';
671
+ if (p.type) n += `: ${p.type}`;
672
+ return n;
673
+ })
647
674
  },
648
675
  after: {
649
676
  signature: newSignature,
650
- params: newParams.map(p => p.name)
677
+ params: newParams.map(p => {
678
+ let n = p.name;
679
+ if (p.optional && !p.default) n += '?';
680
+ if (p.type) n += `: ${p.type}`;
681
+ return n;
682
+ })
651
683
  },
652
684
  totalChanges: changes.length,
653
685
  filesAffected: new Set(changes.map(c => c.file)).size,
package/languages/go.js CHANGED
@@ -290,6 +290,7 @@ function extractInterfaceMembers(interfaceNode, code) {
290
290
  // Name is in a field_identifier child
291
291
  let nameText = null;
292
292
  let paramsText = null;
293
+ let paramsNode = null;
293
294
  let returnType = null;
294
295
  let hasParams = false;
295
296
  for (let j = 0; j < child.namedChildCount; j++) {
@@ -300,6 +301,7 @@ function extractInterfaceMembers(interfaceNode, code) {
300
301
  hasParams = true;
301
302
  if (!paramsText) {
302
303
  paramsText = sub.text.slice(1, -1); // strip parens
304
+ paramsNode = sub;
303
305
  } else {
304
306
  // Second parameter_list is the return type tuple
305
307
  returnType = sub.text;
@@ -312,10 +314,15 @@ function extractInterfaceMembers(interfaceNode, code) {
312
314
  if (nameNode) nameText = nameNode.text;
313
315
  }
314
316
  if (!returnType) {
315
- // Single return type (not a tuple) is a type_identifier
317
+ // Single return type can be type_identifier, pointer_type, slice_type, map_type, etc.
318
+ const returnTypeNodes = new Set([
319
+ 'type_identifier', 'pointer_type', 'slice_type', 'map_type',
320
+ 'array_type', 'channel_type', 'function_type', 'interface_type',
321
+ 'struct_type', 'generic_type', 'qualified_type',
322
+ ]);
316
323
  for (let j = 0; j < child.namedChildCount; j++) {
317
324
  const sub = child.namedChild(j);
318
- if (sub.type === 'type_identifier' && sub.text !== nameText) {
325
+ if (returnTypeNodes.has(sub.type) && sub.text !== nameText) {
319
326
  returnType = sub.text;
320
327
  }
321
328
  }
@@ -341,6 +348,7 @@ function extractInterfaceMembers(interfaceNode, code) {
341
348
  endLine,
342
349
  memberType: 'method',
343
350
  ...(paramsText !== null && { params: paramsText }),
351
+ ...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'go') }),
344
352
  ...(returnType && { returnType })
345
353
  });
346
354
  }
package/languages/java.js CHANGED
@@ -51,7 +51,11 @@ function extractModifiers(node) {
51
51
  if (mod.type === 'marker_annotation' || mod.type === 'annotation') {
52
52
  // Store annotation name (without @) as modifier (e.g., @Test -> 'test', @Override -> 'override')
53
53
  const annoText = mod.text.replace(/^@/, '').split('(')[0].toLowerCase();
54
- modifiers.push(annoText);
54
+ // Skip noise annotations that don't carry semantic meaning
55
+ const SKIP_ANNOTATIONS = new Set(['suppresswarnings', 'safevarargs', 'serial', 'generated']);
56
+ if (!SKIP_ANNOTATIONS.has(annoText)) {
57
+ modifiers.push(annoText);
58
+ }
55
59
  continue;
56
60
  }
57
61
  modifiers.push(mod.text);
@@ -59,10 +63,11 @@ function extractModifiers(node) {
59
63
  }
60
64
 
61
65
  // Also check text before parameter list for modifiers (avoid matching keywords in params)
66
+ // Use the parameters node position to find the real paren (not annotation parens)
62
67
  const text = node.text;
63
- const firstLine = text.split('\n')[0];
64
- const parenIdx = firstLine.indexOf('(');
65
- const preParams = parenIdx >= 0 ? firstLine.substring(0, parenIdx) : firstLine;
68
+ const paramsNode = node.childForFieldName('parameters');
69
+ const paramOffset = paramsNode ? paramsNode.startIndex - node.startIndex : -1;
70
+ const preParams = paramOffset >= 0 ? text.substring(0, paramOffset) : text.split('\n').slice(0, 3).join(' ');
66
71
  const keywords = ['public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native', 'default'];
67
72
  for (const kw of keywords) {
68
73
  if (preParams.includes(kw + ' ') && !modifiers.includes(kw)) {
@@ -130,10 +135,10 @@ function findFunctions(code, parser) {
130
135
  if (processedRanges.has(rangeKey)) return true;
131
136
  processedRanges.add(rangeKey);
132
137
 
133
- // Skip methods inside a class body (they're extracted as class members)
138
+ // Skip methods inside a class/interface/enum body (they're extracted as class members)
134
139
  let parent = node.parent;
135
- if (parent && parent.type === 'class_body') {
136
- return true; // Skip - this is a class method
140
+ if (parent && (parent.type === 'class_body' || parent.type === 'interface_body' || parent.type === 'enum_body' || parent.type === 'enum_body_declarations')) {
141
+ return true; // Skip - this is a class/interface/enum method
137
142
  }
138
143
 
139
144
  const nameNode = node.childForFieldName('name');
@@ -172,10 +177,10 @@ function findFunctions(code, parser) {
172
177
  if (processedRanges.has(rangeKey)) return true;
173
178
  processedRanges.add(rangeKey);
174
179
 
175
- // Skip constructors inside a class body (they're extracted as class members)
180
+ // Skip constructors inside a class/enum body (they're extracted as class members)
176
181
  let parent = node.parent;
177
- if (parent && parent.type === 'class_body') {
178
- return true; // Skip - this is a class constructor
182
+ if (parent && (parent.type === 'class_body' || parent.type === 'enum_body' || parent.type === 'enum_body_declarations')) {
183
+ return true; // Skip - this is a class/enum constructor
179
184
  }
180
185
 
181
186
  const nameNode = node.childForFieldName('name');
@@ -524,6 +529,7 @@ function extractClassMembers(classNode, code) {
524
529
  const members = [];
525
530
  const bodyNode = classNode.childForFieldName('body');
526
531
  if (!bodyNode) return members;
532
+ const isInterface = bodyNode.type === 'interface_body';
527
533
 
528
534
  for (let i = 0; i < bodyNode.namedChildCount; i++) {
529
535
  const child = bodyNode.namedChild(i);
@@ -536,6 +542,13 @@ function extractClassMembers(classNode, code) {
536
542
  if (nameNode) {
537
543
  const { startLine, endLine } = nodeToLocation(child, code);
538
544
  const modifiers = extractModifiers(child);
545
+ // Interface methods are implicitly public and abstract in Java
546
+ if (isInterface) {
547
+ if (!modifiers.includes('public')) modifiers.push('public');
548
+ if (!modifiers.includes('abstract') && !modifiers.includes('default') && !modifiers.includes('static')) {
549
+ modifiers.push('abstract');
550
+ }
551
+ }
539
552
  const returnType = extractReturnType(child);
540
553
  const docstring = extractJavaDocstring(code, startLine);
541
554
  const nameLine = nameNode.startPosition.row + 1;
@@ -181,6 +181,7 @@ function findFunctions(code, parser) {
181
181
  indent,
182
182
  isArrow: false,
183
183
  isGenerator: false,
184
+ isSignature: true,
184
185
  modifiers: [],
185
186
  ...(returnType && { returnType }),
186
187
  ...(generics && { generics }),
@@ -1950,6 +1951,13 @@ function findExportsInCode(code, parser) {
1950
1951
  exports.push({ name: propNode.text, type: 'exports', line });
1951
1952
  return true;
1952
1953
  }
1954
+
1955
+ // module.exports.name = ...
1956
+ if (objNode.type === 'member_expression' && objNode.text === 'module.exports') {
1957
+ const line = node.startPosition.row + 1;
1958
+ exports.push({ name: propNode.text, type: 'module.exports', line });
1959
+ return true;
1960
+ }
1953
1961
  }
1954
1962
  }
1955
1963
  return true;
@@ -231,6 +231,10 @@ function extractBases(classNode) {
231
231
  const arg = argsNode.namedChild(i);
232
232
  if (arg.type === 'identifier' || arg.type === 'attribute') {
233
233
  bases.push(arg.text);
234
+ } else if (arg.type === 'subscript') {
235
+ // Parameterized base: Generic[T], Protocol[T], Dict[str, int]
236
+ const baseNode = arg.childForFieldName('value');
237
+ if (baseNode) bases.push(baseNode.text);
234
238
  }
235
239
  }
236
240
  }
package/languages/rust.js CHANGED
@@ -77,7 +77,11 @@ function extractAttributes(node, code) {
77
77
  const attrContent = match[1];
78
78
  // Get just the attribute name (without arguments)
79
79
  const attrName = attrContent.split('(')[0].trim();
80
- attributes.push(attrName);
80
+ // Skip compiler hint attributes that aren't semantically meaningful for display
81
+ const SKIP_ATTRS = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
82
+ if (!SKIP_ATTRS.has(attrName)) {
83
+ attributes.push(attrName);
84
+ }
81
85
  }
82
86
  } else if (!line.startsWith('//')) {
83
87
  // Stop at non-comment, non-attribute lines
@@ -491,19 +495,25 @@ function extractImplInfo(implNode) {
491
495
  typeName = typeNode.text;
492
496
  }
493
497
 
494
- const prefix = typeParams ? `${typeParams} ` : '';
498
+ // Strip generic type arguments from typeName and traitName for lookup
499
+ // e.g., "CacheService<T>" → "CacheService", "Entity" stays "Entity"
500
+ const stripGenerics = (s) => s ? s.replace(/<[^>]*>/g, '').trim() : s;
501
+ const bareTypeName = stripGenerics(typeName);
502
+ const bareTraitName = stripGenerics(traitName);
503
+
495
504
  let name;
496
- if (traitName && typeName) {
497
- name = `${prefix}${traitName} for ${typeName}`;
498
- } else if (typeName) {
499
- name = `${prefix}${typeName}`;
505
+ if (bareTraitName && bareTypeName) {
506
+ // Use the concrete type as className so Task.get_id works for `impl Entity for Task`
507
+ name = bareTypeName;
508
+ } else if (bareTypeName) {
509
+ name = bareTypeName;
500
510
  } else {
501
511
  const text = implNode.text;
502
512
  const match = text.match(/impl\s*(?:<[^>]+>\s*)?(\w+(?:\s+for\s+\w+)?)/);
503
- name = match ? `${prefix}${match[1]}` : 'impl';
513
+ name = match ? match[1] : 'impl';
504
514
  }
505
515
 
506
- return { name, traitName, typeName };
516
+ return { name, traitName, typeName: bareTypeName, generics: typeParams || undefined };
507
517
  }
508
518
 
509
519
  /**
@@ -565,7 +575,9 @@ function extractTraitMembers(traitNode, code) {
565
575
  endLine,
566
576
  memberType: 'method',
567
577
  isMethod: true,
578
+ modifiers: ['public'], // Trait methods are implicitly public
568
579
  ...(paramsNode && { params: extractRustParams(paramsNode) }),
580
+ ...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'rust') }),
569
581
  ...(returnType && { returnType }),
570
582
  ...(hasSelf && { receiver: 'self' })
571
583
  });
@@ -100,9 +100,37 @@ function parseJSParam(param, info) {
100
100
  } else if (param.type === 'required_parameter' || param.type === 'optional_parameter') {
101
101
  const patternNode = param.childForFieldName('pattern');
102
102
  const typeNode = param.childForFieldName('type');
103
- if (patternNode) info.name = patternNode.text;
103
+ if (patternNode) {
104
+ // Check if pattern is a rest_pattern (e.g., ...args inside required_parameter)
105
+ if (patternNode.type === 'rest_pattern') {
106
+ const innerName = patternNode.namedChild(0);
107
+ info.name = innerName ? innerName.text : patternNode.text.replace(/^\.\.\./, '');
108
+ info.rest = true;
109
+ } else {
110
+ info.name = patternNode.text;
111
+ }
112
+ }
104
113
  if (typeNode) info.type = typeNode.text.replace(/^:\s*/, '');
105
114
  if (param.type === 'optional_parameter') info.optional = true;
115
+ // Check for default value (e.g., priority: number = 1)
116
+ const valueNode = param.childForFieldName('value');
117
+ if (valueNode) {
118
+ info.default = valueNode.text;
119
+ info.optional = true;
120
+ } else if (!info.rest) {
121
+ // Also check for bare number/string/etc. children as defaults
122
+ for (let i = 0; i < param.namedChildCount; i++) {
123
+ const child = param.namedChild(i);
124
+ if (child !== patternNode && child !== (typeNode && typeNode.parent === param ? typeNode : null) &&
125
+ child.type !== 'type_annotation' && child.type !== 'rest_pattern' &&
126
+ !['identifier', 'type_annotation'].includes(child.type)) {
127
+ // This is likely a default value node
128
+ info.default = child.text;
129
+ info.optional = true;
130
+ break;
131
+ }
132
+ }
133
+ }
106
134
  } else if (param.type === 'rest_parameter' || param.type === 'rest_pattern') {
107
135
  // rest_parameter = TypeScript, rest_pattern = JavaScript
108
136
  const patternNode = param.childForFieldName('pattern') || param.namedChild(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.8.3",
3
+ "version": "3.8.4",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
6
6
  "main": "index.js",