ucn 3.1.4 → 3.1.6
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/cli/index.js +36 -4
- package/core/discovery.js +37 -17
- package/languages/javascript.js +48 -0
- package/package.json +1 -1
- package/test/parser.test.js +32 -0
- package/test/public-repos-test.js +0 -477
package/cli/index.js
CHANGED
|
@@ -960,7 +960,8 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
960
960
|
|
|
961
961
|
case 'graph': {
|
|
962
962
|
requireArg(arg, 'Usage: ucn . graph <file>');
|
|
963
|
-
const
|
|
963
|
+
const graphDepth = flags.depth ?? 2; // Default to 2 for cleaner output
|
|
964
|
+
const graphResult = index.graph(arg, { direction: 'both', maxDepth: graphDepth });
|
|
964
965
|
if (graphResult.nodes.length === 0) {
|
|
965
966
|
console.log(`File not found: ${arg}`);
|
|
966
967
|
} else {
|
|
@@ -970,7 +971,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
970
971
|
nodes: r.nodes.map(n => ({ file: n.relativePath, depth: n.depth })),
|
|
971
972
|
edges: r.edges.map(e => ({ from: path.relative(index.root, e.from), to: path.relative(index.root, e.to) }))
|
|
972
973
|
}, null, 2),
|
|
973
|
-
r => { printGraph(r, index.root); }
|
|
974
|
+
r => { printGraph(r, index.root, graphDepth); }
|
|
974
975
|
);
|
|
975
976
|
}
|
|
976
977
|
break;
|
|
@@ -1801,12 +1802,15 @@ function printStats(stats) {
|
|
|
1801
1802
|
}
|
|
1802
1803
|
}
|
|
1803
1804
|
|
|
1804
|
-
function printGraph(graph, root) {
|
|
1805
|
+
function printGraph(graph, root, maxDepth = 2) {
|
|
1805
1806
|
const rootRelPath = path.relative(root, graph.root);
|
|
1806
1807
|
console.log(`Dependency graph for ${rootRelPath}`);
|
|
1807
1808
|
console.log('═'.repeat(60));
|
|
1808
1809
|
|
|
1809
1810
|
const printed = new Set();
|
|
1811
|
+
const maxChildren = 8; // Limit children per node
|
|
1812
|
+
let truncatedNodes = 0;
|
|
1813
|
+
let depthLimited = false;
|
|
1810
1814
|
|
|
1811
1815
|
function printNode(file, indent = 0) {
|
|
1812
1816
|
const fileEntry = graph.nodes.find(n => n.file === file);
|
|
@@ -1819,15 +1823,43 @@ function printGraph(graph, root) {
|
|
|
1819
1823
|
}
|
|
1820
1824
|
printed.add(file);
|
|
1821
1825
|
|
|
1826
|
+
// Depth limiting
|
|
1827
|
+
if (indent > maxDepth) {
|
|
1828
|
+
depthLimited = true;
|
|
1829
|
+
console.log(`${prefix}${relPath} ...`);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1822
1833
|
console.log(`${prefix}${relPath}`);
|
|
1823
1834
|
|
|
1824
1835
|
const edges = graph.edges.filter(e => e.from === file);
|
|
1825
|
-
|
|
1836
|
+
|
|
1837
|
+
// Limit children
|
|
1838
|
+
const displayEdges = edges.slice(0, maxChildren);
|
|
1839
|
+
const hiddenCount = edges.length - displayEdges.length;
|
|
1840
|
+
|
|
1841
|
+
for (const edge of displayEdges) {
|
|
1826
1842
|
printNode(edge.to, indent + 1);
|
|
1827
1843
|
}
|
|
1844
|
+
|
|
1845
|
+
if (hiddenCount > 0) {
|
|
1846
|
+
truncatedNodes += hiddenCount;
|
|
1847
|
+
console.log(`${' '.repeat(indent)}└── ... and ${hiddenCount} more`);
|
|
1848
|
+
}
|
|
1828
1849
|
}
|
|
1829
1850
|
|
|
1830
1851
|
printNode(graph.root);
|
|
1852
|
+
|
|
1853
|
+
// Print helpful note about expanding
|
|
1854
|
+
if (depthLimited || truncatedNodes > 0) {
|
|
1855
|
+
console.log('\n' + '─'.repeat(60));
|
|
1856
|
+
if (depthLimited) {
|
|
1857
|
+
console.log(`Depth limited to ${maxDepth}. Use --depth=N for deeper graph.`);
|
|
1858
|
+
}
|
|
1859
|
+
if (truncatedNodes > 0) {
|
|
1860
|
+
console.log(`${truncatedNodes} nodes hidden. Graph has ${graph.nodes.length} total files.`);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1831
1863
|
}
|
|
1832
1864
|
|
|
1833
1865
|
function printSearchResults(results, term) {
|
package/core/discovery.js
CHANGED
|
@@ -332,31 +332,51 @@ function findProjectRoot(startDir) {
|
|
|
332
332
|
|
|
333
333
|
/**
|
|
334
334
|
* Auto-detect the glob pattern for a project based on its type
|
|
335
|
+
* Checks both project root and immediate subdirectories for config files
|
|
335
336
|
*/
|
|
336
337
|
function detectProjectPattern(projectRoot) {
|
|
337
338
|
const extensions = [];
|
|
338
339
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
340
|
+
// Helper to check for config files in a directory
|
|
341
|
+
const checkDir = (dir) => {
|
|
342
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
343
|
+
extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs');
|
|
344
|
+
}
|
|
342
345
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
346
|
+
if (fs.existsSync(path.join(dir, 'pyproject.toml')) ||
|
|
347
|
+
fs.existsSync(path.join(dir, 'setup.py')) ||
|
|
348
|
+
fs.existsSync(path.join(dir, 'requirements.txt'))) {
|
|
349
|
+
extensions.push('py');
|
|
350
|
+
}
|
|
348
351
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
+
if (fs.existsSync(path.join(dir, 'go.mod'))) {
|
|
353
|
+
extensions.push('go');
|
|
354
|
+
}
|
|
352
355
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
+
if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
|
|
357
|
+
extensions.push('rs');
|
|
358
|
+
}
|
|
356
359
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
+
if (fs.existsSync(path.join(dir, 'pom.xml')) ||
|
|
361
|
+
fs.existsSync(path.join(dir, 'build.gradle'))) {
|
|
362
|
+
extensions.push('java', 'kt');
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Check project root
|
|
367
|
+
checkDir(projectRoot);
|
|
368
|
+
|
|
369
|
+
// Also check immediate subdirectories for multi-language projects (e.g., web/, frontend/, server/)
|
|
370
|
+
try {
|
|
371
|
+
const entries = fs.readdirSync(projectRoot, { withFileTypes: true });
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') &&
|
|
374
|
+
!EXCLUDED_DIRS.has(entry.name)) {
|
|
375
|
+
checkDir(path.join(projectRoot, entry.name));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {
|
|
379
|
+
// Ignore errors reading directory
|
|
360
380
|
}
|
|
361
381
|
|
|
362
382
|
if (extensions.length > 0) {
|
package/languages/javascript.js
CHANGED
|
@@ -812,6 +812,50 @@ function findCallsInCode(code, parser) {
|
|
|
812
812
|
return true;
|
|
813
813
|
}
|
|
814
814
|
|
|
815
|
+
// Handle JSX component usage: <Component /> or <Component>...</Component>
|
|
816
|
+
// Only track PascalCase names (React components), not lowercase (HTML elements)
|
|
817
|
+
if (node.type === 'jsx_self_closing_element' || node.type === 'jsx_opening_element') {
|
|
818
|
+
// First named child is the element name
|
|
819
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
820
|
+
const child = node.namedChild(i);
|
|
821
|
+
if (child.type === 'identifier') {
|
|
822
|
+
const name = child.text;
|
|
823
|
+
// React components start with uppercase
|
|
824
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
825
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
826
|
+
calls.push({
|
|
827
|
+
name: name,
|
|
828
|
+
line: node.startPosition.row + 1,
|
|
829
|
+
isMethod: false,
|
|
830
|
+
isJsxComponent: true,
|
|
831
|
+
enclosingFunction
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
// Handle namespaced components: <Foo.Bar />
|
|
837
|
+
if (child.type === 'member_expression' || child.type === 'nested_identifier') {
|
|
838
|
+
const text = child.text;
|
|
839
|
+
// Get the last part after the dot
|
|
840
|
+
const parts = text.split('.');
|
|
841
|
+
const componentName = parts[parts.length - 1];
|
|
842
|
+
if (componentName && /^[A-Z]/.test(componentName)) {
|
|
843
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
844
|
+
calls.push({
|
|
845
|
+
name: componentName,
|
|
846
|
+
line: node.startPosition.row + 1,
|
|
847
|
+
isMethod: true,
|
|
848
|
+
receiver: parts.slice(0, -1).join('.'),
|
|
849
|
+
isJsxComponent: true,
|
|
850
|
+
enclosingFunction
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
|
|
815
859
|
return true;
|
|
816
860
|
}, {
|
|
817
861
|
onLeave: (node) => {
|
|
@@ -1338,6 +1382,10 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1338
1382
|
usageType = 'reference';
|
|
1339
1383
|
}
|
|
1340
1384
|
}
|
|
1385
|
+
// JSX component usage: <Component /> or <Component>...</Component>
|
|
1386
|
+
else if (parent.type === 'jsx_self_closing_element' || parent.type === 'jsx_opening_element') {
|
|
1387
|
+
usageType = 'call'; // Treat JSX component usage as a "call"
|
|
1388
|
+
}
|
|
1341
1389
|
}
|
|
1342
1390
|
|
|
1343
1391
|
usages.push({ line, column, usageType });
|
package/package.json
CHANGED
package/test/parser.test.js
CHANGED
|
@@ -4547,6 +4547,38 @@ func (s *ServiceB) helper() {}
|
|
|
4547
4547
|
}
|
|
4548
4548
|
});
|
|
4549
4549
|
|
|
4550
|
+
it('should detect JSX component usage as calls', () => {
|
|
4551
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-jsx-'));
|
|
4552
|
+
try {
|
|
4553
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), `{"name": "test"}`);
|
|
4554
|
+
|
|
4555
|
+
fs.writeFileSync(path.join(tmpDir, 'Page.tsx'), `
|
|
4556
|
+
function EnvironmentsPage() {
|
|
4557
|
+
return <div>Hello</div>;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
function App() {
|
|
4561
|
+
return <EnvironmentsPage />;
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
export { App, EnvironmentsPage };
|
|
4565
|
+
`);
|
|
4566
|
+
|
|
4567
|
+
const index = new ProjectIndex(tmpDir);
|
|
4568
|
+
index.build('**/*.tsx', { quiet: true });
|
|
4569
|
+
|
|
4570
|
+
const usages = index.usages('EnvironmentsPage', { codeOnly: true });
|
|
4571
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
4572
|
+
|
|
4573
|
+
// Should find JSX usage as a call
|
|
4574
|
+
assert.ok(calls.length >= 1, 'Should find at least 1 JSX component usage');
|
|
4575
|
+
assert.ok(calls.some(c => c.content && c.content.includes('<EnvironmentsPage')),
|
|
4576
|
+
'Should detect <EnvironmentsPage /> as a call');
|
|
4577
|
+
} finally {
|
|
4578
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
4579
|
+
}
|
|
4580
|
+
});
|
|
4581
|
+
|
|
4550
4582
|
it('should detect Rust method calls in usages', () => {
|
|
4551
4583
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-rust-method-'));
|
|
4552
4584
|
try {
|
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Public Repository Test Script for UCN
|
|
5
|
-
*
|
|
6
|
-
* Tests UCN against real public repositories to find edge cases and bugs.
|
|
7
|
-
* Run with: node test/public-repos-test.js [--verbose] [--keep]
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const { execSync, spawnSync } = require('child_process');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const os = require('os');
|
|
14
|
-
|
|
15
|
-
// Configuration
|
|
16
|
-
const UCN_PATH = path.join(__dirname, '..', 'ucn.js');
|
|
17
|
-
const TEMP_DIR = path.join(os.tmpdir(), 'ucn-test-repos');
|
|
18
|
-
const TIMEOUT = 60000; // 60 seconds
|
|
19
|
-
|
|
20
|
-
// Parse args
|
|
21
|
-
const args = process.argv.slice(2);
|
|
22
|
-
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
23
|
-
const keepRepos = args.includes('--keep');
|
|
24
|
-
|
|
25
|
-
// Colors
|
|
26
|
-
const c = {
|
|
27
|
-
reset: '\x1b[0m',
|
|
28
|
-
red: '\x1b[31m',
|
|
29
|
-
green: '\x1b[32m',
|
|
30
|
-
yellow: '\x1b[33m',
|
|
31
|
-
blue: '\x1b[34m',
|
|
32
|
-
cyan: '\x1b[36m',
|
|
33
|
-
dim: '\x1b[2m',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Results tracking
|
|
37
|
-
const results = {
|
|
38
|
-
passed: 0,
|
|
39
|
-
failed: 0,
|
|
40
|
-
bugs: [],
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Public repositories to test - small, well-known repos
|
|
44
|
-
const repos = {
|
|
45
|
-
javascript: [
|
|
46
|
-
{
|
|
47
|
-
name: 'preact-signals',
|
|
48
|
-
url: 'https://github.com/preactjs/signals',
|
|
49
|
-
dir: 'packages/core/src',
|
|
50
|
-
symbols: ['signal', 'computed', 'effect', 'batch'],
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
typescript: [
|
|
54
|
-
{
|
|
55
|
-
name: 'zod',
|
|
56
|
-
url: 'https://github.com/colinhacks/zod',
|
|
57
|
-
dir: 'src',
|
|
58
|
-
symbols: ['ZodType', 'string', 'parse', 'safeParse'],
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
python: [
|
|
62
|
-
{
|
|
63
|
-
name: 'httpx',
|
|
64
|
-
url: 'https://github.com/encode/httpx',
|
|
65
|
-
dir: 'httpx',
|
|
66
|
-
symbols: ['Client', 'get', 'post', 'request'],
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
go: [
|
|
70
|
-
{
|
|
71
|
-
name: 'cobra',
|
|
72
|
-
url: 'https://github.com/spf13/cobra',
|
|
73
|
-
dir: '.',
|
|
74
|
-
symbols: ['Command', 'Execute', 'AddCommand', 'Flags'],
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
rust: [
|
|
78
|
-
{
|
|
79
|
-
name: 'ripgrep',
|
|
80
|
-
url: 'https://github.com/BurntSushi/ripgrep',
|
|
81
|
-
dir: 'crates/core',
|
|
82
|
-
symbols: ['Searcher', 'search', 'new', 'build'],
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
java: [
|
|
86
|
-
{
|
|
87
|
-
name: 'gson',
|
|
88
|
-
url: 'https://github.com/google/gson',
|
|
89
|
-
dir: 'gson/src/main/java/com/google/gson',
|
|
90
|
-
symbols: ['Gson', 'toJson', 'fromJson', 'JsonElement'],
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Commands to test
|
|
96
|
-
const commands = [
|
|
97
|
-
{ name: 'toc', args: [] },
|
|
98
|
-
{ name: 'stats', args: [] },
|
|
99
|
-
{ name: 'find', args: ['$SYM'] },
|
|
100
|
-
{ name: 'usages', args: ['$SYM'] },
|
|
101
|
-
{ name: 'context', args: ['$SYM'] },
|
|
102
|
-
{ name: 'about', args: ['$SYM'] },
|
|
103
|
-
{ name: 'smart', args: ['$SYM'] },
|
|
104
|
-
{ name: 'impact', args: ['$SYM'] },
|
|
105
|
-
{ name: 'trace', args: ['$SYM', '--depth=2'] },
|
|
106
|
-
{ name: 'api', args: [] },
|
|
107
|
-
{ name: 'deadcode', args: [] },
|
|
108
|
-
{ name: 'find', args: ['$SYM', '--json'], id: 'find-json' },
|
|
109
|
-
{ name: 'toc', args: ['--json'], id: 'toc-json' },
|
|
110
|
-
{ name: 'search', args: ['TODO'] },
|
|
111
|
-
{ name: 'fn', args: ['$SYM'] },
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Clone a repository
|
|
116
|
-
*/
|
|
117
|
-
function cloneRepo(url, name) {
|
|
118
|
-
const repoPath = path.join(TEMP_DIR, name);
|
|
119
|
-
|
|
120
|
-
if (fs.existsSync(repoPath)) {
|
|
121
|
-
if (verbose) console.log(`${c.dim} Using cached: ${name}${c.reset}`);
|
|
122
|
-
return repoPath;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.log(`${c.dim} Cloning: ${name}...${c.reset}`);
|
|
126
|
-
try {
|
|
127
|
-
execSync(`git clone --depth 1 ${url} ${repoPath}`, {
|
|
128
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
129
|
-
timeout: 120000,
|
|
130
|
-
});
|
|
131
|
-
return repoPath;
|
|
132
|
-
} catch (e) {
|
|
133
|
-
console.log(`${c.yellow} ⚠ Failed to clone ${name}: ${e.message}${c.reset}`);
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Run UCN command
|
|
140
|
-
*/
|
|
141
|
-
function runUcn(targetPath, command, args = []) {
|
|
142
|
-
const fullArgs = ['node', UCN_PATH, targetPath, command, ...args, '--no-cache'];
|
|
143
|
-
const cmdStr = fullArgs.join(' ');
|
|
144
|
-
|
|
145
|
-
if (verbose) {
|
|
146
|
-
console.log(`${c.dim} Running: ${cmdStr}${c.reset}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const result = spawnSync('node', [UCN_PATH, targetPath, command, ...args, '--no-cache'], {
|
|
151
|
-
timeout: TIMEOUT,
|
|
152
|
-
encoding: 'utf8',
|
|
153
|
-
maxBuffer: 50 * 1024 * 1024, // 50MB buffer
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (result.error) {
|
|
157
|
-
return { success: false, error: result.error.message, command: cmdStr };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (result.status !== 0) {
|
|
161
|
-
const stderr = result.stderr || '';
|
|
162
|
-
// Check for expected "no results" type errors
|
|
163
|
-
const isExpectedNoResult =
|
|
164
|
-
stderr.includes('No matches found') ||
|
|
165
|
-
stderr.includes('not found') ||
|
|
166
|
-
stderr.includes('No usages found') ||
|
|
167
|
-
stderr.includes('No tests found') ||
|
|
168
|
-
stderr.includes('No deadcode') ||
|
|
169
|
-
stderr.includes('No imports');
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
success: isExpectedNoResult,
|
|
173
|
-
output: result.stdout,
|
|
174
|
-
error: stderr,
|
|
175
|
-
command: cmdStr,
|
|
176
|
-
exitCode: result.status,
|
|
177
|
-
isExpectedNoResult,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return { success: true, output: result.stdout, command: cmdStr };
|
|
182
|
-
} catch (e) {
|
|
183
|
-
return { success: false, error: e.message, command: cmdStr };
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Check for error patterns in output
|
|
189
|
-
*/
|
|
190
|
-
function checkForErrors(output) {
|
|
191
|
-
if (!output) return null;
|
|
192
|
-
|
|
193
|
-
const patterns = [
|
|
194
|
-
{ pattern: /TypeError: .+/g, name: 'TypeError' },
|
|
195
|
-
{ pattern: /ReferenceError: .+/g, name: 'ReferenceError' },
|
|
196
|
-
{ pattern: /SyntaxError: .+/g, name: 'SyntaxError' },
|
|
197
|
-
{ pattern: /Cannot read propert(y|ies) .+ of (undefined|null)/gi, name: 'Property access on null/undefined' },
|
|
198
|
-
{ pattern: /is not a function/gi, name: 'Not a function' },
|
|
199
|
-
{ pattern: /undefined is not/gi, name: 'Undefined error' },
|
|
200
|
-
{ pattern: /FATAL ERROR/gi, name: 'Fatal error' },
|
|
201
|
-
{ pattern: /Maximum call stack/gi, name: 'Stack overflow' },
|
|
202
|
-
{ pattern: /heap out of memory/gi, name: 'Memory error' },
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
for (const { pattern, name } of patterns) {
|
|
206
|
-
const match = output.match(pattern);
|
|
207
|
-
if (match) {
|
|
208
|
-
return { type: name, match: match[0] };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Test a repository
|
|
217
|
-
*/
|
|
218
|
-
function testRepo(lang, repo) {
|
|
219
|
-
console.log(`\n${c.blue}Testing ${lang}/${repo.name}${c.reset}`);
|
|
220
|
-
|
|
221
|
-
const repoPath = cloneRepo(repo.url, repo.name);
|
|
222
|
-
if (!repoPath) {
|
|
223
|
-
results.failed++;
|
|
224
|
-
results.bugs.push({
|
|
225
|
-
language: lang,
|
|
226
|
-
repo: repo.name,
|
|
227
|
-
error: 'Failed to clone repository',
|
|
228
|
-
});
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const targetPath = path.join(repoPath, repo.dir);
|
|
233
|
-
if (!fs.existsSync(targetPath)) {
|
|
234
|
-
console.log(`${c.yellow} ⚠ Directory not found: ${repo.dir}${c.reset}`);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Test each command
|
|
239
|
-
for (const cmd of commands) {
|
|
240
|
-
const cmdArgs = cmd.args.map(a => (a === '$SYM' ? repo.symbols[0] : a));
|
|
241
|
-
const testId = cmd.id || `${cmd.name}(${cmdArgs.join(',')})`;
|
|
242
|
-
|
|
243
|
-
const result = runUcn(targetPath, cmd.name, cmdArgs);
|
|
244
|
-
|
|
245
|
-
// Check for crashes/errors
|
|
246
|
-
const errorInOutput = checkForErrors(result.output);
|
|
247
|
-
const errorInStderr = checkForErrors(result.error);
|
|
248
|
-
const foundError = errorInOutput || errorInStderr;
|
|
249
|
-
|
|
250
|
-
if (foundError) {
|
|
251
|
-
results.failed++;
|
|
252
|
-
results.bugs.push({
|
|
253
|
-
language: lang,
|
|
254
|
-
repo: repo.name,
|
|
255
|
-
command: cmd.name,
|
|
256
|
-
args: cmdArgs,
|
|
257
|
-
error: `${foundError.type}: ${foundError.match}`,
|
|
258
|
-
fullCommand: result.command,
|
|
259
|
-
});
|
|
260
|
-
console.log(` ${c.red}✗${c.reset} ${testId}: ${foundError.type}`);
|
|
261
|
-
if (verbose) {
|
|
262
|
-
console.log(` ${c.red}${foundError.match}${c.reset}`);
|
|
263
|
-
}
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Check for non-zero exit without expected "no results"
|
|
268
|
-
if (!result.success && !result.isExpectedNoResult) {
|
|
269
|
-
results.failed++;
|
|
270
|
-
results.bugs.push({
|
|
271
|
-
language: lang,
|
|
272
|
-
repo: repo.name,
|
|
273
|
-
command: cmd.name,
|
|
274
|
-
args: cmdArgs,
|
|
275
|
-
error: result.error || 'Command failed',
|
|
276
|
-
fullCommand: result.command,
|
|
277
|
-
exitCode: result.exitCode,
|
|
278
|
-
});
|
|
279
|
-
console.log(` ${c.red}✗${c.reset} ${testId}`);
|
|
280
|
-
if (verbose) {
|
|
281
|
-
console.log(` ${c.red}${result.error || 'Failed'}${c.reset}`);
|
|
282
|
-
}
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Check JSON validity for JSON commands
|
|
287
|
-
if (cmd.args.includes('--json') && result.output) {
|
|
288
|
-
try {
|
|
289
|
-
JSON.parse(result.output);
|
|
290
|
-
} catch (e) {
|
|
291
|
-
results.failed++;
|
|
292
|
-
results.bugs.push({
|
|
293
|
-
language: lang,
|
|
294
|
-
repo: repo.name,
|
|
295
|
-
command: cmd.name,
|
|
296
|
-
args: cmdArgs,
|
|
297
|
-
error: `Invalid JSON: ${e.message}`,
|
|
298
|
-
fullCommand: result.command,
|
|
299
|
-
output: result.output.substring(0, 200),
|
|
300
|
-
});
|
|
301
|
-
console.log(` ${c.red}✗${c.reset} ${testId}: Invalid JSON`);
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
results.passed++;
|
|
307
|
-
if (verbose) {
|
|
308
|
-
console.log(` ${c.green}✓${c.reset} ${testId}`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Additional stress tests
|
|
315
|
-
*/
|
|
316
|
-
function runStressTests() {
|
|
317
|
-
console.log(`\n${c.blue}Running STRESS TESTS${c.reset}`);
|
|
318
|
-
|
|
319
|
-
const jsFixture = path.join(__dirname, 'fixtures', 'javascript');
|
|
320
|
-
|
|
321
|
-
const tests = [
|
|
322
|
-
// Large depth values
|
|
323
|
-
{
|
|
324
|
-
name: 'Very large depth',
|
|
325
|
-
run: () => runUcn(jsFixture, 'trace', ['processData', '--depth=100']),
|
|
326
|
-
check: r => !checkForErrors(r.output) && !checkForErrors(r.error),
|
|
327
|
-
},
|
|
328
|
-
// Many concurrent operations simulation (sequential but rapid)
|
|
329
|
-
{
|
|
330
|
-
name: 'Rapid sequential commands',
|
|
331
|
-
run: () => {
|
|
332
|
-
for (let i = 0; i < 10; i++) {
|
|
333
|
-
const r = runUcn(jsFixture, 'toc', []);
|
|
334
|
-
if (!r.success) return r;
|
|
335
|
-
}
|
|
336
|
-
return { success: true };
|
|
337
|
-
},
|
|
338
|
-
check: r => r.success,
|
|
339
|
-
},
|
|
340
|
-
// Large symbol names in search
|
|
341
|
-
{
|
|
342
|
-
name: 'Pattern-like symbol',
|
|
343
|
-
run: () => runUcn(jsFixture, 'find', ['.*']),
|
|
344
|
-
check: r => !checkForErrors(r.output),
|
|
345
|
-
},
|
|
346
|
-
// Empty string searches
|
|
347
|
-
{
|
|
348
|
-
name: 'Empty search',
|
|
349
|
-
run: () => runUcn(jsFixture, 'search', ['']),
|
|
350
|
-
check: r => !checkForErrors(r.output),
|
|
351
|
-
},
|
|
352
|
-
// Newlines in search
|
|
353
|
-
{
|
|
354
|
-
name: 'Newline in search',
|
|
355
|
-
run: () => runUcn(jsFixture, 'search', ['test\ntest']),
|
|
356
|
-
check: r => !checkForErrors(r.output),
|
|
357
|
-
},
|
|
358
|
-
// Null bytes
|
|
359
|
-
{
|
|
360
|
-
name: 'Null byte in search',
|
|
361
|
-
run: () => runUcn(jsFixture, 'search', ['test\x00test']),
|
|
362
|
-
check: r => !checkForErrors(r.output),
|
|
363
|
-
},
|
|
364
|
-
];
|
|
365
|
-
|
|
366
|
-
for (const test of tests) {
|
|
367
|
-
const result = test.run();
|
|
368
|
-
const passed = test.check(result);
|
|
369
|
-
|
|
370
|
-
if (passed) {
|
|
371
|
-
results.passed++;
|
|
372
|
-
if (verbose) {
|
|
373
|
-
console.log(` ${c.green}✓${c.reset} ${test.name}`);
|
|
374
|
-
}
|
|
375
|
-
} else {
|
|
376
|
-
results.failed++;
|
|
377
|
-
results.bugs.push({
|
|
378
|
-
language: 'stress',
|
|
379
|
-
command: test.name,
|
|
380
|
-
error: result.error || 'Test failed',
|
|
381
|
-
fullCommand: result.command,
|
|
382
|
-
});
|
|
383
|
-
console.log(` ${c.red}✗${c.reset} ${test.name}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Print summary
|
|
390
|
-
*/
|
|
391
|
-
function printSummary() {
|
|
392
|
-
console.log(`\n${c.cyan}${'='.repeat(60)}${c.reset}`);
|
|
393
|
-
console.log(`${c.cyan}PUBLIC REPO TEST SUMMARY${c.reset}`);
|
|
394
|
-
console.log(`${c.cyan}${'='.repeat(60)}${c.reset}`);
|
|
395
|
-
|
|
396
|
-
console.log(`\n ${c.green}Passed:${c.reset} ${results.passed}`);
|
|
397
|
-
console.log(` ${c.red}Failed:${c.reset} ${results.failed}`);
|
|
398
|
-
console.log(` Total: ${results.passed + results.failed}`);
|
|
399
|
-
|
|
400
|
-
if (results.bugs.length > 0) {
|
|
401
|
-
console.log(`\n${c.red}BUGS FOUND (${results.bugs.length}):${c.reset}`);
|
|
402
|
-
console.log(`${'-'.repeat(60)}`);
|
|
403
|
-
|
|
404
|
-
// Group by language
|
|
405
|
-
const byLang = {};
|
|
406
|
-
for (const bug of results.bugs) {
|
|
407
|
-
const key = bug.repo ? `${bug.language}/${bug.repo}` : bug.language;
|
|
408
|
-
if (!byLang[key]) byLang[key] = [];
|
|
409
|
-
byLang[key].push(bug);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
for (const [key, bugs] of Object.entries(byLang)) {
|
|
413
|
-
console.log(`\n${c.yellow}${key}:${c.reset}`);
|
|
414
|
-
for (const bug of bugs) {
|
|
415
|
-
console.log(` • ${bug.command}: ${bug.error}`);
|
|
416
|
-
if (bug.fullCommand) {
|
|
417
|
-
console.log(` ${c.dim}${bug.fullCommand}${c.reset}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Save to file
|
|
423
|
-
const bugsFile = path.join(__dirname, 'public-repos-bugs.json');
|
|
424
|
-
fs.writeFileSync(bugsFile, JSON.stringify(results.bugs, null, 2));
|
|
425
|
-
console.log(`\n${c.dim}Bug report saved to: ${bugsFile}${c.reset}`);
|
|
426
|
-
} else {
|
|
427
|
-
console.log(`\n${c.green}No bugs found!${c.reset}`);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
console.log(`${c.cyan}${'='.repeat(60)}${c.reset}`);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Cleanup
|
|
435
|
-
*/
|
|
436
|
-
function cleanup() {
|
|
437
|
-
if (!keepRepos && fs.existsSync(TEMP_DIR)) {
|
|
438
|
-
console.log(`\n${c.dim}Cleaning up temp directory...${c.reset}`);
|
|
439
|
-
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Main
|
|
445
|
-
*/
|
|
446
|
-
async function main() {
|
|
447
|
-
console.log(`${c.cyan}UCN Public Repository Test Suite${c.reset}`);
|
|
448
|
-
console.log(`${c.dim}Testing against real-world repositories${c.reset}`);
|
|
449
|
-
|
|
450
|
-
// Create temp directory
|
|
451
|
-
if (!fs.existsSync(TEMP_DIR)) {
|
|
452
|
-
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Test each language
|
|
456
|
-
for (const [lang, repoList] of Object.entries(repos)) {
|
|
457
|
-
for (const repo of repoList) {
|
|
458
|
-
testRepo(lang, repo);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Run stress tests
|
|
463
|
-
runStressTests();
|
|
464
|
-
|
|
465
|
-
// Print summary
|
|
466
|
-
printSummary();
|
|
467
|
-
|
|
468
|
-
// Cleanup
|
|
469
|
-
cleanup();
|
|
470
|
-
|
|
471
|
-
process.exit(results.failed > 0 ? 1 : 0);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
main().catch(e => {
|
|
475
|
-
console.error(`${c.red}Fatal error: ${e.message}${c.reset}`);
|
|
476
|
-
process.exit(1);
|
|
477
|
-
});
|