ucn 3.1.8 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/.claude/skills/ucn/SKILL.md +113 -48
- package/README.md +159 -29
- package/cli/index.js +147 -137
- package/core/discovery.js +1 -2
- package/core/imports.js +157 -331
- package/core/output.js +129 -147
- package/core/project.js +484 -220
- package/languages/go.js +21 -10
- package/languages/java.js +25 -9
- 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 +967 -7
- package/test/reliability-test-prompt.md +58 -0
package/core/imports.js
CHANGED
|
@@ -9,49 +9,6 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { getParser, getLanguageModule } = require('../languages');
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Import patterns by language
|
|
14
|
-
* @deprecated Use AST-based findImportsInCode() from language modules instead.
|
|
15
|
-
* Kept only as fallback for unsupported languages or when AST parsing fails.
|
|
16
|
-
*/
|
|
17
|
-
const IMPORT_PATTERNS = {
|
|
18
|
-
javascript: {
|
|
19
|
-
importDefault: /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
20
|
-
importNamed: /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
|
|
21
|
-
importNamespace: /import\s*\*\s*as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
22
|
-
require: /(?:const|let|var)\s+(?:\{[^}]+\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
23
|
-
exportNamed: /^\s*export\s+(?:async\s+)?(?:function|class|const|let|var|interface|type)\s+(\w+)/gm,
|
|
24
|
-
exportDefault: /^\s*export\s+default\s+(?:(?:async\s+)?(?:function|class)\s+)?(\w+)?/gm,
|
|
25
|
-
exportList: /^\s*export\s*\{([^}]+)\}/gm,
|
|
26
|
-
moduleExports: /^module\.exports\s*=\s*(?:\{([^}]+)\}|(\w+))/gm,
|
|
27
|
-
exportsNamed: /^exports\.(\w+)\s*=[^=]/gm,
|
|
28
|
-
importType: /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
|
|
29
|
-
importSideEffect: /import\s+['"]([^'"]+)['"]/g,
|
|
30
|
-
importDynamic: /(?:await\s+)?import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
31
|
-
reExportNamed: /^\s*export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/gm,
|
|
32
|
-
reExportAll: /^\s*export\s*\*\s*from\s*['"]([^'"]+)['"]/gm
|
|
33
|
-
},
|
|
34
|
-
python: {
|
|
35
|
-
importModule: /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm,
|
|
36
|
-
fromImport: /^from\s+([.\w]+)\s+import\s+(.+)/gm,
|
|
37
|
-
exportAll: /__all__\s*=\s*\[([^\]]+)\]/g
|
|
38
|
-
},
|
|
39
|
-
go: {
|
|
40
|
-
importSingle: /import\s+"([^"]+)"/g,
|
|
41
|
-
importBlock: /import\s*\(\s*([\s\S]*?)\s*\)/g,
|
|
42
|
-
exportedFunc: /^func\s+(?:\([^)]+\)\s+)?([A-Z]\w*)\s*\(/gm,
|
|
43
|
-
exportedType: /^type\s+([A-Z]\w*)\s+/gm
|
|
44
|
-
},
|
|
45
|
-
java: {
|
|
46
|
-
importStatement: /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g,
|
|
47
|
-
exportedClass: /public\s+(?:abstract\s+)?(?:final\s+)?(?:class|interface|enum)\s+(\w+)/g
|
|
48
|
-
},
|
|
49
|
-
rust: {
|
|
50
|
-
useStatement: /^use\s+([^;]+);/gm,
|
|
51
|
-
modDecl: /^\s*mod\s+(\w+)\s*;/gm
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
12
|
/**
|
|
56
13
|
* Extract imports from file content using AST
|
|
57
14
|
*
|
|
@@ -63,186 +20,21 @@ function extractImports(content, language) {
|
|
|
63
20
|
// Normalize language name for parser
|
|
64
21
|
const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
65
22
|
|
|
66
|
-
// Try AST-based extraction first
|
|
67
23
|
const langModule = getLanguageModule(normalizedLang);
|
|
68
24
|
if (langModule && typeof langModule.findImportsInCode === 'function') {
|
|
69
25
|
try {
|
|
70
26
|
const parser = getParser(normalizedLang);
|
|
71
27
|
if (parser) {
|
|
72
28
|
const imports = langModule.findImportsInCode(content, parser);
|
|
73
|
-
|
|
29
|
+
const dynamicCount = imports.filter(i => i.dynamic).length;
|
|
30
|
+
return { imports, dynamicCount };
|
|
74
31
|
}
|
|
75
32
|
} catch (e) {
|
|
76
|
-
//
|
|
33
|
+
// AST parsing failed
|
|
77
34
|
}
|
|
78
35
|
}
|
|
79
36
|
|
|
80
|
-
|
|
81
|
-
const imports = [];
|
|
82
|
-
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
83
|
-
extractJSImports(content, imports);
|
|
84
|
-
} else if (language === 'python') {
|
|
85
|
-
extractPythonImports(content, imports);
|
|
86
|
-
} else if (language === 'go') {
|
|
87
|
-
extractGoImports(content, imports);
|
|
88
|
-
} else if (language === 'java') {
|
|
89
|
-
extractJavaImports(content, imports);
|
|
90
|
-
} else if (language === 'rust') {
|
|
91
|
-
extractRustImports(content, imports);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { imports };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @deprecated Use AST-based findImportsInCode() from language modules.
|
|
99
|
-
*/
|
|
100
|
-
function extractJSImports(content, imports) {
|
|
101
|
-
const patterns = IMPORT_PATTERNS.javascript;
|
|
102
|
-
let match;
|
|
103
|
-
|
|
104
|
-
// Default imports
|
|
105
|
-
let regex = new RegExp(patterns.importDefault.source, 'g');
|
|
106
|
-
while ((match = regex.exec(content)) !== null) {
|
|
107
|
-
imports.push({ module: match[2], names: [match[1]], type: 'default' });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Named imports
|
|
111
|
-
regex = new RegExp(patterns.importNamed.source, 'g');
|
|
112
|
-
while ((match = regex.exec(content)) !== null) {
|
|
113
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
114
|
-
imports.push({ module: match[2], names, type: 'named' });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Namespace imports
|
|
118
|
-
regex = new RegExp(patterns.importNamespace.source, 'g');
|
|
119
|
-
while ((match = regex.exec(content)) !== null) {
|
|
120
|
-
imports.push({ module: match[2], names: [match[1]], type: 'namespace' });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Require
|
|
124
|
-
regex = new RegExp(patterns.require.source, 'g');
|
|
125
|
-
while ((match = regex.exec(content)) !== null) {
|
|
126
|
-
imports.push({ module: match[2], names: match[1] ? [match[1]] : [], type: 'require' });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Type imports
|
|
130
|
-
regex = new RegExp(patterns.importType.source, 'g');
|
|
131
|
-
while ((match = regex.exec(content)) !== null) {
|
|
132
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
133
|
-
imports.push({ module: match[2], names, type: 'type' });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Side-effect imports
|
|
137
|
-
regex = new RegExp(patterns.importSideEffect.source, 'g');
|
|
138
|
-
while ((match = regex.exec(content)) !== null) {
|
|
139
|
-
const module = match[1];
|
|
140
|
-
if (!imports.some(i => i.module === module)) {
|
|
141
|
-
imports.push({ module, names: [], type: 'side-effect' });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Dynamic imports
|
|
146
|
-
regex = new RegExp(patterns.importDynamic.source, 'g');
|
|
147
|
-
while ((match = regex.exec(content)) !== null) {
|
|
148
|
-
const module = match[1];
|
|
149
|
-
if (!imports.some(i => i.module === module)) {
|
|
150
|
-
imports.push({ module, names: [], type: 'dynamic' });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Re-exports
|
|
155
|
-
regex = new RegExp(patterns.reExportNamed.source, 'gm');
|
|
156
|
-
while ((match = regex.exec(content)) !== null) {
|
|
157
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
158
|
-
imports.push({ module: match[2], names, type: 're-export' });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
regex = new RegExp(patterns.reExportAll.source, 'gm');
|
|
162
|
-
while ((match = regex.exec(content)) !== null) {
|
|
163
|
-
imports.push({ module: match[1], names: ['*'], type: 're-export-all' });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
168
|
-
function extractPythonImports(content, imports) {
|
|
169
|
-
const patterns = IMPORT_PATTERNS.python;
|
|
170
|
-
let match;
|
|
171
|
-
|
|
172
|
-
let regex = new RegExp(patterns.importModule.source, 'gm');
|
|
173
|
-
while ((match = regex.exec(content)) !== null) {
|
|
174
|
-
const moduleName = match[1];
|
|
175
|
-
const alias = match[2] || moduleName.split('.').pop();
|
|
176
|
-
imports.push({ module: moduleName, names: [alias], type: 'module' });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
regex = new RegExp(patterns.fromImport.source, 'gm');
|
|
180
|
-
while ((match = regex.exec(content)) !== null) {
|
|
181
|
-
const moduleName = match[1];
|
|
182
|
-
const importList = match[2].trim();
|
|
183
|
-
|
|
184
|
-
if (importList === '*') {
|
|
185
|
-
imports.push({ module: moduleName, names: ['*'], type: 'star' });
|
|
186
|
-
} else {
|
|
187
|
-
const names = importList.split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n && n !== '(');
|
|
188
|
-
imports.push({ module: moduleName, names, type: 'from' });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
194
|
-
function extractGoImports(content, imports) {
|
|
195
|
-
const patterns = IMPORT_PATTERNS.go;
|
|
196
|
-
let match;
|
|
197
|
-
|
|
198
|
-
let regex = new RegExp(patterns.importSingle.source, 'g');
|
|
199
|
-
while ((match = regex.exec(content)) !== null) {
|
|
200
|
-
const pkg = match[1];
|
|
201
|
-
imports.push({ module: pkg, names: [path.basename(pkg)], type: 'single' });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
regex = new RegExp(patterns.importBlock.source, 'g');
|
|
205
|
-
while ((match = regex.exec(content)) !== null) {
|
|
206
|
-
const block = match[1];
|
|
207
|
-
const pkgMatches = block.matchAll(/"([^"]+)"/g);
|
|
208
|
-
for (const pkgMatch of pkgMatches) {
|
|
209
|
-
const pkg = pkgMatch[1];
|
|
210
|
-
imports.push({ module: pkg, names: [path.basename(pkg)], type: 'block' });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
216
|
-
function extractJavaImports(content, imports) {
|
|
217
|
-
const patterns = IMPORT_PATTERNS.java;
|
|
218
|
-
let match;
|
|
219
|
-
|
|
220
|
-
let regex = new RegExp(patterns.importStatement.source, 'g');
|
|
221
|
-
while ((match = regex.exec(content)) !== null) {
|
|
222
|
-
const fullImport = match[1];
|
|
223
|
-
const parts = fullImport.split('.');
|
|
224
|
-
const name = parts[parts.length - 1];
|
|
225
|
-
imports.push({ module: fullImport, names: name === '*' ? ['*'] : [name], type: 'import' });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
230
|
-
function extractRustImports(content, imports) {
|
|
231
|
-
const patterns = IMPORT_PATTERNS.rust;
|
|
232
|
-
let match;
|
|
233
|
-
|
|
234
|
-
let regex = new RegExp(patterns.useStatement.source, 'gm');
|
|
235
|
-
while ((match = regex.exec(content)) !== null) {
|
|
236
|
-
let raw = match[1].trim().split('{')[0].trim().split(' as ')[0].trim().replace(/::$/, '');
|
|
237
|
-
if (raw) {
|
|
238
|
-
imports.push({ module: raw, names: [], type: 'use' });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
regex = new RegExp(patterns.modDecl.source, 'gm');
|
|
243
|
-
while ((match = regex.exec(content)) !== null) {
|
|
244
|
-
imports.push({ module: `self::${match[1]}`, names: [match[1]], type: 'mod' });
|
|
245
|
-
}
|
|
37
|
+
return { imports: [], dynamicCount: 0 };
|
|
246
38
|
}
|
|
247
39
|
|
|
248
40
|
/**
|
|
@@ -252,7 +44,6 @@ function extractExports(content, language) {
|
|
|
252
44
|
// Normalize language name for parser
|
|
253
45
|
const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
254
46
|
|
|
255
|
-
// Try AST-based extraction first
|
|
256
47
|
const langModule = getLanguageModule(normalizedLang);
|
|
257
48
|
if (langModule && typeof langModule.findExportsInCode === 'function') {
|
|
258
49
|
try {
|
|
@@ -262,123 +53,11 @@ function extractExports(content, language) {
|
|
|
262
53
|
return { exports: foundExports };
|
|
263
54
|
}
|
|
264
55
|
} catch (e) {
|
|
265
|
-
//
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Fallback to regex-based extraction (deprecated)
|
|
270
|
-
const foundExports = [];
|
|
271
|
-
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
272
|
-
extractJSExports(content, foundExports);
|
|
273
|
-
} else if (language === 'python') {
|
|
274
|
-
extractPythonExports(content, foundExports);
|
|
275
|
-
} else if (language === 'go') {
|
|
276
|
-
extractGoExports(content, foundExports);
|
|
277
|
-
} else if (language === 'java') {
|
|
278
|
-
extractJavaExports(content, foundExports);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return { exports: foundExports };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
285
|
-
function extractJSExports(content, exports) {
|
|
286
|
-
const patterns = IMPORT_PATTERNS.javascript;
|
|
287
|
-
let match;
|
|
288
|
-
|
|
289
|
-
let regex = new RegExp(patterns.exportNamed.source, 'gm');
|
|
290
|
-
while ((match = regex.exec(content)) !== null) {
|
|
291
|
-
exports.push({ name: match[1], type: 'named' });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
regex = new RegExp(patterns.exportDefault.source, 'gm');
|
|
295
|
-
while ((match = regex.exec(content)) !== null) {
|
|
296
|
-
exports.push({ name: match[1] || 'default', type: 'default' });
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
regex = new RegExp(patterns.exportList.source, 'gm');
|
|
300
|
-
while ((match = regex.exec(content)) !== null) {
|
|
301
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
302
|
-
for (const name of names) {
|
|
303
|
-
exports.push({ name, type: 'list' });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
regex = new RegExp(patterns.exportsNamed.source, 'gm');
|
|
308
|
-
while ((match = regex.exec(content)) !== null) {
|
|
309
|
-
exports.push({ name: match[1], type: 'commonjs-named' });
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// module.exports = { a, b, c } or module.exports = identifier
|
|
313
|
-
regex = new RegExp(patterns.moduleExports.source, 'gm');
|
|
314
|
-
while ((match = regex.exec(content)) !== null) {
|
|
315
|
-
if (match[1]) {
|
|
316
|
-
// Object literal: module.exports = { a, b, c }
|
|
317
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s*:\s*/)[0].trim()).filter(n => n && !n.includes('('));
|
|
318
|
-
for (const name of names) {
|
|
319
|
-
exports.push({ name, type: 'commonjs-object' });
|
|
320
|
-
}
|
|
321
|
-
} else if (match[2]) {
|
|
322
|
-
// Single identifier: module.exports = SomeClass
|
|
323
|
-
exports.push({ name: match[2], type: 'commonjs-default' });
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
329
|
-
function extractPythonExports(content, exports) {
|
|
330
|
-
let match;
|
|
331
|
-
|
|
332
|
-
// Check for __all__
|
|
333
|
-
let regex = new RegExp(IMPORT_PATTERNS.python.exportAll.source, 'g');
|
|
334
|
-
while ((match = regex.exec(content)) !== null) {
|
|
335
|
-
const names = match[1].split(',').map(n => n.trim().replace(/['"]/g, '')).filter(n => n);
|
|
336
|
-
for (const name of names) {
|
|
337
|
-
exports.push({ name, type: 'explicit' });
|
|
56
|
+
// AST parsing failed
|
|
338
57
|
}
|
|
339
58
|
}
|
|
340
59
|
|
|
341
|
-
|
|
342
|
-
if (exports.length === 0) {
|
|
343
|
-
const funcRegex = /^def\s+([a-zA-Z]\w*)\s*\(/gm;
|
|
344
|
-
while ((match = funcRegex.exec(content)) !== null) {
|
|
345
|
-
if (!match[1].startsWith('_')) {
|
|
346
|
-
exports.push({ name: match[1], type: 'function' });
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const classRegex = /^class\s+([a-zA-Z]\w*)/gm;
|
|
351
|
-
while ((match = classRegex.exec(content)) !== null) {
|
|
352
|
-
if (!match[1].startsWith('_')) {
|
|
353
|
-
exports.push({ name: match[1], type: 'class' });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
360
|
-
function extractGoExports(content, exports) {
|
|
361
|
-
const patterns = IMPORT_PATTERNS.go;
|
|
362
|
-
let match;
|
|
363
|
-
|
|
364
|
-
let regex = new RegExp(patterns.exportedFunc.source, 'gm');
|
|
365
|
-
while ((match = regex.exec(content)) !== null) {
|
|
366
|
-
exports.push({ name: match[1], type: 'function' });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
regex = new RegExp(patterns.exportedType.source, 'gm');
|
|
370
|
-
while ((match = regex.exec(content)) !== null) {
|
|
371
|
-
exports.push({ name: match[1], type: 'type' });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
376
|
-
function extractJavaExports(content, exports) {
|
|
377
|
-
let match;
|
|
378
|
-
let regex = new RegExp(IMPORT_PATTERNS.java.exportedClass.source, 'g');
|
|
379
|
-
while ((match = regex.exec(content)) !== null) {
|
|
380
|
-
exports.push({ name: match[1], type: 'class' });
|
|
381
|
-
}
|
|
60
|
+
return { exports: [] };
|
|
382
61
|
}
|
|
383
62
|
|
|
384
63
|
// Cache for tsconfig lookups
|
|
@@ -437,11 +116,37 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
437
116
|
if (resolved) return resolved;
|
|
438
117
|
}
|
|
439
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
|
+
|
|
440
125
|
return null; // External package
|
|
441
126
|
}
|
|
442
127
|
|
|
128
|
+
// Python relative imports: translate dot-prefix notation to file paths
|
|
129
|
+
// e.g., ".models" -> "./models", "..utils" -> "../utils", "." -> "."
|
|
130
|
+
let normalizedPath = importPath;
|
|
131
|
+
if (config.language === 'python') {
|
|
132
|
+
// Count leading dots and convert to filesystem relative path
|
|
133
|
+
const dotMatch = importPath.match(/^(\.+)(.*)/);
|
|
134
|
+
if (dotMatch) {
|
|
135
|
+
const dots = dotMatch[1];
|
|
136
|
+
const rest = dotMatch[2];
|
|
137
|
+
if (dots.length === 1) {
|
|
138
|
+
// ".models" -> "./models", "." -> "."
|
|
139
|
+
normalizedPath = rest ? './' + rest.replace(/\./g, '/') : '.';
|
|
140
|
+
} else {
|
|
141
|
+
// "..models" -> "../models", "...models" -> "../../models"
|
|
142
|
+
const upDirs = '../'.repeat(dots.length - 1);
|
|
143
|
+
normalizedPath = rest ? upDirs + rest.replace(/\./g, '/') : upDirs.slice(0, -1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
443
148
|
// Relative imports
|
|
444
|
-
const resolved = path.resolve(fromDir,
|
|
149
|
+
const resolved = path.resolve(fromDir, normalizedPath);
|
|
445
150
|
return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
|
|
446
151
|
}
|
|
447
152
|
|
|
@@ -522,6 +227,125 @@ function resolveGoImport(importPath, fromFile, projectRoot) {
|
|
|
522
227
|
return null;
|
|
523
228
|
}
|
|
524
229
|
|
|
230
|
+
// Cache for Rust crate roots (Cargo.toml locations)
|
|
231
|
+
const cargoCache = new Map();
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Find the nearest Cargo.toml and return the crate's source root
|
|
235
|
+
* @param {string} startDir - Directory to start searching from
|
|
236
|
+
* @returns {{root: string, srcDir: string}|null}
|
|
237
|
+
*/
|
|
238
|
+
function findCargoRoot(startDir) {
|
|
239
|
+
if (cargoCache.has(startDir)) {
|
|
240
|
+
return cargoCache.get(startDir);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let dir = startDir;
|
|
244
|
+
while (dir !== path.dirname(dir)) {
|
|
245
|
+
const cargoPath = path.join(dir, 'Cargo.toml');
|
|
246
|
+
if (fs.existsSync(cargoPath)) {
|
|
247
|
+
const srcDir = path.join(dir, 'src');
|
|
248
|
+
const result = fs.existsSync(srcDir) ? { root: dir, srcDir } : null;
|
|
249
|
+
cargoCache.set(startDir, result);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
dir = path.dirname(dir);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
cargoCache.set(startDir, null);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Try to resolve a Rust module path to a file
|
|
261
|
+
* Checks both <path>.rs and <path>/mod.rs
|
|
262
|
+
* @param {string} dir - Base directory
|
|
263
|
+
* @param {string[]} segments - Path segments to resolve
|
|
264
|
+
* @returns {string|null}
|
|
265
|
+
*/
|
|
266
|
+
function resolveRustModulePath(dir, segments) {
|
|
267
|
+
// Try progressively shorter paths (items at the end may be types, not modules)
|
|
268
|
+
for (let len = segments.length; len >= 1; len--) {
|
|
269
|
+
const modPath = path.join(dir, ...segments.slice(0, len));
|
|
270
|
+
// Try <path>.rs
|
|
271
|
+
const rsFile = modPath + '.rs';
|
|
272
|
+
if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
|
|
273
|
+
return rsFile;
|
|
274
|
+
}
|
|
275
|
+
// Try <path>/mod.rs
|
|
276
|
+
const modFile = path.join(modPath, 'mod.rs');
|
|
277
|
+
if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
|
|
278
|
+
return modFile;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Resolve Rust import paths to local files
|
|
286
|
+
* Handles: crate::, super::, self::, and mod declarations
|
|
287
|
+
* @param {string} importPath - Rust import path (e.g., "crate::display::Display" or "display")
|
|
288
|
+
* @param {string} fromFile - File containing the import
|
|
289
|
+
* @param {string} projectRoot - Project root directory
|
|
290
|
+
* @returns {string|null}
|
|
291
|
+
*/
|
|
292
|
+
function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
293
|
+
const fromDir = path.dirname(fromFile);
|
|
294
|
+
|
|
295
|
+
// crate:: paths - resolve from the crate's src/ directory
|
|
296
|
+
if (importPath.startsWith('crate::')) {
|
|
297
|
+
const cargo = findCargoRoot(fromDir);
|
|
298
|
+
if (!cargo) return null;
|
|
299
|
+
|
|
300
|
+
const rest = importPath.slice('crate::'.length);
|
|
301
|
+
const segments = rest.split('::');
|
|
302
|
+
return resolveRustModulePath(cargo.srcDir, segments);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// super:: paths - resolve relative to parent directory
|
|
306
|
+
if (importPath.startsWith('super::')) {
|
|
307
|
+
let dir = fromDir;
|
|
308
|
+
let rest = importPath;
|
|
309
|
+
while (rest.startsWith('super::')) {
|
|
310
|
+
// If current file is mod.rs, go up one more directory
|
|
311
|
+
const basename = path.basename(fromFile);
|
|
312
|
+
if (basename === 'mod.rs' && dir === fromDir) {
|
|
313
|
+
dir = path.dirname(dir);
|
|
314
|
+
}
|
|
315
|
+
dir = path.dirname(dir);
|
|
316
|
+
rest = rest.slice('super::'.length);
|
|
317
|
+
}
|
|
318
|
+
const segments = rest.split('::');
|
|
319
|
+
return resolveRustModulePath(dir, segments);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// self:: paths - resolve within current module directory
|
|
323
|
+
if (importPath.startsWith('self::')) {
|
|
324
|
+
const rest = importPath.slice('self::'.length);
|
|
325
|
+
const segments = rest.split('::');
|
|
326
|
+
// If current file is mod.rs, resolve relative to its directory
|
|
327
|
+
const basename = path.basename(fromFile);
|
|
328
|
+
const dir = basename === 'mod.rs' ? fromDir : path.dirname(fromDir);
|
|
329
|
+
return resolveRustModulePath(dir, segments);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Plain module name without :: (potential mod declaration)
|
|
333
|
+
// e.g., "display" from `mod display;` - resolve relative to declaring file
|
|
334
|
+
if (!importPath.includes('::')) {
|
|
335
|
+
// For mod declarations: <dir>/<name>.rs or <dir>/<name>/mod.rs
|
|
336
|
+
const rsFile = path.join(fromDir, importPath + '.rs');
|
|
337
|
+
if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
|
|
338
|
+
return rsFile;
|
|
339
|
+
}
|
|
340
|
+
const modFile = path.join(fromDir, importPath, 'mod.rs');
|
|
341
|
+
if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
|
|
342
|
+
return modFile;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
525
349
|
/**
|
|
526
350
|
* Try to resolve a path with various extensions
|
|
527
351
|
*/
|
|
@@ -537,11 +361,14 @@ function resolveFilePath(basePath, extensions) {
|
|
|
537
361
|
if (fs.existsSync(withExt)) return withExt;
|
|
538
362
|
}
|
|
539
363
|
|
|
540
|
-
// Try index files
|
|
364
|
+
// Try index files (index.js for JS/TS, __init__.py for Python)
|
|
541
365
|
for (const ext of extensions) {
|
|
542
366
|
const indexPath = path.join(basePath, 'index' + ext);
|
|
543
367
|
if (fs.existsSync(indexPath)) return indexPath;
|
|
544
368
|
}
|
|
369
|
+
// Python __init__.py
|
|
370
|
+
const initPath = path.join(basePath, '__init__.py');
|
|
371
|
+
if (fs.existsSync(initPath)) return initPath;
|
|
545
372
|
|
|
546
373
|
return null;
|
|
547
374
|
}
|
|
@@ -636,6 +463,5 @@ function stripJsonComments(content) {
|
|
|
636
463
|
module.exports = {
|
|
637
464
|
extractImports,
|
|
638
465
|
extractExports,
|
|
639
|
-
resolveImport
|
|
640
|
-
IMPORT_PATTERNS
|
|
466
|
+
resolveImport
|
|
641
467
|
};
|