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 +13 -6
- package/cli/index.js +66 -4
- package/core/output.js +39 -5
- package/core/project.js +142 -11
- package/languages/java.js +8 -0
- package/languages/javascript.js +3 -0
- package/languages/python.js +15 -0
- package/languages/rust.js +61 -2
- package/package.json +1 -1
- package/test/parser.test.js +643 -1
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
|
-
|
|
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
|
-
|
|
1569
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
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:
|
|
314
|
-
calleeCount:
|
|
315
|
-
callers:
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1611
|
-
|
|
1612
|
-
(
|
|
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:
|
|
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
|
|
3167
|
-
|
|
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
|
}
|
package/languages/javascript.js
CHANGED
|
@@ -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
|
}
|
package/languages/python.js
CHANGED
|
@@ -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
package/test/parser.test.js
CHANGED
|
@@ -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,
|
|
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');
|