ucn 3.0.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 +77 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/cli/index.js +2437 -0
- package/core/discovery.js +513 -0
- package/core/imports.js +558 -0
- package/core/output.js +1274 -0
- package/core/parser.js +279 -0
- package/core/project.js +3261 -0
- package/index.js +52 -0
- package/languages/go.js +653 -0
- package/languages/index.js +267 -0
- package/languages/java.js +826 -0
- package/languages/javascript.js +1346 -0
- package/languages/python.js +667 -0
- package/languages/rust.js +950 -0
- package/languages/utils.js +457 -0
- package/package.json +42 -0
- package/test/fixtures/go/go.mod +3 -0
- package/test/fixtures/go/main.go +257 -0
- package/test/fixtures/go/service.go +187 -0
- package/test/fixtures/java/DataService.java +279 -0
- package/test/fixtures/java/Main.java +287 -0
- package/test/fixtures/java/Utils.java +199 -0
- package/test/fixtures/java/pom.xml +6 -0
- package/test/fixtures/javascript/main.js +109 -0
- package/test/fixtures/javascript/package.json +1 -0
- package/test/fixtures/javascript/service.js +88 -0
- package/test/fixtures/javascript/utils.js +67 -0
- package/test/fixtures/python/main.py +198 -0
- package/test/fixtures/python/pyproject.toml +3 -0
- package/test/fixtures/python/service.py +166 -0
- package/test/fixtures/python/utils.py +118 -0
- package/test/fixtures/rust/Cargo.toml +3 -0
- package/test/fixtures/rust/main.rs +253 -0
- package/test/fixtures/rust/service.rs +210 -0
- package/test/fixtures/rust/utils.rs +154 -0
- package/test/fixtures/typescript/main.ts +154 -0
- package/test/fixtures/typescript/package.json +1 -0
- package/test/fixtures/typescript/repository.ts +149 -0
- package/test/fixtures/typescript/types.ts +114 -0
- package/test/parser.test.js +3661 -0
- package/test/public-repos-test.js +477 -0
- package/test/systematic-test.js +619 -0
- package/ucn.js +8 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/discovery.js - File discovery and glob pattern expansion
|
|
3
|
+
*
|
|
4
|
+
* Pure Node.js implementation (no external dependencies)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// Always ignore - unambiguous, never user code
|
|
11
|
+
const DEFAULT_IGNORES = [
|
|
12
|
+
// Package managers (unambiguous names)
|
|
13
|
+
'node_modules',
|
|
14
|
+
'bower_components',
|
|
15
|
+
'.bundle',
|
|
16
|
+
|
|
17
|
+
// Version control
|
|
18
|
+
'.git',
|
|
19
|
+
'.svn',
|
|
20
|
+
'.hg',
|
|
21
|
+
|
|
22
|
+
// Python
|
|
23
|
+
'__pycache__',
|
|
24
|
+
'.venv',
|
|
25
|
+
'venv',
|
|
26
|
+
'.env',
|
|
27
|
+
'.tox',
|
|
28
|
+
'.eggs',
|
|
29
|
+
'*.egg-info',
|
|
30
|
+
|
|
31
|
+
// Build outputs
|
|
32
|
+
'dist',
|
|
33
|
+
'build',
|
|
34
|
+
'out',
|
|
35
|
+
'.next',
|
|
36
|
+
'.nuxt',
|
|
37
|
+
'.output',
|
|
38
|
+
'.vercel',
|
|
39
|
+
'.netlify',
|
|
40
|
+
|
|
41
|
+
// Test/coverage
|
|
42
|
+
'coverage',
|
|
43
|
+
'.nyc_output',
|
|
44
|
+
'.pytest_cache',
|
|
45
|
+
'.mypy_cache',
|
|
46
|
+
|
|
47
|
+
// Bundled/minified
|
|
48
|
+
'*.min.js',
|
|
49
|
+
'*.bundle.js',
|
|
50
|
+
'*.map',
|
|
51
|
+
|
|
52
|
+
// System
|
|
53
|
+
'.DS_Store',
|
|
54
|
+
'.ucn-cache'
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Conditional ignores - only ignore when marker file exists in same directory
|
|
58
|
+
// Maps directory name -> array of marker files that indicate it's a vendor dir
|
|
59
|
+
const CONDITIONAL_IGNORES = {
|
|
60
|
+
'vendor': ['go.mod', 'composer.json', 'Gemfile'], // Go, PHP, Ruby
|
|
61
|
+
'Pods': ['Podfile'], // iOS CocoaPods
|
|
62
|
+
'Carthage': ['Cartfile'], // iOS Carthage
|
|
63
|
+
'deps': ['mix.exs', 'rebar.config'], // Elixir, Erlang
|
|
64
|
+
'target': ['Cargo.toml', 'pom.xml', 'build.gradle'], // Rust, Maven, Gradle
|
|
65
|
+
'env': ['requirements.txt', 'pyproject.toml'], // Python virtualenv
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Project root markers
|
|
69
|
+
const PROJECT_MARKERS = [
|
|
70
|
+
'.git',
|
|
71
|
+
'.ucn.js',
|
|
72
|
+
'package.json',
|
|
73
|
+
'pyproject.toml',
|
|
74
|
+
'setup.py',
|
|
75
|
+
'go.mod',
|
|
76
|
+
'Cargo.toml',
|
|
77
|
+
'pom.xml',
|
|
78
|
+
'build.gradle',
|
|
79
|
+
'Makefile'
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// Test file patterns by language
|
|
83
|
+
const TEST_PATTERNS = {
|
|
84
|
+
javascript: [
|
|
85
|
+
/\.test\.(js|jsx|ts|tsx|mjs|cjs)$/,
|
|
86
|
+
/\.spec\.(js|jsx|ts|tsx|mjs|cjs)$/,
|
|
87
|
+
/__tests__\//,
|
|
88
|
+
/\.test$/
|
|
89
|
+
],
|
|
90
|
+
typescript: [
|
|
91
|
+
/\.test\.(ts|tsx)$/,
|
|
92
|
+
/\.spec\.(ts|tsx)$/,
|
|
93
|
+
/__tests__\//
|
|
94
|
+
],
|
|
95
|
+
python: [
|
|
96
|
+
/^test_.*\.py$/,
|
|
97
|
+
/.*_test\.py$/,
|
|
98
|
+
/\/tests?\//
|
|
99
|
+
],
|
|
100
|
+
go: [
|
|
101
|
+
/.*_test\.go$/
|
|
102
|
+
],
|
|
103
|
+
java: [
|
|
104
|
+
/.*Test\.java$/,
|
|
105
|
+
/.*TestCase\.java$/,
|
|
106
|
+
/.*Tests\.java$/
|
|
107
|
+
],
|
|
108
|
+
rust: [
|
|
109
|
+
/.*_test\.rs$/,
|
|
110
|
+
/\/tests\//,
|
|
111
|
+
/mod tests/
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function compareNames(a, b) {
|
|
116
|
+
const aLower = a.toLowerCase();
|
|
117
|
+
const bLower = b.toLowerCase();
|
|
118
|
+
if (aLower < bLower) return -1;
|
|
119
|
+
if (aLower > bLower) return 1;
|
|
120
|
+
if (a < b) return -1;
|
|
121
|
+
if (a > b) return 1;
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Expand a glob pattern to matching file paths
|
|
127
|
+
*
|
|
128
|
+
* @param {string} pattern - Glob pattern (e.g., "src/**\/*.py", "*.js")
|
|
129
|
+
* @param {object} options - Configuration options
|
|
130
|
+
* @param {string} options.root - Root directory (defaults to cwd)
|
|
131
|
+
* @param {string[]} options.ignores - Patterns to ignore
|
|
132
|
+
* @param {number} options.maxDepth - Maximum directory depth (default: 20)
|
|
133
|
+
* @param {number} options.maxFiles - Maximum files to return (default: 10000)
|
|
134
|
+
* @returns {string[]} - Array of absolute file paths
|
|
135
|
+
*/
|
|
136
|
+
function expandGlob(pattern, options = {}) {
|
|
137
|
+
const root = path.resolve(options.root || process.cwd());
|
|
138
|
+
const ignores = options.ignores || DEFAULT_IGNORES;
|
|
139
|
+
const maxDepth = options.maxDepth || 20;
|
|
140
|
+
const maxFiles = options.maxFiles || 10000;
|
|
141
|
+
const followSymlinks = options.followSymlinks !== false; // default true
|
|
142
|
+
|
|
143
|
+
// Handle home directory expansion
|
|
144
|
+
if (pattern.startsWith('~/')) {
|
|
145
|
+
pattern = pattern.replace('~', require('os').homedir());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse the pattern
|
|
149
|
+
const { baseDir, filePattern, recursive } = parseGlobPattern(pattern, root);
|
|
150
|
+
|
|
151
|
+
// Collect matching files
|
|
152
|
+
const files = [];
|
|
153
|
+
walkDir(baseDir, {
|
|
154
|
+
filePattern,
|
|
155
|
+
recursive,
|
|
156
|
+
ignores,
|
|
157
|
+
maxDepth,
|
|
158
|
+
followSymlinks,
|
|
159
|
+
onFile: (filePath) => {
|
|
160
|
+
if (files.length < maxFiles) {
|
|
161
|
+
files.push(filePath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return files.sort(compareNames);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse a glob pattern into components
|
|
171
|
+
*/
|
|
172
|
+
function parseGlobPattern(pattern, root) {
|
|
173
|
+
const recursive = pattern.includes('**');
|
|
174
|
+
const parts = pattern.split(/[/\\]/);
|
|
175
|
+
|
|
176
|
+
let dirParts = [];
|
|
177
|
+
let wildcardStart = -1;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < parts.length; i++) {
|
|
180
|
+
if (parts[i].includes('*') || parts[i].includes('?')) {
|
|
181
|
+
wildcardStart = i;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
dirParts.push(parts[i]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let baseDir;
|
|
188
|
+
if (dirParts.length === 0) {
|
|
189
|
+
baseDir = root;
|
|
190
|
+
} else if (path.isAbsolute(dirParts.join('/'))) {
|
|
191
|
+
baseDir = dirParts.join('/');
|
|
192
|
+
} else {
|
|
193
|
+
baseDir = path.join(root, ...dirParts);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let filePatternStr = wildcardStart >= 0
|
|
197
|
+
? parts.slice(wildcardStart).join('/')
|
|
198
|
+
: '*';
|
|
199
|
+
|
|
200
|
+
filePatternStr = filePatternStr.replace(/^\*\*[/\\]?/, '');
|
|
201
|
+
const filePattern = globToRegex(filePatternStr || '*');
|
|
202
|
+
|
|
203
|
+
return { baseDir, filePattern, recursive };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convert a glob pattern to a regular expression
|
|
208
|
+
*/
|
|
209
|
+
function globToRegex(glob) {
|
|
210
|
+
let regex = glob.replace(/[.+^$[\]\\]/g, '\\$&');
|
|
211
|
+
|
|
212
|
+
// Handle brace expansion: {js,ts} -> (js|ts)
|
|
213
|
+
regex = regex.replace(/\{([^}]+)\}/g, (_, group) => {
|
|
214
|
+
const alternatives = group.split(',').map(s => s.trim());
|
|
215
|
+
return '(' + alternatives.join('|') + ')';
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
regex = regex.replace(/\*\*/g, '.*');
|
|
219
|
+
regex = regex.replace(/\*/g, '[^/]*');
|
|
220
|
+
regex = regex.replace(/\?/g, '.');
|
|
221
|
+
|
|
222
|
+
return new RegExp('^' + regex + '$');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Walk a directory tree, calling onFile for each matching file
|
|
227
|
+
*/
|
|
228
|
+
function walkDir(dir, options, depth = 0, visited = new Set()) {
|
|
229
|
+
if (depth > options.maxDepth) return;
|
|
230
|
+
if (!fs.existsSync(dir)) return;
|
|
231
|
+
|
|
232
|
+
// Track visited directories to avoid circular symlinks
|
|
233
|
+
let realDir;
|
|
234
|
+
try {
|
|
235
|
+
realDir = fs.realpathSync(dir);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return; // broken symlink
|
|
238
|
+
}
|
|
239
|
+
if (visited.has(realDir)) return;
|
|
240
|
+
visited.add(realDir);
|
|
241
|
+
|
|
242
|
+
let entries;
|
|
243
|
+
try {
|
|
244
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
245
|
+
} catch (e) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
entries.sort((a, b) => compareNames(a.name, b.name));
|
|
250
|
+
|
|
251
|
+
const followSymlinks = options.followSymlinks !== false; // default true
|
|
252
|
+
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const fullPath = path.join(dir, entry.name);
|
|
255
|
+
|
|
256
|
+
if (shouldIgnore(entry.name, options.ignores, dir)) continue;
|
|
257
|
+
|
|
258
|
+
let isDir = entry.isDirectory();
|
|
259
|
+
let isFile = entry.isFile();
|
|
260
|
+
|
|
261
|
+
// Follow symlinks if enabled
|
|
262
|
+
if (followSymlinks && entry.isSymbolicLink()) {
|
|
263
|
+
try {
|
|
264
|
+
const stat = fs.statSync(fullPath);
|
|
265
|
+
isDir = stat.isDirectory();
|
|
266
|
+
isFile = stat.isFile();
|
|
267
|
+
} catch (e) {
|
|
268
|
+
continue; // broken symlink
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (isDir) {
|
|
273
|
+
if (options.recursive) {
|
|
274
|
+
walkDir(fullPath, options, depth + 1, visited);
|
|
275
|
+
}
|
|
276
|
+
} else if (isFile) {
|
|
277
|
+
if (options.filePattern.test(entry.name)) {
|
|
278
|
+
options.onFile(fullPath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if a file/directory name should be ignored
|
|
286
|
+
* @param {string} name - File/directory name
|
|
287
|
+
* @param {string[]} ignores - Patterns to always ignore
|
|
288
|
+
* @param {string} [parentDir] - Parent directory path (for conditional checks)
|
|
289
|
+
*/
|
|
290
|
+
function shouldIgnore(name, ignores, parentDir) {
|
|
291
|
+
// Check unconditional ignores
|
|
292
|
+
for (const pattern of ignores) {
|
|
293
|
+
if (pattern.includes('*')) {
|
|
294
|
+
const regex = globToRegex(pattern);
|
|
295
|
+
if (regex.test(name)) return true;
|
|
296
|
+
} else if (name === pattern) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check conditional ignores (only if parentDir provided)
|
|
302
|
+
if (parentDir && CONDITIONAL_IGNORES[name]) {
|
|
303
|
+
const markers = CONDITIONAL_IGNORES[name];
|
|
304
|
+
for (const marker of markers) {
|
|
305
|
+
if (fs.existsSync(path.join(parentDir, marker))) {
|
|
306
|
+
return true; // Marker found, this is a real vendor dir
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Find the project root directory by looking for marker files
|
|
316
|
+
*/
|
|
317
|
+
function findProjectRoot(startDir) {
|
|
318
|
+
let dir = path.resolve(startDir);
|
|
319
|
+
const root = path.parse(dir).root;
|
|
320
|
+
|
|
321
|
+
while (dir !== root) {
|
|
322
|
+
for (const marker of PROJECT_MARKERS) {
|
|
323
|
+
if (fs.existsSync(path.join(dir, marker))) {
|
|
324
|
+
return dir;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
dir = path.dirname(dir);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return path.resolve(startDir);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Auto-detect the glob pattern for a project based on its type
|
|
335
|
+
*/
|
|
336
|
+
function detectProjectPattern(projectRoot) {
|
|
337
|
+
const extensions = [];
|
|
338
|
+
|
|
339
|
+
if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
|
|
340
|
+
extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (fs.existsSync(path.join(projectRoot, 'pyproject.toml')) ||
|
|
344
|
+
fs.existsSync(path.join(projectRoot, 'setup.py')) ||
|
|
345
|
+
fs.existsSync(path.join(projectRoot, 'requirements.txt'))) {
|
|
346
|
+
extensions.push('py');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
|
|
350
|
+
extensions.push('go');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (fs.existsSync(path.join(projectRoot, 'Cargo.toml'))) {
|
|
354
|
+
extensions.push('rs');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (fs.existsSync(path.join(projectRoot, 'pom.xml')) ||
|
|
358
|
+
fs.existsSync(path.join(projectRoot, 'build.gradle'))) {
|
|
359
|
+
extensions.push('java', 'kt');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (extensions.length > 0) {
|
|
363
|
+
const unique = [...new Set(extensions)];
|
|
364
|
+
return `**/*.{${unique.join(',')}}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return '**/*.{js,jsx,ts,tsx,py,go,java,rs,rb,php,c,cpp,h,hpp}';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get file statistics for a set of files
|
|
372
|
+
*/
|
|
373
|
+
function getFileStats(files) {
|
|
374
|
+
const stats = {
|
|
375
|
+
totalFiles: files.length,
|
|
376
|
+
totalLines: 0,
|
|
377
|
+
byExtension: {}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
for (const file of files) {
|
|
381
|
+
const ext = path.extname(file).toLowerCase() || '(none)';
|
|
382
|
+
|
|
383
|
+
if (!stats.byExtension[ext]) {
|
|
384
|
+
stats.byExtension[ext] = { count: 0, lines: 0 };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
389
|
+
const lines = content.split('\n').length;
|
|
390
|
+
stats.totalLines += lines;
|
|
391
|
+
stats.byExtension[ext].count++;
|
|
392
|
+
stats.byExtension[ext].lines += lines;
|
|
393
|
+
} catch (e) {
|
|
394
|
+
// Skip files that can't be read
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return stats;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Check if a file is a test file based on its path and language
|
|
403
|
+
*/
|
|
404
|
+
function isTestFile(filePath, language) {
|
|
405
|
+
const patterns = TEST_PATTERNS[language] || TEST_PATTERNS.javascript;
|
|
406
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
407
|
+
const basename = path.basename(filePath);
|
|
408
|
+
|
|
409
|
+
for (const pattern of patterns) {
|
|
410
|
+
if (pattern.test(normalizedPath) || pattern.test(basename)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Find the test file for a given source file
|
|
419
|
+
*/
|
|
420
|
+
function findTestFileFor(sourceFile, language) {
|
|
421
|
+
const dir = path.dirname(sourceFile);
|
|
422
|
+
const ext = path.extname(sourceFile);
|
|
423
|
+
const base = path.basename(sourceFile, ext);
|
|
424
|
+
|
|
425
|
+
const candidates = [];
|
|
426
|
+
|
|
427
|
+
switch (language) {
|
|
428
|
+
case 'javascript':
|
|
429
|
+
case 'typescript':
|
|
430
|
+
case 'tsx':
|
|
431
|
+
candidates.push(
|
|
432
|
+
`${base}.test${ext}`,
|
|
433
|
+
`${base}.spec${ext}`,
|
|
434
|
+
`${base}.test.ts`,
|
|
435
|
+
`${base}.test.js`,
|
|
436
|
+
`${base}.spec.ts`,
|
|
437
|
+
`${base}.spec.js`
|
|
438
|
+
);
|
|
439
|
+
break;
|
|
440
|
+
case 'python':
|
|
441
|
+
candidates.push(
|
|
442
|
+
`test_${base}.py`,
|
|
443
|
+
`${base}_test.py`
|
|
444
|
+
);
|
|
445
|
+
break;
|
|
446
|
+
case 'go':
|
|
447
|
+
candidates.push(`${base}_test.go`);
|
|
448
|
+
break;
|
|
449
|
+
case 'java':
|
|
450
|
+
candidates.push(
|
|
451
|
+
`${base}Test.java`,
|
|
452
|
+
`${base}Tests.java`,
|
|
453
|
+
`${base}TestCase.java`
|
|
454
|
+
);
|
|
455
|
+
break;
|
|
456
|
+
case 'rust':
|
|
457
|
+
candidates.push(`${base}_test.rs`);
|
|
458
|
+
break;
|
|
459
|
+
default:
|
|
460
|
+
candidates.push(
|
|
461
|
+
`${base}.test${ext}`,
|
|
462
|
+
`${base}.spec${ext}`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Check in same directory
|
|
467
|
+
for (const candidate of candidates) {
|
|
468
|
+
const testPath = path.join(dir, candidate);
|
|
469
|
+
if (fs.existsSync(testPath)) {
|
|
470
|
+
return testPath;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check in __tests__ subdirectory
|
|
475
|
+
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
476
|
+
const testsDir = path.join(dir, '__tests__');
|
|
477
|
+
for (const candidate of candidates) {
|
|
478
|
+
const testPath = path.join(testsDir, candidate);
|
|
479
|
+
if (fs.existsSync(testPath)) {
|
|
480
|
+
return testPath;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Check in tests subdirectory
|
|
486
|
+
if (language === 'python' || language === 'rust') {
|
|
487
|
+
const testsDir = path.join(dir, 'tests');
|
|
488
|
+
for (const candidate of candidates) {
|
|
489
|
+
const testPath = path.join(testsDir, candidate);
|
|
490
|
+
if (fs.existsSync(testPath)) {
|
|
491
|
+
return testPath;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
module.exports = {
|
|
500
|
+
expandGlob,
|
|
501
|
+
parseGlobPattern,
|
|
502
|
+
globToRegex,
|
|
503
|
+
walkDir,
|
|
504
|
+
shouldIgnore,
|
|
505
|
+
findProjectRoot,
|
|
506
|
+
detectProjectPattern,
|
|
507
|
+
getFileStats,
|
|
508
|
+
isTestFile,
|
|
509
|
+
findTestFileFor,
|
|
510
|
+
DEFAULT_IGNORES,
|
|
511
|
+
PROJECT_MARKERS,
|
|
512
|
+
TEST_PATTERNS
|
|
513
|
+
};
|