ucn 3.2.0 → 3.4.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.
- package/README.md +6 -2
- package/cli/index.js +146 -44
- package/core/imports.js +162 -4
- package/core/output.js +129 -147
- package/core/project.js +412 -133
- package/languages/go.js +21 -10
- package/languages/java.js +29 -10
- package/languages/javascript.js +56 -37
- package/languages/python.js +39 -10
- package/languages/rust.js +36 -8
- package/package.json +1 -1
- package/test/parser.test.js +1217 -7
- package/test/reliability-test-prompt.md +58 -0
package/README.md
CHANGED
|
@@ -201,7 +201,7 @@ FIND CODE
|
|
|
201
201
|
═══════════════════════════════════════════════════════════════════════════════
|
|
202
202
|
find <name> Find symbol definitions (top 5 by usage count)
|
|
203
203
|
usages <name> All usages grouped: definitions, calls, imports, references
|
|
204
|
-
toc Table of contents (
|
|
204
|
+
toc Table of contents (compact; --detailed lists all symbols)
|
|
205
205
|
search <term> Text search (for simple patterns, consider grep instead)
|
|
206
206
|
tests <name> Find test files for a function
|
|
207
207
|
|
|
@@ -250,13 +250,17 @@ Common Flags:
|
|
|
250
250
|
--top=N / --all Limit or show all results
|
|
251
251
|
--include-tests Include test files
|
|
252
252
|
--include-methods Include method calls (obj.fn) in caller/callee analysis
|
|
253
|
+
--include-uncertain Include ambiguous/uncertain call matches
|
|
254
|
+
--include-exported Include exported symbols in deadcode
|
|
255
|
+
--detailed List all symbols in toc (compact counts by default)
|
|
253
256
|
--no-cache Disable caching
|
|
254
257
|
--clear-cache Clear cache before running
|
|
255
258
|
--no-follow-symlinks Don't follow symbolic links
|
|
256
259
|
-i, --interactive Keep index in memory for multiple queries
|
|
257
260
|
|
|
258
261
|
Quick Start:
|
|
259
|
-
ucn toc # See project structure
|
|
262
|
+
ucn toc # See project structure (compact)
|
|
263
|
+
ucn toc --detailed # List all functions/classes
|
|
260
264
|
ucn about handleRequest # Understand a function
|
|
261
265
|
ucn impact handleRequest # Before modifying
|
|
262
266
|
ucn fn handleRequest --file api # Extract specific function
|
package/cli/index.js
CHANGED
|
@@ -46,6 +46,10 @@ const flags = {
|
|
|
46
46
|
includeTests: args.includes('--include-tests'),
|
|
47
47
|
// Deadcode options
|
|
48
48
|
includeExported: args.includes('--include-exported'),
|
|
49
|
+
// Uncertain matches (off by default)
|
|
50
|
+
includeUncertain: args.includes('--include-uncertain'),
|
|
51
|
+
// Detailed listing (e.g. toc with all symbols)
|
|
52
|
+
detailed: args.includes('--detailed'),
|
|
49
53
|
// Output depth
|
|
50
54
|
depth: args.find(a => a.startsWith('--depth='))?.split('=')[1] || null,
|
|
51
55
|
// Inline expansion for callees
|
|
@@ -78,7 +82,7 @@ const knownFlags = new Set([
|
|
|
78
82
|
'--json', '--verbose', '--no-quiet', '--quiet',
|
|
79
83
|
'--code-only', '--with-types', '--top-level', '--exact',
|
|
80
84
|
'--no-cache', '--clear-cache', '--include-tests',
|
|
81
|
-
'--include-exported', '--expand', '--interactive', '-i', '--all', '--include-methods',
|
|
85
|
+
'--include-exported', '--expand', '--interactive', '-i', '--all', '--include-methods', '--include-uncertain', '--detailed',
|
|
82
86
|
'--file', '--context', '--exclude', '--not', '--in',
|
|
83
87
|
'--depth', '--add-param', '--remove-param', '--rename-to',
|
|
84
88
|
'--default', '--top', '--no-follow-symlinks'
|
|
@@ -652,7 +656,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
652
656
|
|
|
653
657
|
switch (command) {
|
|
654
658
|
case 'toc': {
|
|
655
|
-
const toc = index.getToc();
|
|
659
|
+
const toc = index.getToc({ detailed: flags.detailed, topLevel: flags.topLevel });
|
|
656
660
|
printOutput(toc, output.formatTocJson, printProjectToc);
|
|
657
661
|
break;
|
|
658
662
|
}
|
|
@@ -699,7 +703,15 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
699
703
|
|
|
700
704
|
case 'context': {
|
|
701
705
|
requireArg(arg, 'Usage: ucn . context <name>');
|
|
702
|
-
const ctx = index.context(arg, {
|
|
706
|
+
const ctx = index.context(arg, {
|
|
707
|
+
includeMethods: flags.includeMethods,
|
|
708
|
+
includeUncertain: flags.includeUncertain,
|
|
709
|
+
file: flags.file
|
|
710
|
+
});
|
|
711
|
+
if (!ctx) {
|
|
712
|
+
console.log(`Symbol "${arg}" not found.`);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
703
715
|
printOutput(ctx,
|
|
704
716
|
output.formatContextJson,
|
|
705
717
|
r => { printContext(r, { expand: flags.expand, root: index.root }); }
|
|
@@ -730,7 +742,11 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
730
742
|
|
|
731
743
|
case 'smart': {
|
|
732
744
|
requireArg(arg, 'Usage: ucn . smart <name>');
|
|
733
|
-
const smart = index.smart(arg, {
|
|
745
|
+
const smart = index.smart(arg, {
|
|
746
|
+
withTypes: flags.withTypes,
|
|
747
|
+
includeMethods: flags.includeMethods,
|
|
748
|
+
includeUncertain: flags.includeUncertain
|
|
749
|
+
});
|
|
734
750
|
if (smart) {
|
|
735
751
|
printOutput(smart, output.formatSmartJson, printSmart);
|
|
736
752
|
} else {
|
|
@@ -790,7 +806,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
790
806
|
|
|
791
807
|
case 'verify': {
|
|
792
808
|
requireArg(arg, 'Usage: ucn . verify <name>');
|
|
793
|
-
const verifyResult = index.verify(arg);
|
|
809
|
+
const verifyResult = index.verify(arg, { file: flags.file });
|
|
794
810
|
printOutput(verifyResult, r => JSON.stringify(r, null, 2), output.formatVerify);
|
|
795
811
|
break;
|
|
796
812
|
}
|
|
@@ -906,6 +922,9 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
906
922
|
const exported = item.isExported ? ' [exported]' : '';
|
|
907
923
|
console.log(` ${output.lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}`);
|
|
908
924
|
}
|
|
925
|
+
if (!flags.includeExported) {
|
|
926
|
+
console.log(`\nExported symbols excluded by default. Add --include-exported to include them.`);
|
|
927
|
+
}
|
|
909
928
|
}
|
|
910
929
|
);
|
|
911
930
|
}
|
|
@@ -977,26 +996,27 @@ function extractFunctionFromProject(index, name) {
|
|
|
977
996
|
return;
|
|
978
997
|
}
|
|
979
998
|
|
|
999
|
+
let match;
|
|
980
1000
|
if (matches.length > 1 && !flags.file) {
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
1001
|
+
// Auto-select best match using same scoring as resolveSymbol
|
|
1002
|
+
match = pickBestDefinition(matches);
|
|
1003
|
+
console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Use --file to disambiguate.`);
|
|
1004
|
+
} else {
|
|
1005
|
+
match = matches[0];
|
|
984
1006
|
}
|
|
985
1007
|
|
|
986
|
-
|
|
1008
|
+
// Extract code directly using symbol index location (works for class methods and overloads)
|
|
987
1009
|
const code = fs.readFileSync(match.file, 'utf-8');
|
|
988
|
-
const
|
|
989
|
-
const
|
|
1010
|
+
const lines = code.split('\n');
|
|
1011
|
+
const fnCode = lines.slice(match.startLine - 1, match.endLine).join('\n');
|
|
990
1012
|
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
console.log(fnCode);
|
|
999
|
-
}
|
|
1013
|
+
if (flags.json) {
|
|
1014
|
+
console.log(output.formatFunctionJson(match, fnCode));
|
|
1015
|
+
} else {
|
|
1016
|
+
console.log(`${match.relativePath}:${match.startLine}`);
|
|
1017
|
+
console.log(`${output.lineRange(match.startLine, match.endLine)} ${output.formatFunctionSignature(match)}`);
|
|
1018
|
+
console.log('─'.repeat(60));
|
|
1019
|
+
console.log(fnCode);
|
|
1000
1020
|
}
|
|
1001
1021
|
}
|
|
1002
1022
|
|
|
@@ -1010,13 +1030,15 @@ function extractClassFromProject(index, name) {
|
|
|
1010
1030
|
return;
|
|
1011
1031
|
}
|
|
1012
1032
|
|
|
1033
|
+
let match;
|
|
1013
1034
|
if (matches.length > 1 && !flags.file) {
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1016
|
-
|
|
1035
|
+
// Auto-select best match using same scoring as resolveSymbol
|
|
1036
|
+
match = pickBestDefinition(matches);
|
|
1037
|
+
console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Use --file to disambiguate.`);
|
|
1038
|
+
} else {
|
|
1039
|
+
match = matches[0];
|
|
1017
1040
|
}
|
|
1018
1041
|
|
|
1019
|
-
const match = matches[0];
|
|
1020
1042
|
const code = fs.readFileSync(match.file, 'utf-8');
|
|
1021
1043
|
const language = detectLanguage(match.file);
|
|
1022
1044
|
const { cls, code: clsCode } = extractClass(code, language, match.name);
|
|
@@ -1034,29 +1056,61 @@ function extractClassFromProject(index, name) {
|
|
|
1034
1056
|
}
|
|
1035
1057
|
|
|
1036
1058
|
function printProjectToc(toc) {
|
|
1037
|
-
|
|
1038
|
-
console.log(`
|
|
1039
|
-
console.log(
|
|
1059
|
+
const t = toc.totals;
|
|
1060
|
+
console.log(`PROJECT: ${t.files} files, ${t.lines} lines`);
|
|
1061
|
+
console.log(` ${t.functions} functions, ${t.classes} classes, ${t.state} state objects`);
|
|
1040
1062
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
console.log(`\n${file.file} (${file.lines} lines)`);
|
|
1063
|
+
// Show meta warnings only when there's something noteworthy
|
|
1064
|
+
const meta = toc.meta || {};
|
|
1065
|
+
const warnings = [];
|
|
1066
|
+
if (meta.dynamicImports) warnings.push(`${meta.dynamicImports} dynamic import(s)`);
|
|
1067
|
+
if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
|
|
1068
|
+
if (warnings.length) {
|
|
1069
|
+
const hint = meta.uncertain ? ' — use --include-uncertain to include all' : '';
|
|
1070
|
+
console.log(` Note: ${warnings.join(', ')}${hint}`);
|
|
1071
|
+
}
|
|
1051
1072
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1073
|
+
// Hints
|
|
1074
|
+
if (toc.summary) {
|
|
1075
|
+
if (toc.summary.topFunctionFiles?.length) {
|
|
1076
|
+
const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
|
|
1077
|
+
console.log(` Most functions: ${hint}`);
|
|
1078
|
+
}
|
|
1079
|
+
if (toc.summary.topLineFiles?.length) {
|
|
1080
|
+
const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
|
|
1081
|
+
console.log(` Largest files: ${hint}`);
|
|
1054
1082
|
}
|
|
1083
|
+
if (toc.summary.entryFiles?.length) {
|
|
1084
|
+
console.log(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1055
1087
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1088
|
+
console.log('═'.repeat(60));
|
|
1089
|
+
const hasDetail = toc.files.some(f => f.symbols);
|
|
1090
|
+
for (const file of toc.files) {
|
|
1091
|
+
const parts = [`${file.lines} lines`];
|
|
1092
|
+
if (file.functions) parts.push(`${file.functions} fn`);
|
|
1093
|
+
if (file.classes) parts.push(`${file.classes} cls`);
|
|
1094
|
+
if (file.state) parts.push(`${file.state} state`);
|
|
1095
|
+
|
|
1096
|
+
if (hasDetail) {
|
|
1097
|
+
console.log(`\n${file.file} (${parts.join(', ')})`);
|
|
1098
|
+
if (file.symbols) {
|
|
1099
|
+
for (const fn of file.symbols.functions) {
|
|
1100
|
+
console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
|
|
1101
|
+
}
|
|
1102
|
+
for (const cls of file.symbols.classes) {
|
|
1103
|
+
console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
} else {
|
|
1107
|
+
console.log(` ${file.file} — ${parts.join(', ')}`);
|
|
1058
1108
|
}
|
|
1059
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
if (!hasDetail) {
|
|
1112
|
+
console.log(`\nAdd --detailed to list all functions, or "ucn . about <name>" for full details on a symbol`);
|
|
1113
|
+
}
|
|
1060
1114
|
}
|
|
1061
1115
|
|
|
1062
1116
|
function printSymbols(symbols, query, options = {}) {
|
|
@@ -1506,7 +1560,7 @@ function printBestExample(index, name) {
|
|
|
1506
1560
|
|
|
1507
1561
|
function printContext(ctx, options = {}) {
|
|
1508
1562
|
// Handle struct/interface types differently
|
|
1509
|
-
if (ctx.type && ['struct', 'interface', 'type'].includes(ctx.type)) {
|
|
1563
|
+
if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
|
|
1510
1564
|
console.log(`Context for ${ctx.type} ${ctx.name}:`);
|
|
1511
1565
|
console.log('═'.repeat(60));
|
|
1512
1566
|
|
|
@@ -1568,6 +1622,17 @@ function printContext(ctx, options = {}) {
|
|
|
1568
1622
|
console.log(`Context for ${ctx.function}:`);
|
|
1569
1623
|
console.log('═'.repeat(60));
|
|
1570
1624
|
|
|
1625
|
+
// Show meta note when results may be incomplete
|
|
1626
|
+
if (ctx.meta) {
|
|
1627
|
+
const notes = [];
|
|
1628
|
+
if (ctx.meta.dynamicImports) notes.push(`${ctx.meta.dynamicImports} dynamic import(s)`);
|
|
1629
|
+
if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
|
|
1630
|
+
if (notes.length) {
|
|
1631
|
+
const hint = ctx.meta.uncertain ? ' — use --include-uncertain to include all' : '';
|
|
1632
|
+
console.log(` Note: ${notes.join(', ')}${hint}`);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1571
1636
|
// Display warnings if any
|
|
1572
1637
|
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
1573
1638
|
console.log('\n⚠️ WARNINGS:');
|
|
@@ -1717,6 +1782,18 @@ function printExpandedItem(item, root) {
|
|
|
1717
1782
|
function printSmart(smart) {
|
|
1718
1783
|
console.log(`${smart.target.name} (${smart.target.file}:${smart.target.startLine})`);
|
|
1719
1784
|
console.log('═'.repeat(60));
|
|
1785
|
+
|
|
1786
|
+
// Show meta note when results may be incomplete
|
|
1787
|
+
if (smart.meta) {
|
|
1788
|
+
const notes = [];
|
|
1789
|
+
if (smart.meta.dynamicImports) notes.push(`${smart.meta.dynamicImports} dynamic import(s)`);
|
|
1790
|
+
if (smart.meta.uncertain) notes.push(`${smart.meta.uncertain} uncertain call(s) skipped`);
|
|
1791
|
+
if (notes.length) {
|
|
1792
|
+
const hint = smart.meta.uncertain ? ' — use --include-uncertain to include all' : '';
|
|
1793
|
+
console.log(` Note: ${notes.join(', ')}${hint}`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1720
1797
|
console.log(smart.target.code);
|
|
1721
1798
|
|
|
1722
1799
|
if (smart.dependencies.length > 0) {
|
|
@@ -2076,6 +2153,30 @@ function printLines(lines, range) {
|
|
|
2076
2153
|
}
|
|
2077
2154
|
}
|
|
2078
2155
|
|
|
2156
|
+
/**
|
|
2157
|
+
* Pick the best definition from multiple matches using same scoring as resolveSymbol.
|
|
2158
|
+
* Prefers lib/src/core over test/examples/vendor directories.
|
|
2159
|
+
*/
|
|
2160
|
+
function pickBestDefinition(matches) {
|
|
2161
|
+
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
2162
|
+
const scored = matches.map(m => {
|
|
2163
|
+
let score = 0;
|
|
2164
|
+
const rp = m.relativePath || '';
|
|
2165
|
+
// Prefer class/struct/interface types (+1000) - same as resolveSymbol
|
|
2166
|
+
if (typeOrder.has(m.type)) score += 1000;
|
|
2167
|
+
if (isTestFile(rp, detectLanguage(m.file))) score -= 500;
|
|
2168
|
+
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
|
|
2169
|
+
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
|
|
2170
|
+
// Tiebreaker: prefer larger function bodies (more important/complex)
|
|
2171
|
+
if (m.startLine && m.endLine) {
|
|
2172
|
+
score += Math.min(m.endLine - m.startLine, 100);
|
|
2173
|
+
}
|
|
2174
|
+
return { match: m, score };
|
|
2175
|
+
});
|
|
2176
|
+
scored.sort((a, b) => b.score - a.score);
|
|
2177
|
+
return scored[0].match;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2079
2180
|
function suggestSimilar(query, names) {
|
|
2080
2181
|
const lower = query.toLowerCase();
|
|
2081
2182
|
const similar = names.filter(n => n.toLowerCase().includes(lower));
|
|
@@ -2109,6 +2210,7 @@ Usage:
|
|
|
2109
2210
|
ucn <file> [command] [args] Single file mode
|
|
2110
2211
|
ucn <dir> [command] [args] Project mode (specific directory)
|
|
2111
2212
|
ucn "pattern" [command] [args] Glob pattern mode
|
|
2213
|
+
(Default output is text; add --json for machine-readable JSON)
|
|
2112
2214
|
|
|
2113
2215
|
═══════════════════════════════════════════════════════════════════════════════
|
|
2114
2216
|
UNDERSTAND CODE (UCN's strength - semantic analysis)
|
|
@@ -2321,7 +2423,7 @@ function executeInteractiveCommand(index, command, arg) {
|
|
|
2321
2423
|
console.log('Usage: context <name>');
|
|
2322
2424
|
return;
|
|
2323
2425
|
}
|
|
2324
|
-
const ctx = index.context(arg);
|
|
2426
|
+
const ctx = index.context(arg, { includeUncertain: flags.includeUncertain });
|
|
2325
2427
|
printContext(ctx, { expand: flags.expand, root: index.root });
|
|
2326
2428
|
break;
|
|
2327
2429
|
}
|
|
@@ -2331,7 +2433,7 @@ function executeInteractiveCommand(index, command, arg) {
|
|
|
2331
2433
|
console.log('Usage: smart <name>');
|
|
2332
2434
|
return;
|
|
2333
2435
|
}
|
|
2334
|
-
const smart = index.smart(arg, {});
|
|
2436
|
+
const smart = index.smart(arg, { includeUncertain: flags.includeUncertain });
|
|
2335
2437
|
if (smart) {
|
|
2336
2438
|
printSmart(smart);
|
|
2337
2439
|
} else {
|
package/core/imports.js
CHANGED
|
@@ -26,14 +26,15 @@ function extractImports(content, language) {
|
|
|
26
26
|
const parser = getParser(normalizedLang);
|
|
27
27
|
if (parser) {
|
|
28
28
|
const imports = langModule.findImportsInCode(content, parser);
|
|
29
|
-
|
|
29
|
+
const dynamicCount = imports.filter(i => i.dynamic).length;
|
|
30
|
+
return { imports, dynamicCount };
|
|
30
31
|
}
|
|
31
32
|
} catch (e) {
|
|
32
33
|
// AST parsing failed
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
return { imports: [] };
|
|
37
|
+
return { imports: [], dynamicCount: 0 };
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -115,11 +116,46 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
115
116
|
if (resolved) return resolved;
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// Rust: crate::, super::, self:: paths and mod declarations
|
|
120
|
+
if (config.language === 'rust') {
|
|
121
|
+
const resolved = resolveRustImport(importPath, fromFile, config.root);
|
|
122
|
+
if (resolved) return resolved;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Python: non-relative package imports (e.g., "tools.analyzer" -> "tools/analyzer.py")
|
|
126
|
+
// Try resolving dotted module path from the project root
|
|
127
|
+
if (config.language === 'python' && config.root) {
|
|
128
|
+
const modulePath = importPath.replace(/\./g, '/');
|
|
129
|
+
const fullPath = path.join(config.root, modulePath);
|
|
130
|
+
const resolved = resolveFilePath(fullPath, getExtensions('python'));
|
|
131
|
+
if (resolved) return resolved;
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
return null; // External package
|
|
119
135
|
}
|
|
120
136
|
|
|
137
|
+
// Python relative imports: translate dot-prefix notation to file paths
|
|
138
|
+
// e.g., ".models" -> "./models", "..utils" -> "../utils", "." -> "."
|
|
139
|
+
let normalizedPath = importPath;
|
|
140
|
+
if (config.language === 'python') {
|
|
141
|
+
// Count leading dots and convert to filesystem relative path
|
|
142
|
+
const dotMatch = importPath.match(/^(\.+)(.*)/);
|
|
143
|
+
if (dotMatch) {
|
|
144
|
+
const dots = dotMatch[1];
|
|
145
|
+
const rest = dotMatch[2];
|
|
146
|
+
if (dots.length === 1) {
|
|
147
|
+
// ".models" -> "./models", "." -> "."
|
|
148
|
+
normalizedPath = rest ? './' + rest.replace(/\./g, '/') : '.';
|
|
149
|
+
} else {
|
|
150
|
+
// "..models" -> "../models", "...models" -> "../../models"
|
|
151
|
+
const upDirs = '../'.repeat(dots.length - 1);
|
|
152
|
+
normalizedPath = rest ? upDirs + rest.replace(/\./g, '/') : upDirs.slice(0, -1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
121
157
|
// Relative imports
|
|
122
|
-
const resolved = path.resolve(fromDir,
|
|
158
|
+
const resolved = path.resolve(fromDir, normalizedPath);
|
|
123
159
|
return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
|
|
124
160
|
}
|
|
125
161
|
|
|
@@ -200,6 +236,125 @@ function resolveGoImport(importPath, fromFile, projectRoot) {
|
|
|
200
236
|
return null;
|
|
201
237
|
}
|
|
202
238
|
|
|
239
|
+
// Cache for Rust crate roots (Cargo.toml locations)
|
|
240
|
+
const cargoCache = new Map();
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Find the nearest Cargo.toml and return the crate's source root
|
|
244
|
+
* @param {string} startDir - Directory to start searching from
|
|
245
|
+
* @returns {{root: string, srcDir: string}|null}
|
|
246
|
+
*/
|
|
247
|
+
function findCargoRoot(startDir) {
|
|
248
|
+
if (cargoCache.has(startDir)) {
|
|
249
|
+
return cargoCache.get(startDir);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let dir = startDir;
|
|
253
|
+
while (dir !== path.dirname(dir)) {
|
|
254
|
+
const cargoPath = path.join(dir, 'Cargo.toml');
|
|
255
|
+
if (fs.existsSync(cargoPath)) {
|
|
256
|
+
const srcDir = path.join(dir, 'src');
|
|
257
|
+
const result = fs.existsSync(srcDir) ? { root: dir, srcDir } : null;
|
|
258
|
+
cargoCache.set(startDir, result);
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
dir = path.dirname(dir);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
cargoCache.set(startDir, null);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Try to resolve a Rust module path to a file
|
|
270
|
+
* Checks both <path>.rs and <path>/mod.rs
|
|
271
|
+
* @param {string} dir - Base directory
|
|
272
|
+
* @param {string[]} segments - Path segments to resolve
|
|
273
|
+
* @returns {string|null}
|
|
274
|
+
*/
|
|
275
|
+
function resolveRustModulePath(dir, segments) {
|
|
276
|
+
// Try progressively shorter paths (items at the end may be types, not modules)
|
|
277
|
+
for (let len = segments.length; len >= 1; len--) {
|
|
278
|
+
const modPath = path.join(dir, ...segments.slice(0, len));
|
|
279
|
+
// Try <path>.rs
|
|
280
|
+
const rsFile = modPath + '.rs';
|
|
281
|
+
if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
|
|
282
|
+
return rsFile;
|
|
283
|
+
}
|
|
284
|
+
// Try <path>/mod.rs
|
|
285
|
+
const modFile = path.join(modPath, 'mod.rs');
|
|
286
|
+
if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
|
|
287
|
+
return modFile;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve Rust import paths to local files
|
|
295
|
+
* Handles: crate::, super::, self::, and mod declarations
|
|
296
|
+
* @param {string} importPath - Rust import path (e.g., "crate::display::Display" or "display")
|
|
297
|
+
* @param {string} fromFile - File containing the import
|
|
298
|
+
* @param {string} projectRoot - Project root directory
|
|
299
|
+
* @returns {string|null}
|
|
300
|
+
*/
|
|
301
|
+
function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
302
|
+
const fromDir = path.dirname(fromFile);
|
|
303
|
+
|
|
304
|
+
// crate:: paths - resolve from the crate's src/ directory
|
|
305
|
+
if (importPath.startsWith('crate::')) {
|
|
306
|
+
const cargo = findCargoRoot(fromDir);
|
|
307
|
+
if (!cargo) return null;
|
|
308
|
+
|
|
309
|
+
const rest = importPath.slice('crate::'.length);
|
|
310
|
+
const segments = rest.split('::');
|
|
311
|
+
return resolveRustModulePath(cargo.srcDir, segments);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// super:: paths - resolve relative to parent directory
|
|
315
|
+
if (importPath.startsWith('super::')) {
|
|
316
|
+
let dir = fromDir;
|
|
317
|
+
let rest = importPath;
|
|
318
|
+
while (rest.startsWith('super::')) {
|
|
319
|
+
// If current file is mod.rs, go up one more directory
|
|
320
|
+
const basename = path.basename(fromFile);
|
|
321
|
+
if (basename === 'mod.rs' && dir === fromDir) {
|
|
322
|
+
dir = path.dirname(dir);
|
|
323
|
+
}
|
|
324
|
+
dir = path.dirname(dir);
|
|
325
|
+
rest = rest.slice('super::'.length);
|
|
326
|
+
}
|
|
327
|
+
const segments = rest.split('::');
|
|
328
|
+
return resolveRustModulePath(dir, segments);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// self:: paths - resolve within current module directory
|
|
332
|
+
if (importPath.startsWith('self::')) {
|
|
333
|
+
const rest = importPath.slice('self::'.length);
|
|
334
|
+
const segments = rest.split('::');
|
|
335
|
+
// If current file is mod.rs, resolve relative to its directory
|
|
336
|
+
const basename = path.basename(fromFile);
|
|
337
|
+
const dir = basename === 'mod.rs' ? fromDir : path.dirname(fromDir);
|
|
338
|
+
return resolveRustModulePath(dir, segments);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Plain module name without :: (potential mod declaration)
|
|
342
|
+
// e.g., "display" from `mod display;` - resolve relative to declaring file
|
|
343
|
+
if (!importPath.includes('::')) {
|
|
344
|
+
// For mod declarations: <dir>/<name>.rs or <dir>/<name>/mod.rs
|
|
345
|
+
const rsFile = path.join(fromDir, importPath + '.rs');
|
|
346
|
+
if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
|
|
347
|
+
return rsFile;
|
|
348
|
+
}
|
|
349
|
+
const modFile = path.join(fromDir, importPath, 'mod.rs');
|
|
350
|
+
if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
|
|
351
|
+
return modFile;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
203
358
|
/**
|
|
204
359
|
* Try to resolve a path with various extensions
|
|
205
360
|
*/
|
|
@@ -215,11 +370,14 @@ function resolveFilePath(basePath, extensions) {
|
|
|
215
370
|
if (fs.existsSync(withExt)) return withExt;
|
|
216
371
|
}
|
|
217
372
|
|
|
218
|
-
// Try index files
|
|
373
|
+
// Try index files (index.js for JS/TS, __init__.py for Python)
|
|
219
374
|
for (const ext of extensions) {
|
|
220
375
|
const indexPath = path.join(basePath, 'index' + ext);
|
|
221
376
|
if (fs.existsSync(indexPath)) return indexPath;
|
|
222
377
|
}
|
|
378
|
+
// Python __init__.py
|
|
379
|
+
const initPath = path.join(basePath, '__init__.py');
|
|
380
|
+
if (fs.existsSync(initPath)) return initPath;
|
|
223
381
|
|
|
224
382
|
return null;
|
|
225
383
|
}
|