ucn 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ucn might be problematic. Click here for more details.

package/README.md CHANGED
@@ -38,13 +38,8 @@ JavaScript, TypeScript, Python, Go, Rust, Java
38
38
 
39
39
  ## Install
40
40
 
41
- Not published to npm yet. Install from source:
42
-
43
41
  ```bash
44
- git clone https://github.com/mleoca/ucn.git
45
- cd ucn
46
- npm install
47
- npm link # makes 'ucn' available globally
42
+ npm install -g ucn
48
43
  ```
49
44
 
50
45
  ### Claude Code (optional)
@@ -53,6 +48,12 @@ To use UCN as a skill in Claude Code:
53
48
 
54
49
  ```bash
55
50
  mkdir -p ~/.claude/skills
51
+
52
+ # If installed via npm:
53
+ cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.claude/skills/
54
+
55
+ # If cloned from git:
56
+ git clone https://github.com/mleoca/ucn.git
56
57
  cp -r ucn/.claude/skills/ucn ~/.claude/skills/
57
58
  ```
58
59
 
@@ -62,6 +63,12 @@ To use UCN as a skill in OpenAI Codex:
62
63
 
63
64
  ```bash
64
65
  mkdir -p ~/.agents/skills
66
+
67
+ # If installed via npm:
68
+ cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.agents/skills/
69
+
70
+ # If cloned from git:
71
+ git clone https://github.com/mleoca/ucn.git
65
72
  cp -r ucn/.claude/skills/ucn ~/.agents/skills/
66
73
  ```
67
74
 
package/cli/index.js CHANGED
@@ -1550,6 +1550,66 @@ function printBestExample(index, name) {
1550
1550
  }
1551
1551
 
1552
1552
  function printContext(ctx, options = {}) {
1553
+ // Handle struct/interface types differently
1554
+ if (ctx.type && ['struct', 'interface', 'type'].includes(ctx.type)) {
1555
+ console.log(`Context for ${ctx.type} ${ctx.name}:`);
1556
+ console.log('═'.repeat(60));
1557
+
1558
+ // Display warnings if any
1559
+ if (ctx.warnings && ctx.warnings.length > 0) {
1560
+ console.log('\n⚠️ WARNINGS:');
1561
+ for (const w of ctx.warnings) {
1562
+ console.log(` ${w.message}`);
1563
+ }
1564
+ }
1565
+
1566
+ const expandable = [];
1567
+ let itemNum = 1;
1568
+
1569
+ // Show methods for structs/interfaces
1570
+ const methods = ctx.methods || [];
1571
+ console.log(`\nMETHODS (${methods.length}):`);
1572
+ for (const m of methods) {
1573
+ const receiver = m.receiver ? `(${m.receiver}) ` : '';
1574
+ const params = m.params || '...';
1575
+ const returnType = m.returnType ? `: ${m.returnType}` : '';
1576
+ console.log(` [${itemNum}] ${receiver}${m.name}(${params})${returnType}`);
1577
+ console.log(` ${m.file}:${m.line}`);
1578
+ expandable.push({
1579
+ num: itemNum++,
1580
+ type: 'method',
1581
+ name: m.name,
1582
+ relativePath: m.file,
1583
+ startLine: m.line
1584
+ });
1585
+ }
1586
+
1587
+ // Show callers (type references/usages)
1588
+ const callers = ctx.callers || [];
1589
+ console.log(`\nUSAGES (${callers.length}):`);
1590
+ for (const c of callers) {
1591
+ const callerName = c.callerName || '(module level)';
1592
+ const displayName = c.callerName ? ` [${callerName}]` : '';
1593
+ console.log(` [${itemNum}] ${c.relativePath}:${c.line}${displayName}`);
1594
+ expandable.push({
1595
+ num: itemNum++,
1596
+ type: 'usage',
1597
+ name: callerName,
1598
+ file: c.callerFile || c.file,
1599
+ relativePath: c.relativePath,
1600
+ line: c.line,
1601
+ startLine: c.callerStartLine || c.line,
1602
+ endLine: c.callerEndLine || c.line
1603
+ });
1604
+ console.log(` ${c.content.trim()}`);
1605
+ }
1606
+
1607
+ console.log(`\nUse "ucn . expand <N>" to see code for item N`);
1608
+ lastContextExpandable = expandable;
1609
+ return;
1610
+ }
1611
+
1612
+ // Standard function/method context
1553
1613
  console.log(`Context for ${ctx.function}:`);
1554
1614
  console.log('═'.repeat(60));
1555
1615
 
@@ -1565,8 +1625,9 @@ function printContext(ctx, options = {}) {
1565
1625
  const expandable = [];
1566
1626
  let itemNum = 1;
1567
1627
 
1568
- console.log(`\nCALLERS (${ctx.callers.length}):`);
1569
- for (const c of ctx.callers) {
1628
+ const callers = ctx.callers || [];
1629
+ console.log(`\nCALLERS (${callers.length}):`);
1630
+ for (const c of callers) {
1570
1631
  // All callers are numbered for expand command
1571
1632
  const callerName = c.callerName || '(module level)';
1572
1633
  const displayName = c.callerName ? ` [${callerName}]` : '';
@@ -1584,8 +1645,9 @@ function printContext(ctx, options = {}) {
1584
1645
  console.log(` ${c.content.trim()}`);
1585
1646
  }
1586
1647
 
1587
- console.log(`\nCALLEES (${ctx.callees.length}):`);
1588
- for (const c of ctx.callees) {
1648
+ const callees = ctx.callees || [];
1649
+ console.log(`\nCALLEES (${callees.length}):`);
1650
+ for (const c of callees) {
1589
1651
  const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
1590
1652
  console.log(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
1591
1653
  expandable.push({
package/core/output.js CHANGED
@@ -307,25 +307,59 @@ function formatUsagesJson(usages, name) {
307
307
  * Format context (callers + callees) as JSON
308
308
  */
309
309
  function formatContextJson(context) {
310
+ // Handle struct/interface types differently
311
+ if (context.type && ['struct', 'interface', 'type'].includes(context.type)) {
312
+ const callers = context.callers || [];
313
+ const methods = context.methods || [];
314
+ return JSON.stringify({
315
+ type: context.type,
316
+ name: context.name,
317
+ file: context.file,
318
+ startLine: context.startLine,
319
+ endLine: context.endLine,
320
+ methodCount: methods.length,
321
+ usageCount: callers.length,
322
+ methods: methods.map(m => ({
323
+ name: m.name,
324
+ file: m.file,
325
+ line: m.line,
326
+ params: m.params,
327
+ returnType: m.returnType,
328
+ receiver: m.receiver
329
+ })),
330
+ usages: callers.map(c => ({
331
+ file: c.relativePath || c.file,
332
+ line: c.line,
333
+ expression: c.content,
334
+ callerName: c.callerName
335
+ })),
336
+ ...(context.warnings && { warnings: context.warnings })
337
+ }, null, 2);
338
+ }
339
+
340
+ // Standard function/method context
341
+ const callers = context.callers || [];
342
+ const callees = context.callees || [];
310
343
  return JSON.stringify({
311
344
  function: context.function,
312
345
  file: context.file,
313
- callerCount: context.callers.length,
314
- calleeCount: context.callees.length,
315
- callers: context.callers.map(c => ({
346
+ callerCount: callers.length,
347
+ calleeCount: callees.length,
348
+ callers: callers.map(c => ({
316
349
  file: c.relativePath || c.file,
317
350
  line: c.line,
318
351
  expression: c.content, // FULL expression
319
352
  callerName: c.callerName
320
353
  })),
321
- callees: context.callees.map(c => ({
354
+ callees: callees.map(c => ({
322
355
  name: c.name,
323
356
  type: c.type,
324
357
  file: c.relativePath || c.file,
325
358
  line: c.startLine,
326
359
  params: c.params, // FULL params
327
360
  weight: c.weight || 'normal' // Dependency weight: core, setup, utility
328
- }))
361
+ })),
362
+ ...(context.warnings && { warnings: context.warnings })
329
363
  }, null, 2);
330
364
  }
331
365
 
package/core/project.js CHANGED
@@ -166,7 +166,11 @@ class ProjectIndex {
166
166
  ...(item.extends && { extends: item.extends }),
167
167
  ...(item.implements && { implements: item.implements }),
168
168
  ...(item.indent !== undefined && { indent: item.indent }),
169
- ...(item.isNested && { isNested: item.isNested })
169
+ ...(item.isNested && { isNested: item.isNested }),
170
+ ...(item.isMethod && { isMethod: item.isMethod }),
171
+ ...(item.receiver && { receiver: item.receiver }),
172
+ ...(item.className && { className: item.className }),
173
+ ...(item.memberType && { memberType: item.memberType })
170
174
  };
171
175
  fileEntry.symbols.push(symbol);
172
176
 
@@ -649,6 +653,55 @@ class ProjectIndex {
649
653
  return usages;
650
654
  }
651
655
 
656
+ /**
657
+ * Find methods that belong to a class/struct/type
658
+ * Works for:
659
+ * - Go: methods with receiver field (e.g., receiver: "*TypeName")
660
+ * - Python/Java: methods with className field
661
+ * - Rust: impl methods with receiver field
662
+ * @param {string} typeName - The class/struct/interface name
663
+ * @returns {Array} Methods belonging to this type
664
+ */
665
+ findMethodsForType(typeName) {
666
+ const methods = [];
667
+ // Match both "TypeName" and "*TypeName" receivers (for Go/Rust pointer receivers)
668
+ const baseTypeName = typeName.replace(/^\*/, '');
669
+
670
+ for (const [name, symbols] of this.symbols) {
671
+ for (const symbol of symbols) {
672
+ // Skip non-method types (fields, properties, etc.)
673
+ if (symbol.type === 'field' || symbol.type === 'property') {
674
+ continue;
675
+ }
676
+
677
+ // Check Go/Rust-style receiver (e.g., func (r *Router) Method())
678
+ if (symbol.isMethod && symbol.receiver) {
679
+ const receiverBase = symbol.receiver.replace(/^\*/, '');
680
+ if (receiverBase === baseTypeName) {
681
+ methods.push(symbol);
682
+ continue;
683
+ }
684
+ }
685
+
686
+ // Check Python/Java/JS-style className (class members)
687
+ // Must be a method type, not just any symbol with className
688
+ if (symbol.className === baseTypeName &&
689
+ (symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor')) {
690
+ methods.push(symbol);
691
+ continue;
692
+ }
693
+ }
694
+ }
695
+
696
+ // Sort by file then line
697
+ methods.sort((a, b) => {
698
+ if (a.relativePath !== b.relativePath) return a.relativePath.localeCompare(b.relativePath);
699
+ return a.startLine - b.startLine;
700
+ });
701
+
702
+ return methods;
703
+ }
704
+
652
705
  /**
653
706
  * Get context for a symbol (callers + callees)
654
707
  */
@@ -658,7 +711,53 @@ class ProjectIndex {
658
711
  return { function: name, file: null, callers: [], callees: [] };
659
712
  }
660
713
 
661
- const def = definitions[0]; // Use first definition
714
+ // Prefer class/struct/interface definitions over functions/methods/constructors
715
+ // This ensures context('ClassName') finds the class, not a constructor with same name
716
+ const typeOrder = ['class', 'struct', 'interface', 'type', 'impl'];
717
+ let def = definitions[0];
718
+ for (const d of definitions) {
719
+ if (typeOrder.includes(d.type)) {
720
+ def = d;
721
+ break;
722
+ }
723
+ }
724
+
725
+ // Special handling for class/struct/interface types
726
+ if (['class', 'struct', 'interface', 'type'].includes(def.type)) {
727
+ const methods = this.findMethodsForType(name);
728
+
729
+ const result = {
730
+ type: def.type,
731
+ name: name,
732
+ file: def.relativePath,
733
+ startLine: def.startLine,
734
+ endLine: def.endLine,
735
+ methods: methods.map(m => ({
736
+ name: m.name,
737
+ file: m.relativePath,
738
+ line: m.startLine,
739
+ params: m.params,
740
+ returnType: m.returnType,
741
+ receiver: m.receiver
742
+ })),
743
+ // Also include places where the type is used in function parameters/returns
744
+ callers: this.findCallers(name, { includeMethods: options.includeMethods })
745
+ };
746
+
747
+ if (definitions.length > 1) {
748
+ result.warnings = [{
749
+ type: 'ambiguous',
750
+ message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
751
+ alternatives: definitions.slice(1).map(d => ({
752
+ file: d.relativePath,
753
+ line: d.startLine
754
+ }))
755
+ }];
756
+ }
757
+
758
+ return result;
759
+ }
760
+
662
761
  const callers = this.findCallers(name, { includeMethods: options.includeMethods });
663
762
  const callees = this.findCallees(def, { includeMethods: options.includeMethods });
664
763
 
@@ -787,8 +886,9 @@ class ProjectIndex {
787
886
  if (call.isMethod) {
788
887
  // Always skip this/self/cls calls (internal state access, not function calls)
789
888
  if (['this', 'self', 'cls'].includes(call.receiver)) continue;
790
- // Skip other method calls unless explicitly requested
791
- if (!options.includeMethods) continue;
889
+ // Go doesn't use this/self/cls - always include Go method calls
890
+ // For other languages, skip method calls unless explicitly requested
891
+ if (fileEntry.language !== 'go' && !options.includeMethods) continue;
792
892
  }
793
893
 
794
894
  // Skip definition lines
@@ -1594,7 +1694,14 @@ class ProjectIndex {
1594
1694
  for (const [name, symbols] of this.symbols) {
1595
1695
  for (const symbol of symbols) {
1596
1696
  // Skip non-function/class types
1597
- if (!['function', 'class', 'method'].includes(symbol.type)) {
1697
+ // Include various method types from different languages:
1698
+ // - function: standalone functions
1699
+ // - class, struct, interface: type definitions (skip them in deadcode)
1700
+ // - method: class methods
1701
+ // - static, public, abstract: Java method modifiers used as types
1702
+ // - constructor: constructors
1703
+ const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
1704
+ if (!callableTypes.includes(symbol.type)) {
1598
1705
  continue;
1599
1706
  }
1600
1707
 
@@ -1605,11 +1712,33 @@ class ProjectIndex {
1605
1712
 
1606
1713
  // Check if exported
1607
1714
  const fileEntry = this.files.get(symbol.file);
1715
+ const lang = fileEntry?.language;
1716
+ const mods = symbol.modifiers || [];
1717
+
1718
+ // Language-specific entry points (called by runtime, no AST-visible callers)
1719
+ // Go: main() and init() are called by runtime
1720
+ const isGoEntryPoint = lang === 'go' && (name === 'main' || name === 'init');
1721
+
1722
+ // Java: public static void main(String[] args) is the entry point
1723
+ const isJavaEntryPoint = lang === 'java' && name === 'main' &&
1724
+ mods.includes('public') && mods.includes('static');
1725
+
1726
+ // Python: Magic/dunder methods are called by the interpreter, not user code
1727
+ const isPythonMagicMethod = lang === 'python' && /^__\w+__$/.test(name);
1728
+
1729
+ // Rust: main() is entry point, #[test] functions are called by test runner
1730
+ const isRustEntryPoint = lang === 'rust' &&
1731
+ (name === 'main' || mods.includes('test'));
1732
+
1733
+ const isEntryPoint = isGoEntryPoint || isJavaEntryPoint ||
1734
+ isPythonMagicMethod || isRustEntryPoint;
1735
+
1608
1736
  const isExported = fileEntry && (
1609
1737
  fileEntry.exports.includes(name) ||
1610
- (symbol.modifiers && symbol.modifiers.includes('export')) ||
1611
- (symbol.modifiers && symbol.modifiers.includes('public')) ||
1612
- (fileEntry.language === 'go' && /^[A-Z]/.test(name))
1738
+ mods.includes('export') ||
1739
+ mods.includes('public') ||
1740
+ (lang === 'go' && /^[A-Z]/.test(name)) ||
1741
+ isEntryPoint
1613
1742
  );
1614
1743
 
1615
1744
  // Skip exported unless requested
@@ -3130,7 +3259,7 @@ class ProjectIndex {
3130
3259
  }
3131
3260
 
3132
3261
  const cacheData = {
3133
- version: 2, // Bump version for new cache format
3262
+ version: 4, // v4: className, memberType, isMethod for all languages
3134
3263
  root: this.root,
3135
3264
  buildTime: this.buildTime,
3136
3265
  timestamp: Date.now(),
@@ -3163,8 +3292,10 @@ class ProjectIndex {
3163
3292
  try {
3164
3293
  const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
3165
3294
 
3166
- // Check version compatibility (support v1 and v2)
3167
- if (cacheData.version !== 1 && cacheData.version !== 2) {
3295
+ // Check version compatibility
3296
+ // v4 adds className, memberType, isMethod for all languages
3297
+ // Only accept exactly version 4 (or future versions handled explicitly)
3298
+ if (cacheData.version !== 4) {
3168
3299
  return false;
3169
3300
  }
3170
3301
 
package/languages/java.js CHANGED
@@ -111,6 +111,12 @@ function findFunctions(code, parser) {
111
111
  if (processedRanges.has(rangeKey)) return true;
112
112
  processedRanges.add(rangeKey);
113
113
 
114
+ // Skip methods inside a class body (they're extracted as class members)
115
+ let parent = node.parent;
116
+ if (parent && parent.type === 'class_body') {
117
+ return true; // Skip - this is a class method
118
+ }
119
+
114
120
  const nameNode = node.childForFieldName('name');
115
121
  const paramsNode = node.childForFieldName('parameters');
116
122
 
@@ -393,6 +399,7 @@ function extractClassMembers(classNode, code) {
393
399
  endLine,
394
400
  memberType,
395
401
  modifiers,
402
+ isMethod: true, // Mark as method for context() lookups
396
403
  ...(returnType && { returnType }),
397
404
  ...(docstring && { docstring })
398
405
  });
@@ -417,6 +424,7 @@ function extractClassMembers(classNode, code) {
417
424
  endLine,
418
425
  memberType: 'constructor',
419
426
  modifiers,
427
+ isMethod: true, // Mark as method for context() lookups
420
428
  ...(docstring && { docstring })
421
429
  });
422
430
  }
@@ -531,6 +531,7 @@ function extractClassMembers(classNode, code) {
531
531
  memberType,
532
532
  isAsync,
533
533
  isGenerator: isGen,
534
+ isMethod: true, // Mark as method for context() lookups
534
535
  ...(returnType && { returnType }),
535
536
  ...(docstring && { docstring })
536
537
  });
@@ -557,6 +558,7 @@ function extractClassMembers(classNode, code) {
557
558
  endLine,
558
559
  memberType: name.startsWith('#') ? 'private' : 'field',
559
560
  isArrow: true,
561
+ isMethod: true, // Arrow fields are callable like methods
560
562
  ...(returnType && { returnType })
561
563
  });
562
564
  } else {
@@ -565,6 +567,7 @@ function extractClassMembers(classNode, code) {
565
567
  startLine,
566
568
  endLine,
567
569
  memberType: name.startsWith('#') ? 'private field' : 'field'
570
+ // Not a method - regular field
568
571
  });
569
572
  }
570
573
  }
@@ -73,6 +73,20 @@ function findFunctions(code, parser) {
73
73
  if (processedRanges.has(rangeKey)) return true;
74
74
  processedRanges.add(rangeKey);
75
75
 
76
+ // Skip functions that are inside a class (they're extracted as class members)
77
+ let parent = node.parent;
78
+ // Handle decorated_definition wrapper
79
+ if (parent && parent.type === 'decorated_definition') {
80
+ parent = parent.parent;
81
+ }
82
+ // Check if parent is a class body (block inside class_definition)
83
+ if (parent && parent.type === 'block') {
84
+ const grandparent = parent.parent;
85
+ if (grandparent && grandparent.type === 'class_definition') {
86
+ return true; // Skip - this is a class method
87
+ }
88
+ }
89
+
76
90
  const nameNode = node.childForFieldName('name');
77
91
  const paramsNode = node.childForFieldName('parameters');
78
92
 
@@ -282,6 +296,7 @@ function extractClassMembers(classNode, code) {
282
296
  endLine,
283
297
  memberType,
284
298
  isAsync,
299
+ isMethod: true, // Mark as method for context() lookups
285
300
  ...(returnType && { returnType }),
286
301
  ...(docstring && { docstring }),
287
302
  ...(memberDecorators.length > 0 && { decorators: memberDecorators })
package/languages/rust.js CHANGED
@@ -51,6 +51,39 @@ function extractVisibility(text) {
51
51
  return null;
52
52
  }
53
53
 
54
+ /**
55
+ * Extract attributes from a function node (e.g., #[test], #[tokio::main])
56
+ * @param {Node} node - AST node
57
+ * @param {string} code - Source code
58
+ * @returns {string[]} Array of attribute names
59
+ */
60
+ function extractAttributes(node, code) {
61
+ const attributes = [];
62
+ const lines = code.split('\n');
63
+
64
+ // Look at lines before the function for attributes
65
+ const startLine = node.startPosition.row;
66
+ for (let i = startLine - 1; i >= 0 && i >= startLine - 5; i--) {
67
+ const line = lines[i]?.trim();
68
+ if (!line) continue;
69
+ if (line.startsWith('#[')) {
70
+ // Extract attribute name (e.g., #[test] -> test, #[tokio::main] -> tokio::main)
71
+ const match = line.match(/#\[([^\]]+)\]/);
72
+ if (match) {
73
+ const attrContent = match[1];
74
+ // Get just the attribute name (without arguments)
75
+ const attrName = attrContent.split('(')[0].trim();
76
+ attributes.push(attrName);
77
+ }
78
+ } else if (!line.startsWith('//')) {
79
+ // Stop at non-comment, non-attribute lines
80
+ break;
81
+ }
82
+ }
83
+
84
+ return attributes;
85
+ }
86
+
54
87
  /**
55
88
  * Find all functions in Rust code using tree-sitter
56
89
  */
@@ -66,6 +99,19 @@ function findFunctions(code, parser) {
66
99
  if (processedRanges.has(rangeKey)) return true;
67
100
  processedRanges.add(rangeKey);
68
101
 
102
+ // Skip functions inside impl blocks (they're extracted as impl members)
103
+ let parent = node.parent;
104
+ if (parent && (parent.type === 'impl_item' || parent.type === 'declaration_list')) {
105
+ // declaration_list is the body of an impl block
106
+ const grandparent = parent.parent;
107
+ if (grandparent && grandparent.type === 'impl_item') {
108
+ return true; // Skip - this is an impl method
109
+ }
110
+ if (parent.type === 'impl_item') {
111
+ return true; // Skip - this is an impl method
112
+ }
113
+ }
114
+
69
115
  const nameNode = node.childForFieldName('name');
70
116
  const paramsNode = node.childForFieldName('parameters');
71
117
 
@@ -81,12 +127,17 @@ function findFunctions(code, parser) {
81
127
  const returnType = extractReturnType(node);
82
128
  const docstring = extractRustDocstring(code, startLine);
83
129
  const generics = extractGenerics(node);
130
+ const attributes = extractAttributes(node, code);
84
131
 
85
132
  const modifiers = [];
86
133
  if (visibility) modifiers.push(visibility);
87
134
  if (isAsync) modifiers.push('async');
88
135
  if (isUnsafe) modifiers.push('unsafe');
89
136
  if (isConst) modifiers.push('const');
137
+ // Add attributes like #[test] to modifiers
138
+ for (const attr of attributes) {
139
+ modifiers.push(attr);
140
+ }
90
141
 
91
142
  functions.push({
92
143
  name: nameNode.text,
@@ -228,7 +279,7 @@ function findClasses(code, parser) {
228
279
  type: 'impl',
229
280
  traitName: implInfo.traitName,
230
281
  typeName: implInfo.typeName,
231
- members: extractImplMembers(node, code),
282
+ members: extractImplMembers(node, code, implInfo.typeName),
232
283
  modifiers: [],
233
284
  ...(docstring && { docstring })
234
285
  });
@@ -390,8 +441,11 @@ function extractImplInfo(implNode) {
390
441
 
391
442
  /**
392
443
  * Extract impl block members (functions)
444
+ * @param {Node} implNode - The impl block AST node
445
+ * @param {string} code - Source code
446
+ * @param {string} [typeName] - The type this impl is for (e.g., "MyStruct")
393
447
  */
394
- function extractImplMembers(implNode, code) {
448
+ function extractImplMembers(implNode, code, typeName) {
395
449
  const members = [];
396
450
  const bodyNode = implNode.childForFieldName('body');
397
451
  if (!bodyNode) return members;
@@ -411,6 +465,9 @@ function extractImplMembers(implNode, code) {
411
465
  const docstring = extractRustDocstring(code, startLine);
412
466
  const visibility = extractVisibility(text);
413
467
 
468
+ // Check if this is a method (has self parameter) or associated function
469
+ const hasSelf = paramsNode && paramsNode.text.includes('self');
470
+
414
471
  members.push({
415
472
  name: nameNode.text,
416
473
  params: extractRustParams(paramsNode),
@@ -419,6 +476,8 @@ function extractImplMembers(implNode, code) {
419
476
  endLine,
420
477
  memberType: visibility ? 'public' : 'method',
421
478
  isAsync: firstLine.includes('async '),
479
+ isMethod: true, // Mark as method for context() lookups
480
+ ...(typeName && { receiver: typeName }), // Track which type this impl is for
422
481
  ...(returnType && { returnType }),
423
482
  ...(docstring && { docstring })
424
483
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Code navigation built by AI, for AI. Reduces context usage by 90%+ when working with large codebases.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -3557,7 +3557,7 @@ function helper() { return 42; }
3557
3557
 
3558
3558
  // Verify cache file has callsCache
3559
3559
  const cacheData = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
3560
- assert.strictEqual(cacheData.version, 2, 'Cache version should be 2');
3560
+ assert.strictEqual(cacheData.version, 4, 'Cache version should be 4 (className, memberType, isMethod for all languages)');
3561
3561
  assert.ok(Array.isArray(cacheData.callsCache), 'Cache should have callsCache array');
3562
3562
  assert.ok(cacheData.callsCache.length > 0, 'callsCache should have entries');
3563
3563
 
@@ -3657,5 +3657,647 @@ function helper() { return 42; }
3657
3657
  });
3658
3658
  });
3659
3659
 
3660
+ // ============================================================================
3661
+ // REGRESSION TESTS: Go-specific bug fixes (2026-02)
3662
+ // ============================================================================
3663
+
3664
+ describe('Regression: Go entry points not flagged as deadcode', () => {
3665
+ it('should NOT report main() as dead code in Go', () => {
3666
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-main-'));
3667
+ try {
3668
+ // Create a Go project with main and init functions
3669
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/test\n\ngo 1.21\n');
3670
+ fs.writeFileSync(path.join(tmpDir, 'main.go'), `package main
3671
+
3672
+ func main() {
3673
+ run()
3674
+ }
3675
+
3676
+ func init() {
3677
+ setup()
3678
+ }
3679
+
3680
+ func run() {
3681
+ println("running")
3682
+ }
3683
+
3684
+ func setup() {
3685
+ println("setup")
3686
+ }
3687
+
3688
+ func unusedHelper() {
3689
+ println("unused")
3690
+ }
3691
+ `);
3692
+
3693
+ const index = new ProjectIndex(tmpDir);
3694
+ index.build('**/*.go', { quiet: true });
3695
+
3696
+ const deadcode = index.deadcode();
3697
+ const deadNames = deadcode.map(d => d.name);
3698
+
3699
+ // main and init should NOT be reported as dead
3700
+ assert.ok(!deadNames.includes('main'), 'main() should not be flagged as dead code');
3701
+ assert.ok(!deadNames.includes('init'), 'init() should not be flagged as dead code');
3702
+
3703
+ // run and setup are called, so not dead
3704
+ assert.ok(!deadNames.includes('run'), 'run() is called by main, not dead');
3705
+ assert.ok(!deadNames.includes('setup'), 'setup() is called by init, not dead');
3706
+
3707
+ // unusedHelper should be flagged as dead
3708
+ assert.ok(deadNames.includes('unusedHelper'), 'unusedHelper() should be flagged as dead code');
3709
+ } finally {
3710
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3711
+ }
3712
+ });
3713
+ });
3714
+
3715
+ describe('Regression: Go method calls included in findCallers', () => {
3716
+ it('should find Go method call sites without --include-methods flag', () => {
3717
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-methods-'));
3718
+ try {
3719
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/test\n\ngo 1.21\n');
3720
+ fs.writeFileSync(path.join(tmpDir, 'server.go'), `package main
3721
+
3722
+ type Server struct {
3723
+ port int
3724
+ }
3725
+
3726
+ func (s *Server) Start() {
3727
+ s.listen()
3728
+ }
3729
+
3730
+ func (s *Server) listen() {
3731
+ println("listening on", s.port)
3732
+ }
3733
+
3734
+ func main() {
3735
+ srv := &Server{port: 8080}
3736
+ srv.Start()
3737
+ }
3738
+ `);
3739
+
3740
+ const index = new ProjectIndex(tmpDir);
3741
+ index.build('**/*.go', { quiet: true });
3742
+
3743
+ // Find callers of Start method - should find the call in main
3744
+ const callers = index.findCallers('Start');
3745
+
3746
+ assert.strictEqual(callers.length, 1, 'Should find 1 caller for Start method');
3747
+ assert.strictEqual(callers[0].callerName, 'main', 'Caller should be main function');
3748
+ assert.ok(callers[0].content.includes('srv.Start()'), 'Should capture the method call');
3749
+ } finally {
3750
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3751
+ }
3752
+ });
3753
+
3754
+ it('should still filter this/self/cls in non-Go languages', () => {
3755
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-py-self-'));
3756
+ try {
3757
+ fs.writeFileSync(path.join(tmpDir, 'app.py'), `class Server:
3758
+ def __init__(self, port):
3759
+ self.port = port
3760
+
3761
+ def start(self):
3762
+ self.listen()
3763
+
3764
+ def listen(self):
3765
+ print(f"listening on {self.port}")
3766
+
3767
+ def main():
3768
+ srv = Server(8080)
3769
+ srv.start()
3770
+ `);
3771
+
3772
+ const index = new ProjectIndex(tmpDir);
3773
+ index.build('**/*.py', { quiet: true });
3774
+
3775
+ // self.listen() should be filtered out (self call)
3776
+ // but srv.start() should be included (external call)
3777
+ const listenCallers = index.findCallers('listen');
3778
+ // self.listen() is internal, so it depends on implementation
3779
+ // At minimum, without --include-methods, non-self calls should not show
3780
+
3781
+ const startCallers = index.findCallers('start');
3782
+ // srv.start() is a method call with receiver 'srv', which is not this/self/cls
3783
+ // But since it's Python (not Go), it should be filtered unless --include-methods
3784
+ assert.strictEqual(startCallers.length, 0, 'Python method calls should be filtered by default');
3785
+
3786
+ // With --include-methods, should find the call
3787
+ const startCallersIncluded = index.findCallers('start', { includeMethods: true });
3788
+ assert.strictEqual(startCallersIncluded.length, 1, 'With includeMethods, should find srv.start()');
3789
+ } finally {
3790
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3791
+ }
3792
+ });
3793
+ });
3794
+
3795
+ describe('Regression: context for structs shows methods', () => {
3796
+ it('should return methods for Go struct types', () => {
3797
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-struct-'));
3798
+ try {
3799
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/test\n\ngo 1.21\n');
3800
+ fs.writeFileSync(path.join(tmpDir, 'types.go'), `package main
3801
+
3802
+ type User struct {
3803
+ Name string
3804
+ Email string
3805
+ }
3806
+
3807
+ func (u *User) Validate() bool {
3808
+ return u.Name != "" && u.Email != ""
3809
+ }
3810
+
3811
+ func (u *User) String() string {
3812
+ return u.Name + " <" + u.Email + ">"
3813
+ }
3814
+
3815
+ func (u User) IsEmpty() bool {
3816
+ return u.Name == "" && u.Email == ""
3817
+ }
3818
+ `);
3819
+
3820
+ const index = new ProjectIndex(tmpDir);
3821
+ index.build('**/*.go', { quiet: true });
3822
+
3823
+ const ctx = index.context('User');
3824
+
3825
+ // Should identify as struct type
3826
+ assert.strictEqual(ctx.type, 'struct', 'User should be identified as struct');
3827
+ assert.strictEqual(ctx.name, 'User', 'Should return correct name');
3828
+
3829
+ // Should have methods
3830
+ assert.ok(ctx.methods, 'Should have methods array');
3831
+ assert.strictEqual(ctx.methods.length, 3, 'User struct should have 3 methods');
3832
+
3833
+ const methodNames = ctx.methods.map(m => m.name);
3834
+ assert.ok(methodNames.includes('Validate'), 'Should include Validate method');
3835
+ assert.ok(methodNames.includes('String'), 'Should include String method');
3836
+ assert.ok(methodNames.includes('IsEmpty'), 'Should include IsEmpty method');
3837
+
3838
+ // Methods should have receiver info
3839
+ const validateMethod = ctx.methods.find(m => m.name === 'Validate');
3840
+ assert.strictEqual(validateMethod.receiver, '*User', 'Validate has pointer receiver');
3841
+
3842
+ const isEmptyMethod = ctx.methods.find(m => m.name === 'IsEmpty');
3843
+ assert.strictEqual(isEmptyMethod.receiver, 'User', 'IsEmpty has value receiver');
3844
+ } finally {
3845
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3846
+ }
3847
+ });
3848
+
3849
+ it('should return empty methods for struct with no methods', () => {
3850
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-struct-empty-'));
3851
+ try {
3852
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/test\n\ngo 1.21\n');
3853
+ fs.writeFileSync(path.join(tmpDir, 'types.go'), `package main
3854
+
3855
+ type Config struct {
3856
+ Port int
3857
+ Host string
3858
+ }
3859
+ `);
3860
+
3861
+ const index = new ProjectIndex(tmpDir);
3862
+ index.build('**/*.go', { quiet: true });
3863
+
3864
+ const ctx = index.context('Config');
3865
+
3866
+ assert.strictEqual(ctx.type, 'struct', 'Config should be identified as struct');
3867
+ assert.ok(ctx.methods, 'Should have methods array');
3868
+ assert.strictEqual(ctx.methods.length, 0, 'Config struct should have 0 methods');
3869
+ } finally {
3870
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3871
+ }
3872
+ });
3873
+ });
3874
+
3875
+ describe('Regression: receiver field preserved in Go method symbols', () => {
3876
+ it('should store receiver info for Go methods in symbol index', () => {
3877
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-receiver-'));
3878
+ try {
3879
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/test\n\ngo 1.21\n');
3880
+ fs.writeFileSync(path.join(tmpDir, 'handler.go'), `package main
3881
+
3882
+ type Handler struct{}
3883
+
3884
+ func (h *Handler) ServeHTTP(w, r) {
3885
+ h.handleRequest(w, r)
3886
+ }
3887
+
3888
+ func (h *Handler) handleRequest(w, r) {
3889
+ println("handling")
3890
+ }
3891
+ `);
3892
+
3893
+ const index = new ProjectIndex(tmpDir);
3894
+ index.build('**/*.go', { quiet: true });
3895
+
3896
+ // Check that receiver is preserved in symbols
3897
+ const serveHTTP = index.symbols.get('ServeHTTP');
3898
+ assert.ok(serveHTTP, 'ServeHTTP should be indexed');
3899
+ assert.strictEqual(serveHTTP.length, 1, 'Should have one definition');
3900
+ assert.strictEqual(serveHTTP[0].receiver, '*Handler', 'Receiver should be *Handler');
3901
+ assert.strictEqual(serveHTTP[0].isMethod, true, 'Should be marked as method');
3902
+
3903
+ const handleRequest = index.symbols.get('handleRequest');
3904
+ assert.ok(handleRequest, 'handleRequest should be indexed');
3905
+ assert.strictEqual(handleRequest[0].receiver, '*Handler', 'Receiver should be *Handler');
3906
+ } finally {
3907
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3908
+ }
3909
+ });
3910
+ });
3911
+
3912
+ // ============================================================================
3913
+ // REGRESSION TESTS: Multi-language class/method handling (2026-02)
3914
+ // ============================================================================
3915
+
3916
+ describe('Regression: Python class methods in context', () => {
3917
+ it('should show methods for Python classes via className', () => {
3918
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-py-class-'));
3919
+ try {
3920
+ fs.writeFileSync(path.join(tmpDir, 'user.py'), `class User:
3921
+ def __init__(self, name):
3922
+ self.name = name
3923
+
3924
+ def greet(self):
3925
+ return f"Hello {self.name}"
3926
+
3927
+ def validate(self):
3928
+ return len(self.name) > 0
3929
+
3930
+ @staticmethod
3931
+ def create(name):
3932
+ return User(name)
3933
+ `);
3934
+
3935
+ const index = new ProjectIndex(tmpDir);
3936
+ index.build('**/*.py', { quiet: true });
3937
+
3938
+ const ctx = index.context('User');
3939
+
3940
+ // Should identify as class
3941
+ assert.strictEqual(ctx.type, 'class', 'User should be identified as class');
3942
+ assert.ok(ctx.methods, 'Should have methods array');
3943
+ assert.strictEqual(ctx.methods.length, 4, 'User class should have 4 methods');
3944
+
3945
+ const methodNames = ctx.methods.map(m => m.name);
3946
+ assert.ok(methodNames.includes('__init__'), 'Should include __init__');
3947
+ assert.ok(methodNames.includes('greet'), 'Should include greet');
3948
+ assert.ok(methodNames.includes('validate'), 'Should include validate');
3949
+ assert.ok(methodNames.includes('create'), 'Should include create');
3950
+ } finally {
3951
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3952
+ }
3953
+ });
3954
+ });
3955
+
3956
+ describe('Regression: Java class methods in context', () => {
3957
+ it('should show methods for Java classes via className', () => {
3958
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-java-class-'));
3959
+ try {
3960
+ fs.writeFileSync(path.join(tmpDir, 'User.java'), `public class User {
3961
+ private String name;
3962
+
3963
+ public User(String name) {
3964
+ this.name = name;
3965
+ }
3966
+
3967
+ public String greet() {
3968
+ return "Hello " + this.name;
3969
+ }
3970
+
3971
+ public boolean validate() {
3972
+ return this.name != null && this.name.length() > 0;
3973
+ }
3974
+ }
3975
+ `);
3976
+
3977
+ const index = new ProjectIndex(tmpDir);
3978
+ index.build('**/*.java', { quiet: true });
3979
+
3980
+ const ctx = index.context('User');
3981
+
3982
+ // Should identify as class
3983
+ assert.strictEqual(ctx.type, 'class', 'User should be identified as class');
3984
+ assert.ok(ctx.methods, 'Should have methods array');
3985
+ assert.strictEqual(ctx.methods.length, 3, 'User class should have 3 methods (constructor + 2 methods)');
3986
+
3987
+ const methodNames = ctx.methods.map(m => m.name);
3988
+ assert.ok(methodNames.includes('User'), 'Should include constructor User');
3989
+ assert.ok(methodNames.includes('greet'), 'Should include greet');
3990
+ assert.ok(methodNames.includes('validate'), 'Should include validate');
3991
+ } finally {
3992
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3993
+ }
3994
+ });
3995
+ });
3996
+
3997
+ describe('Regression: Rust impl methods in context', () => {
3998
+ it('should show impl methods for Rust structs via receiver', () => {
3999
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-rust-impl-'));
4000
+ try {
4001
+ fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), `[package]
4002
+ name = "test"
4003
+ version = "0.1.0"
4004
+ `);
4005
+ fs.writeFileSync(path.join(tmpDir, 'lib.rs'), `pub struct User {
4006
+ name: String,
4007
+ }
4008
+
4009
+ impl User {
4010
+ pub fn new(name: String) -> Self {
4011
+ User { name }
4012
+ }
4013
+
4014
+ pub fn greet(&self) -> String {
4015
+ format!("Hello {}", self.name)
4016
+ }
4017
+
4018
+ fn validate(&self) -> bool {
4019
+ !self.name.is_empty()
4020
+ }
4021
+ }
4022
+ `);
4023
+
4024
+ const index = new ProjectIndex(tmpDir);
4025
+ index.build('**/*.rs', { quiet: true });
4026
+
4027
+ const ctx = index.context('User');
4028
+
4029
+ // Should identify as struct
4030
+ assert.strictEqual(ctx.type, 'struct', 'User should be identified as struct');
4031
+ assert.ok(ctx.methods, 'Should have methods array');
4032
+ assert.strictEqual(ctx.methods.length, 3, 'User impl should have 3 methods');
4033
+
4034
+ const methodNames = ctx.methods.map(m => m.name);
4035
+ assert.ok(methodNames.includes('new'), 'Should include new');
4036
+ assert.ok(methodNames.includes('greet'), 'Should include greet');
4037
+ assert.ok(methodNames.includes('validate'), 'Should include validate');
4038
+
4039
+ // Methods should have receiver info pointing to User
4040
+ const greetMethod = ctx.methods.find(m => m.name === 'greet');
4041
+ assert.strictEqual(greetMethod.receiver, 'User', 'greet should have User as receiver');
4042
+ } finally {
4043
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4044
+ }
4045
+ });
4046
+ });
4047
+
4048
+ describe('Regression: JavaScript class methods in context', () => {
4049
+ it('should show methods for JS classes via className', () => {
4050
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-js-class-'));
4051
+ try {
4052
+ fs.writeFileSync(path.join(tmpDir, 'user.js'), `class User {
4053
+ constructor(name) {
4054
+ this.name = name;
4055
+ }
4056
+
4057
+ greet() {
4058
+ return 'Hello ' + this.name;
4059
+ }
4060
+
4061
+ static create(name) {
4062
+ return new User(name);
4063
+ }
4064
+ }
4065
+ `);
4066
+
4067
+ const index = new ProjectIndex(tmpDir);
4068
+ index.build('**/*.js', { quiet: true });
4069
+
4070
+ const ctx = index.context('User');
4071
+
4072
+ // Should identify as class
4073
+ assert.strictEqual(ctx.type, 'class', 'User should be identified as class');
4074
+ assert.ok(ctx.methods, 'Should have methods array');
4075
+ assert.strictEqual(ctx.methods.length, 3, 'User class should have 3 methods');
4076
+
4077
+ const methodNames = ctx.methods.map(m => m.name);
4078
+ assert.ok(methodNames.includes('constructor'), 'Should include constructor');
4079
+ assert.ok(methodNames.includes('greet'), 'Should include greet');
4080
+ assert.ok(methodNames.includes('create'), 'Should include create');
4081
+ } finally {
4082
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4083
+ }
4084
+ });
4085
+ });
4086
+
4087
+ describe('Regression: Java main() not flagged as deadcode', () => {
4088
+ it('should NOT report public static main as dead code in Java', () => {
4089
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-java-main-'));
4090
+ try {
4091
+ fs.writeFileSync(path.join(tmpDir, 'App.java'), `public class App {
4092
+ public static void main(String[] args) {
4093
+ System.out.println("Hello");
4094
+ helper();
4095
+ }
4096
+
4097
+ private static void helper() {
4098
+ System.out.println("Helper");
4099
+ }
4100
+
4101
+ private static void unusedMethod() {
4102
+ System.out.println("Unused");
4103
+ }
4104
+ }
4105
+ `);
4106
+
4107
+ const index = new ProjectIndex(tmpDir);
4108
+ index.build('**/*.java', { quiet: true });
4109
+
4110
+ const deadcode = index.deadcode();
4111
+ const deadNames = deadcode.map(d => d.name);
4112
+
4113
+ // main should NOT be flagged as dead code (entry point)
4114
+ assert.ok(!deadNames.includes('main'), 'main() should not be flagged as dead code');
4115
+
4116
+ // helper is called by main, so not dead
4117
+ assert.ok(!deadNames.includes('helper'), 'helper() is called by main, not dead');
4118
+
4119
+ // unusedMethod should be flagged as dead
4120
+ assert.ok(deadNames.includes('unusedMethod'), 'unusedMethod() should be flagged as dead code');
4121
+ } finally {
4122
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4123
+ }
4124
+ });
4125
+ });
4126
+
4127
+ describe('Regression: Python magic methods not flagged as deadcode', () => {
4128
+ it('should NOT report __init__, __call__, __enter__, __exit__ as dead code', () => {
4129
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-py-magic-'));
4130
+ try {
4131
+ fs.writeFileSync(path.join(tmpDir, 'context.py'), `class MyContext:
4132
+ def __init__(self):
4133
+ self.count = 0
4134
+
4135
+ def __enter__(self):
4136
+ self.count += 1
4137
+ return self
4138
+
4139
+ def __exit__(self, exc_type, exc_val, exc_tb):
4140
+ self.count -= 1
4141
+ return False
4142
+
4143
+ def __call__(self, x):
4144
+ return x * 2
4145
+
4146
+ def unused_method(self):
4147
+ pass
4148
+ `);
4149
+
4150
+ const index = new ProjectIndex(tmpDir);
4151
+ index.build('**/*.py', { quiet: true });
4152
+
4153
+ const deadcode = index.deadcode();
4154
+ const deadNames = deadcode.map(d => d.name);
4155
+
4156
+ // Magic methods should NOT be flagged as dead code
4157
+ assert.ok(!deadNames.includes('__init__'), '__init__ should not be flagged as dead code');
4158
+ assert.ok(!deadNames.includes('__enter__'), '__enter__ should not be flagged as dead code');
4159
+ assert.ok(!deadNames.includes('__exit__'), '__exit__ should not be flagged as dead code');
4160
+ assert.ok(!deadNames.includes('__call__'), '__call__ should not be flagged as dead code');
4161
+
4162
+ // unused_method should be flagged as dead
4163
+ assert.ok(deadNames.includes('unused_method'), 'unused_method() should be flagged as dead code');
4164
+ } finally {
4165
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4166
+ }
4167
+ });
4168
+ });
4169
+
4170
+ describe('Regression: Rust main and #[test] not flagged as deadcode', () => {
4171
+ it('should NOT report main() or #[test] functions as dead code in Rust', () => {
4172
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-rust-main-'));
4173
+ try {
4174
+ fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), `[package]
4175
+ name = "test"
4176
+ version = "0.1.0"
4177
+ `);
4178
+ fs.writeFileSync(path.join(tmpDir, 'main.rs'), `fn main() {
4179
+ helper();
4180
+ }
4181
+
4182
+ fn helper() {
4183
+ println!("Helper");
4184
+ }
4185
+
4186
+ fn unused_fn() {
4187
+ println!("Unused");
4188
+ }
4189
+
4190
+ #[test]
4191
+ fn test_something() {
4192
+ assert!(true);
4193
+ }
4194
+ `);
4195
+
4196
+ const index = new ProjectIndex(tmpDir);
4197
+ index.build('**/*.rs', { quiet: true });
4198
+
4199
+ const deadcode = index.deadcode();
4200
+ const deadNames = deadcode.map(d => d.name);
4201
+
4202
+ // main should NOT be flagged as dead code (entry point)
4203
+ assert.ok(!deadNames.includes('main'), 'main() should not be flagged as dead code');
4204
+
4205
+ // test_something should NOT be flagged (has #[test] attribute)
4206
+ assert.ok(!deadNames.includes('test_something'), '#[test] function should not be flagged as dead code');
4207
+
4208
+ // helper is called by main
4209
+ assert.ok(!deadNames.includes('helper'), 'helper() is called by main, not dead');
4210
+
4211
+ // unused_fn should be flagged as dead
4212
+ assert.ok(deadNames.includes('unused_fn'), 'unused_fn() should be flagged as dead code');
4213
+ } finally {
4214
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4215
+ }
4216
+ });
4217
+ });
4218
+
4219
+ describe('Regression: className field preserved in symbol index', () => {
4220
+ it('should store className for Python class methods', () => {
4221
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-classname-py-'));
4222
+ try {
4223
+ fs.writeFileSync(path.join(tmpDir, 'models.py'), `class User:
4224
+ def save(self):
4225
+ pass
4226
+
4227
+ class Product:
4228
+ def save(self):
4229
+ pass
4230
+ `);
4231
+
4232
+ const index = new ProjectIndex(tmpDir);
4233
+ index.build('**/*.py', { quiet: true });
4234
+
4235
+ // Both save methods should have className field
4236
+ const saveMethods = index.symbols.get('save');
4237
+ assert.ok(saveMethods, 'save methods should be indexed');
4238
+ assert.strictEqual(saveMethods.length, 2, 'Should have 2 save methods');
4239
+
4240
+ const classNames = saveMethods.map(m => m.className).sort();
4241
+ assert.deepStrictEqual(classNames, ['Product', 'User'], 'Should have User and Product as classNames');
4242
+ } finally {
4243
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4244
+ }
4245
+ });
4246
+
4247
+ it('should store className for Java class methods', () => {
4248
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-classname-java-'));
4249
+ try {
4250
+ fs.writeFileSync(path.join(tmpDir, 'Models.java'), `class User {
4251
+ public void save() {}
4252
+ }
4253
+
4254
+ class Product {
4255
+ public void save() {}
4256
+ }
4257
+ `);
4258
+
4259
+ const index = new ProjectIndex(tmpDir);
4260
+ index.build('**/*.java', { quiet: true });
4261
+
4262
+ // Both save methods should have className field
4263
+ const saveMethods = index.symbols.get('save');
4264
+ assert.ok(saveMethods, 'save methods should be indexed');
4265
+ assert.strictEqual(saveMethods.length, 2, 'Should have 2 save methods');
4266
+
4267
+ const classNames = saveMethods.map(m => m.className).sort();
4268
+ assert.deepStrictEqual(classNames, ['Product', 'User'], 'Should have User and Product as classNames');
4269
+ } finally {
4270
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4271
+ }
4272
+ });
4273
+
4274
+ it('should store className for JavaScript class methods', () => {
4275
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-classname-js-'));
4276
+ try {
4277
+ fs.writeFileSync(path.join(tmpDir, 'models.js'), `class User {
4278
+ save() {}
4279
+ }
4280
+
4281
+ class Product {
4282
+ save() {}
4283
+ }
4284
+ `);
4285
+
4286
+ const index = new ProjectIndex(tmpDir);
4287
+ index.build('**/*.js', { quiet: true });
4288
+
4289
+ // Both save methods should have className field
4290
+ const saveMethods = index.symbols.get('save');
4291
+ assert.ok(saveMethods, 'save methods should be indexed');
4292
+ assert.strictEqual(saveMethods.length, 2, 'Should have 2 save methods');
4293
+
4294
+ const classNames = saveMethods.map(m => m.className).sort();
4295
+ assert.deepStrictEqual(classNames, ['Product', 'User'], 'Should have User and Product as classNames');
4296
+ } finally {
4297
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4298
+ }
4299
+ });
4300
+ });
4301
+
3660
4302
  console.log('UCN v3 Test Suite');
3661
4303
  console.log('Run with: node --test test/parser.test.js');