ucn 3.1.8 → 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/cli/index.js CHANGED
@@ -46,6 +46,10 @@ const flags = {
46
46
  includeTests: args.includes('--include-tests'),
47
47
  // Deadcode options
48
48
  includeExported: args.includes('--include-exported'),
49
+ // Uncertain matches (off by default)
50
+ includeUncertain: args.includes('--include-uncertain'),
51
+ // Detailed listing (e.g. toc with all symbols)
52
+ detailed: args.includes('--detailed'),
49
53
  // Output depth
50
54
  depth: args.find(a => a.startsWith('--depth='))?.split('=')[1] || null,
51
55
  // Inline expansion for callees
@@ -78,7 +82,7 @@ const knownFlags = new Set([
78
82
  '--json', '--verbose', '--no-quiet', '--quiet',
79
83
  '--code-only', '--with-types', '--top-level', '--exact',
80
84
  '--no-cache', '--clear-cache', '--include-tests',
81
- '--include-exported', '--expand', '--interactive', '-i', '--all', '--include-methods',
85
+ '--include-exported', '--expand', '--interactive', '-i', '--all', '--include-methods', '--include-uncertain', '--detailed',
82
86
  '--file', '--context', '--exclude', '--not', '--in',
83
87
  '--depth', '--add-param', '--remove-param', '--rename-to',
84
88
  '--default', '--top', '--no-follow-symlinks'
@@ -529,57 +533,11 @@ function usagesInFile(code, lines, name, filePath, result) {
529
533
  return;
530
534
  }
531
535
  } catch (e) {
532
- // Fall through to regex-based detection
536
+ // AST parsing failed usages will be empty, only definitions shown
533
537
  }
534
538
  }
535
539
 
536
- // Fallback to regex-based detection (for unsupported languages)
537
- const regex = new RegExp('\\b' + escapeRegExp(name) + '\\b');
538
- lines.forEach((line, idx) => {
539
- const lineNum = idx + 1;
540
-
541
- // Skip definition lines
542
- if (defs.some(d => d.startLine === lineNum)) {
543
- return;
544
- }
545
-
546
- if (regex.test(line)) {
547
- if (flags.codeOnly && isCommentOrString(line)) {
548
- return;
549
- }
550
-
551
- // Skip if the match is inside a string literal
552
- if (isInsideString(line, name)) {
553
- return;
554
- }
555
-
556
- const usageType = classifyUsage(line, name);
557
- const usage = {
558
- file: filePath,
559
- relativePath: filePath,
560
- line: lineNum,
561
- content: line,
562
- usageType,
563
- isDefinition: false
564
- };
565
-
566
- // Add context
567
- if (flags.context > 0) {
568
- const before = [];
569
- const after = [];
570
- for (let i = 1; i <= flags.context; i++) {
571
- if (idx - i >= 0) before.unshift(lines[idx - i]);
572
- if (idx + i < lines.length) after.push(lines[idx + i]);
573
- }
574
- usage.before = before;
575
- usage.after = after;
576
- }
577
-
578
- usages.push(usage);
579
- }
580
- });
581
-
582
- // Add definitions to result
540
+ // Output definitions + any usages found via AST
583
541
  const allUsages = [
584
542
  ...defs.map(d => ({
585
543
  ...d,
@@ -698,7 +656,7 @@ function runProjectCommand(rootDir, command, arg) {
698
656
 
699
657
  switch (command) {
700
658
  case 'toc': {
701
- const toc = index.getToc();
659
+ const toc = index.getToc({ detailed: flags.detailed, topLevel: flags.topLevel });
702
660
  printOutput(toc, output.formatTocJson, printProjectToc);
703
661
  break;
704
662
  }
@@ -745,7 +703,15 @@ function runProjectCommand(rootDir, command, arg) {
745
703
 
746
704
  case 'context': {
747
705
  requireArg(arg, 'Usage: ucn . context <name>');
748
- const ctx = index.context(arg, { includeMethods: flags.includeMethods, file: flags.file });
706
+ const ctx = index.context(arg, {
707
+ includeMethods: flags.includeMethods,
708
+ includeUncertain: flags.includeUncertain,
709
+ file: flags.file
710
+ });
711
+ if (!ctx) {
712
+ console.log(`Symbol "${arg}" not found.`);
713
+ break;
714
+ }
749
715
  printOutput(ctx,
750
716
  output.formatContextJson,
751
717
  r => { printContext(r, { expand: flags.expand, root: index.root }); }
@@ -776,7 +742,11 @@ function runProjectCommand(rootDir, command, arg) {
776
742
 
777
743
  case 'smart': {
778
744
  requireArg(arg, 'Usage: ucn . smart <name>');
779
- const smart = index.smart(arg, { withTypes: flags.withTypes, includeMethods: flags.includeMethods });
745
+ const smart = index.smart(arg, {
746
+ withTypes: flags.withTypes,
747
+ includeMethods: flags.includeMethods,
748
+ includeUncertain: flags.includeUncertain
749
+ });
780
750
  if (smart) {
781
751
  printOutput(smart, output.formatSmartJson, printSmart);
782
752
  } else {
@@ -952,6 +922,9 @@ function runProjectCommand(rootDir, command, arg) {
952
922
  const exported = item.isExported ? ' [exported]' : '';
953
923
  console.log(` ${output.lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}`);
954
924
  }
925
+ if (!flags.includeExported) {
926
+ console.log(`\nExported symbols excluded by default. Add --include-exported to include them.`);
927
+ }
955
928
  }
956
929
  );
957
930
  }
@@ -1023,26 +996,27 @@ function extractFunctionFromProject(index, name) {
1023
996
  return;
1024
997
  }
1025
998
 
999
+ let match;
1026
1000
  if (matches.length > 1 && !flags.file) {
1027
- // Disambiguation needed
1028
- console.log(output.formatDisambiguation(matches, name, 'fn'));
1029
- return;
1001
+ // Auto-select best match using same scoring as resolveSymbol
1002
+ match = pickBestDefinition(matches);
1003
+ console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Use --file to disambiguate.`);
1004
+ } else {
1005
+ match = matches[0];
1030
1006
  }
1031
1007
 
1032
- const match = matches[0];
1008
+ // Extract code directly using symbol index location (works for class methods and overloads)
1033
1009
  const code = fs.readFileSync(match.file, 'utf-8');
1034
- const language = detectLanguage(match.file);
1035
- const { fn, code: fnCode } = extractFunction(code, language, match.name);
1010
+ const lines = code.split('\n');
1011
+ const fnCode = lines.slice(match.startLine - 1, match.endLine).join('\n');
1036
1012
 
1037
- if (fn) {
1038
- if (flags.json) {
1039
- console.log(output.formatFunctionJson(fn, fnCode));
1040
- } else {
1041
- console.log(`${match.relativePath}:${fn.startLine}`);
1042
- console.log(`${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
1043
- console.log('─'.repeat(60));
1044
- console.log(fnCode);
1045
- }
1013
+ if (flags.json) {
1014
+ console.log(output.formatFunctionJson(match, fnCode));
1015
+ } else {
1016
+ console.log(`${match.relativePath}:${match.startLine}`);
1017
+ console.log(`${output.lineRange(match.startLine, match.endLine)} ${output.formatFunctionSignature(match)}`);
1018
+ console.log('─'.repeat(60));
1019
+ console.log(fnCode);
1046
1020
  }
1047
1021
  }
1048
1022
 
@@ -1056,13 +1030,15 @@ function extractClassFromProject(index, name) {
1056
1030
  return;
1057
1031
  }
1058
1032
 
1033
+ let match;
1059
1034
  if (matches.length > 1 && !flags.file) {
1060
- // Disambiguation needed
1061
- console.log(output.formatDisambiguation(matches, name, 'class'));
1062
- return;
1035
+ // Auto-select best match using same scoring as resolveSymbol
1036
+ match = pickBestDefinition(matches);
1037
+ console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Use --file to disambiguate.`);
1038
+ } else {
1039
+ match = matches[0];
1063
1040
  }
1064
1041
 
1065
- const match = matches[0];
1066
1042
  const code = fs.readFileSync(match.file, 'utf-8');
1067
1043
  const language = detectLanguage(match.file);
1068
1044
  const { cls, code: clsCode } = extractClass(code, language, match.name);
@@ -1080,29 +1056,61 @@ function extractClassFromProject(index, name) {
1080
1056
  }
1081
1057
 
1082
1058
  function printProjectToc(toc) {
1083
- console.log(`PROJECT: ${toc.totalFiles} files, ${toc.totalLines} lines`);
1084
- console.log(` ${toc.totalFunctions} functions, ${toc.totalClasses} classes, ${toc.totalState} state objects`);
1085
- console.log('═'.repeat(60));
1086
-
1087
- for (const file of toc.byFile) {
1088
- // Filter for top-level only if flag is set
1089
- let functions = file.functions;
1090
- if (flags.topLevel) {
1091
- functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
1092
- }
1059
+ const t = toc.totals;
1060
+ console.log(`PROJECT: ${t.files} files, ${t.lines} lines`);
1061
+ console.log(` ${t.functions} functions, ${t.classes} classes, ${t.state} state objects`);
1093
1062
 
1094
- if (functions.length === 0 && file.classes.length === 0) continue;
1095
-
1096
- console.log(`\n${file.file} (${file.lines} lines)`);
1063
+ // Show meta warnings only when there's something noteworthy
1064
+ const meta = toc.meta || {};
1065
+ const warnings = [];
1066
+ if (meta.dynamicImports) warnings.push(`${meta.dynamicImports} dynamic import(s)`);
1067
+ if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
1068
+ if (warnings.length) {
1069
+ const hint = meta.uncertain ? ' — use --include-uncertain to include all' : '';
1070
+ console.log(` Note: ${warnings.join(', ')}${hint}`);
1071
+ }
1097
1072
 
1098
- for (const fn of functions) {
1099
- console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
1073
+ // Hints
1074
+ if (toc.summary) {
1075
+ if (toc.summary.topFunctionFiles?.length) {
1076
+ const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
1077
+ console.log(` Most functions: ${hint}`);
1100
1078
  }
1079
+ if (toc.summary.topLineFiles?.length) {
1080
+ const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
1081
+ console.log(` Largest files: ${hint}`);
1082
+ }
1083
+ if (toc.summary.entryFiles?.length) {
1084
+ console.log(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
1085
+ }
1086
+ }
1101
1087
 
1102
- for (const cls of file.classes) {
1103
- console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
1088
+ console.log('═'.repeat(60));
1089
+ const hasDetail = toc.files.some(f => f.symbols);
1090
+ for (const file of toc.files) {
1091
+ const parts = [`${file.lines} lines`];
1092
+ if (file.functions) parts.push(`${file.functions} fn`);
1093
+ if (file.classes) parts.push(`${file.classes} cls`);
1094
+ if (file.state) parts.push(`${file.state} state`);
1095
+
1096
+ if (hasDetail) {
1097
+ console.log(`\n${file.file} (${parts.join(', ')})`);
1098
+ if (file.symbols) {
1099
+ for (const fn of file.symbols.functions) {
1100
+ console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
1101
+ }
1102
+ for (const cls of file.symbols.classes) {
1103
+ console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
1104
+ }
1105
+ }
1106
+ } else {
1107
+ console.log(` ${file.file} — ${parts.join(', ')}`);
1104
1108
  }
1105
1109
  }
1110
+
1111
+ if (!hasDetail) {
1112
+ console.log(`\nAdd --detailed to list all functions, or "ucn . about <name>" for full details on a symbol`);
1113
+ }
1106
1114
  }
1107
1115
 
1108
1116
  function printSymbols(symbols, query, options = {}) {
@@ -1552,7 +1560,7 @@ function printBestExample(index, name) {
1552
1560
 
1553
1561
  function printContext(ctx, options = {}) {
1554
1562
  // Handle struct/interface types differently
1555
- if (ctx.type && ['struct', 'interface', 'type'].includes(ctx.type)) {
1563
+ if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
1556
1564
  console.log(`Context for ${ctx.type} ${ctx.name}:`);
1557
1565
  console.log('═'.repeat(60));
1558
1566
 
@@ -1614,6 +1622,17 @@ function printContext(ctx, options = {}) {
1614
1622
  console.log(`Context for ${ctx.function}:`);
1615
1623
  console.log('═'.repeat(60));
1616
1624
 
1625
+ // Show meta note when results may be incomplete
1626
+ if (ctx.meta) {
1627
+ const notes = [];
1628
+ if (ctx.meta.dynamicImports) notes.push(`${ctx.meta.dynamicImports} dynamic import(s)`);
1629
+ if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
1630
+ if (notes.length) {
1631
+ const hint = ctx.meta.uncertain ? ' — use --include-uncertain to include all' : '';
1632
+ console.log(` Note: ${notes.join(', ')}${hint}`);
1633
+ }
1634
+ }
1635
+
1617
1636
  // Display warnings if any
1618
1637
  if (ctx.warnings && ctx.warnings.length > 0) {
1619
1638
  console.log('\n⚠️ WARNINGS:');
@@ -1763,6 +1782,18 @@ function printExpandedItem(item, root) {
1763
1782
  function printSmart(smart) {
1764
1783
  console.log(`${smart.target.name} (${smart.target.file}:${smart.target.startLine})`);
1765
1784
  console.log('═'.repeat(60));
1785
+
1786
+ // Show meta note when results may be incomplete
1787
+ if (smart.meta) {
1788
+ const notes = [];
1789
+ if (smart.meta.dynamicImports) notes.push(`${smart.meta.dynamicImports} dynamic import(s)`);
1790
+ if (smart.meta.uncertain) notes.push(`${smart.meta.uncertain} uncertain call(s) skipped`);
1791
+ if (notes.length) {
1792
+ const hint = smart.meta.uncertain ? ' — use --include-uncertain to include all' : '';
1793
+ console.log(` Note: ${notes.join(', ')}${hint}`);
1794
+ }
1795
+ }
1796
+
1766
1797
  console.log(smart.target.code);
1767
1798
 
1768
1799
  if (smart.dependencies.length > 0) {
@@ -2122,6 +2153,30 @@ function printLines(lines, range) {
2122
2153
  }
2123
2154
  }
2124
2155
 
2156
+ /**
2157
+ * Pick the best definition from multiple matches using same scoring as resolveSymbol.
2158
+ * Prefers lib/src/core over test/examples/vendor directories.
2159
+ */
2160
+ function pickBestDefinition(matches) {
2161
+ const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
2162
+ const scored = matches.map(m => {
2163
+ let score = 0;
2164
+ const rp = m.relativePath || '';
2165
+ // Prefer class/struct/interface types (+1000) - same as resolveSymbol
2166
+ if (typeOrder.has(m.type)) score += 1000;
2167
+ if (isTestFile(rp, detectLanguage(m.file))) score -= 500;
2168
+ if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
2169
+ if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
2170
+ // Tiebreaker: prefer larger function bodies (more important/complex)
2171
+ if (m.startLine && m.endLine) {
2172
+ score += Math.min(m.endLine - m.startLine, 100);
2173
+ }
2174
+ return { match: m, score };
2175
+ });
2176
+ scored.sort((a, b) => b.score - a.score);
2177
+ return scored[0].match;
2178
+ }
2179
+
2125
2180
  function suggestSimilar(query, names) {
2126
2181
  const lower = query.toLowerCase();
2127
2182
  const similar = names.filter(n => n.toLowerCase().includes(lower));
@@ -2137,27 +2192,6 @@ function escapeRegExp(text) {
2137
2192
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2138
2193
  }
2139
2194
 
2140
- function classifyUsage(line, name) {
2141
- // Check if it's an import first
2142
- if (/^\s*(import|from|require|use)\b/.test(line)) {
2143
- return 'import';
2144
- }
2145
- // Check if it's a function call (but not a method call)
2146
- if (new RegExp('\\b' + escapeRegExp(name) + '\\s*\\(').test(line)) {
2147
- // Exclude method calls (obj.name, this.name, JSON.name, etc.)
2148
- if (!isMethodCall(line, name)) {
2149
- return 'call';
2150
- }
2151
- }
2152
- return 'reference';
2153
- }
2154
-
2155
- function isMethodCall(line, name) {
2156
- // Check if there's a dot or ] immediately before the name
2157
- const methodPattern = new RegExp('[.\\]]\\s*' + escapeRegExp(name) + '\\s*\\(');
2158
- return methodPattern.test(line);
2159
- }
2160
-
2161
2195
  function isCommentOrString(line) {
2162
2196
  const trimmed = line.trim();
2163
2197
  return trimmed.startsWith('//') ||
@@ -2166,31 +2200,6 @@ function isCommentOrString(line) {
2166
2200
  trimmed.startsWith('/*');
2167
2201
  }
2168
2202
 
2169
- function isInsideString(line, name) {
2170
- // Simple heuristic: check if name appears inside quotes
2171
- // Find all string regions in the line
2172
- const stringRegex = /(['"`])(?:(?!\1|\\).|\\.)*\1/g;
2173
- let match;
2174
-
2175
- while ((match = stringRegex.exec(line)) !== null) {
2176
- const stringContent = match[0];
2177
- const stringStart = match.index;
2178
- const stringEnd = stringStart + stringContent.length;
2179
-
2180
- // Find where the name appears in the line
2181
- const nameRegex = new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g');
2182
- let nameMatch;
2183
- while ((nameMatch = nameRegex.exec(line)) !== null) {
2184
- const nameStart = nameMatch.index;
2185
- // Check if this name occurrence is inside the string
2186
- if (nameStart > stringStart && nameStart < stringEnd) {
2187
- return true;
2188
- }
2189
- }
2190
- }
2191
- return false;
2192
- }
2193
-
2194
2203
  function printUsage() {
2195
2204
  console.log(`UCN - Universal Code Navigator
2196
2205
 
@@ -2201,6 +2210,7 @@ Usage:
2201
2210
  ucn <file> [command] [args] Single file mode
2202
2211
  ucn <dir> [command] [args] Project mode (specific directory)
2203
2212
  ucn "pattern" [command] [args] Glob pattern mode
2213
+ (Default output is text; add --json for machine-readable JSON)
2204
2214
 
2205
2215
  ═══════════════════════════════════════════════════════════════════════════════
2206
2216
  UNDERSTAND CODE (UCN's strength - semantic analysis)
@@ -2413,7 +2423,7 @@ function executeInteractiveCommand(index, command, arg) {
2413
2423
  console.log('Usage: context <name>');
2414
2424
  return;
2415
2425
  }
2416
- const ctx = index.context(arg);
2426
+ const ctx = index.context(arg, { includeUncertain: flags.includeUncertain });
2417
2427
  printContext(ctx, { expand: flags.expand, root: index.root });
2418
2428
  break;
2419
2429
  }
@@ -2423,7 +2433,7 @@ function executeInteractiveCommand(index, command, arg) {
2423
2433
  console.log('Usage: smart <name>');
2424
2434
  return;
2425
2435
  }
2426
- const smart = index.smart(arg, {});
2436
+ const smart = index.smart(arg, { includeUncertain: flags.includeUncertain });
2427
2437
  if (smart) {
2428
2438
  printSmart(smart);
2429
2439
  } else {
package/core/discovery.js CHANGED
@@ -107,8 +107,7 @@ const TEST_PATTERNS = {
107
107
  ],
108
108
  rust: [
109
109
  /.*_test\.rs$/,
110
- /\/tests\//,
111
- /mod tests/
110
+ /\/tests\//
112
111
  ]
113
112
  };
114
113