ucn 3.4.3 → 3.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/README.md +103 -24
- package/cli/index.js +7 -1
- package/core/discovery.js +2 -1
- package/core/project.js +9 -11
- package/languages/java.js +1 -1
- package/languages/rust.js +1 -1
- package/mcp/server.js +1566 -0
- package/package.json +7 -2
- package/test/mcp-edge-cases.js +453 -0
- package/test/parser.test.js +158 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.5",
|
|
4
4
|
"description": "Code navigation built by AI, for AI. Reduces context usage when working with large codebases.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ucn": "./cli/index.js"
|
|
7
|
+
"ucn": "./cli/index.js",
|
|
8
|
+
"ucn-mcp": "./mcp/server.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"test": "node --test test/parser.test.js"
|
|
@@ -38,5 +39,9 @@
|
|
|
38
39
|
"tree-sitter-python": "^0.21.0",
|
|
39
40
|
"tree-sitter-rust": "^0.21.0",
|
|
40
41
|
"tree-sitter-typescript": "^0.21.0"
|
|
42
|
+
},
|
|
43
|
+
"optionalDependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
|
+
"zod": "^3.25.0"
|
|
41
46
|
}
|
|
42
47
|
}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Server Edge Case Test Suite
|
|
5
|
+
*
|
|
6
|
+
* Tests all 23 UCN MCP tools with null/crash safety, input validation,
|
|
7
|
+
* and normal operation edge cases.
|
|
8
|
+
*
|
|
9
|
+
* Communicates with the MCP server over stdio using newline-delimited JSON-RPC.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const SERVER_PATH = path.join(__dirname, '..', 'mcp', 'server.js');
|
|
16
|
+
const PROJECT_DIR = '/Users/mihail/ucn';
|
|
17
|
+
const TIMEOUT_MS = 30000;
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// JSON-RPC over stdio transport (newline-delimited JSON)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
class McpClient {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.proc = null;
|
|
26
|
+
this.requestId = 0;
|
|
27
|
+
this.pending = new Map();
|
|
28
|
+
this.buffer = '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
start() {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
this.proc = spawn('node', [SERVER_PATH], {
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
+
env: { ...process.env, NODE_ENV: 'test' }
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.proc.stderr.on('data', (data) => {
|
|
39
|
+
// MCP server logs to stderr - ignore
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.proc.stdout.on('data', (chunk) => {
|
|
43
|
+
this.buffer += chunk.toString();
|
|
44
|
+
this._processBuffer();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.proc.on('error', (err) => {
|
|
48
|
+
reject(err);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.proc.on('exit', (code) => {
|
|
52
|
+
for (const [id, entry] of this.pending) {
|
|
53
|
+
clearTimeout(entry.timer);
|
|
54
|
+
entry.reject(new Error(`Server exited with code ${code}`));
|
|
55
|
+
}
|
|
56
|
+
this.pending.clear();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Give server a moment to start
|
|
60
|
+
setTimeout(() => resolve(), 500);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_processBuffer() {
|
|
65
|
+
// Newline-delimited JSON: each message is a single line terminated by \n
|
|
66
|
+
while (true) {
|
|
67
|
+
const nlIndex = this.buffer.indexOf('\n');
|
|
68
|
+
if (nlIndex === -1) break;
|
|
69
|
+
|
|
70
|
+
const line = this.buffer.substring(0, nlIndex).replace(/\r$/, '');
|
|
71
|
+
this.buffer = this.buffer.substring(nlIndex + 1);
|
|
72
|
+
|
|
73
|
+
if (!line.trim()) continue;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const msg = JSON.parse(line);
|
|
77
|
+
this._handleMessage(msg);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error('Failed to parse JSON-RPC message:', e.message, 'line:', line.substring(0, 100));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_handleMessage(msg) {
|
|
85
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
86
|
+
const entry = this.pending.get(msg.id);
|
|
87
|
+
clearTimeout(entry.timer);
|
|
88
|
+
this.pending.delete(msg.id);
|
|
89
|
+
if (msg.error) {
|
|
90
|
+
entry.resolve({ error: msg.error });
|
|
91
|
+
} else {
|
|
92
|
+
entry.resolve({ result: msg.result });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Notifications (no id) are ignored
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
send(method, params) {
|
|
99
|
+
const id = ++this.requestId;
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
this.pending.delete(id);
|
|
103
|
+
reject(new Error('TIMEOUT'));
|
|
104
|
+
}, TIMEOUT_MS);
|
|
105
|
+
|
|
106
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
107
|
+
|
|
108
|
+
const message = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
|
109
|
+
this.proc.stdin.write(message);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
notify(method, params) {
|
|
114
|
+
const message = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
|
|
115
|
+
this.proc.stdin.write(message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async initialize() {
|
|
119
|
+
const res = await this.send('initialize', {
|
|
120
|
+
protocolVersion: '2025-03-26',
|
|
121
|
+
capabilities: {},
|
|
122
|
+
clientInfo: { name: 'test-client', version: '1.0.0' }
|
|
123
|
+
});
|
|
124
|
+
this.notify('notifications/initialized', {});
|
|
125
|
+
return res;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async callTool(toolName, args) {
|
|
129
|
+
return this.send('tools/call', { name: toolName, arguments: args });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
stop() {
|
|
133
|
+
if (this.proc) {
|
|
134
|
+
this.proc.stdin.end();
|
|
135
|
+
this.proc.kill();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Test definitions
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
const tests = [
|
|
145
|
+
// ========================================================================
|
|
146
|
+
// CATEGORY 1: Null/Crash Safety (nonexistent symbols/files)
|
|
147
|
+
// ========================================================================
|
|
148
|
+
{
|
|
149
|
+
category: 'Null/Crash Safety',
|
|
150
|
+
tool: 'ucn_about',
|
|
151
|
+
desc: 'nonexistent symbol',
|
|
152
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
category: 'Null/Crash Safety',
|
|
156
|
+
tool: 'ucn_context',
|
|
157
|
+
desc: 'nonexistent symbol',
|
|
158
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
category: 'Null/Crash Safety',
|
|
162
|
+
tool: 'ucn_impact',
|
|
163
|
+
desc: 'nonexistent symbol',
|
|
164
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
category: 'Null/Crash Safety',
|
|
168
|
+
tool: 'ucn_smart',
|
|
169
|
+
desc: 'nonexistent symbol',
|
|
170
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
category: 'Null/Crash Safety',
|
|
174
|
+
tool: 'ucn_trace',
|
|
175
|
+
desc: 'nonexistent symbol',
|
|
176
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
category: 'Null/Crash Safety',
|
|
180
|
+
tool: 'ucn_verify',
|
|
181
|
+
desc: 'nonexistent symbol',
|
|
182
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
category: 'Null/Crash Safety',
|
|
186
|
+
tool: 'ucn_related',
|
|
187
|
+
desc: 'nonexistent symbol',
|
|
188
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
category: 'Null/Crash Safety',
|
|
192
|
+
tool: 'ucn_example',
|
|
193
|
+
desc: 'nonexistent symbol',
|
|
194
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' }
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
category: 'Null/Crash Safety',
|
|
198
|
+
tool: 'ucn_fn',
|
|
199
|
+
desc: 'nonexistent function',
|
|
200
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_function_xyz' }
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
category: 'Null/Crash Safety',
|
|
204
|
+
tool: 'ucn_class',
|
|
205
|
+
desc: 'nonexistent class',
|
|
206
|
+
args: { project_dir: PROJECT_DIR, name: 'ZzzNonexistentClassXyz' }
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
category: 'Null/Crash Safety',
|
|
210
|
+
tool: 'ucn_tests',
|
|
211
|
+
desc: 'nonexistent name',
|
|
212
|
+
args: { project_dir: PROJECT_DIR, name: 'zzz_nonexistent_test_xyz' }
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
category: 'Null/Crash Safety',
|
|
216
|
+
tool: 'ucn_typedef',
|
|
217
|
+
desc: 'nonexistent type',
|
|
218
|
+
args: { project_dir: PROJECT_DIR, name: 'ZzzNonexistentTypeXyz' }
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
category: 'Null/Crash Safety',
|
|
222
|
+
tool: 'ucn_graph',
|
|
223
|
+
desc: 'nonexistent file',
|
|
224
|
+
args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
category: 'Null/Crash Safety',
|
|
228
|
+
tool: 'ucn_file_exports',
|
|
229
|
+
desc: 'nonexistent file',
|
|
230
|
+
args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
category: 'Null/Crash Safety',
|
|
234
|
+
tool: 'ucn_imports',
|
|
235
|
+
desc: 'nonexistent file',
|
|
236
|
+
args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
category: 'Null/Crash Safety',
|
|
240
|
+
tool: 'ucn_exporters',
|
|
241
|
+
desc: 'nonexistent file',
|
|
242
|
+
args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// ========================================================================
|
|
246
|
+
// CATEGORY 2: Input Validation
|
|
247
|
+
// ========================================================================
|
|
248
|
+
{
|
|
249
|
+
category: 'Input Validation',
|
|
250
|
+
tool: 'ucn_find',
|
|
251
|
+
desc: 'whitespace-only name',
|
|
252
|
+
args: { project_dir: PROJECT_DIR, name: ' ' }
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
category: 'Input Validation',
|
|
256
|
+
tool: 'ucn_about',
|
|
257
|
+
desc: 'name with special chars "foo()"',
|
|
258
|
+
args: { project_dir: PROJECT_DIR, name: 'foo()' }
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
category: 'Input Validation',
|
|
262
|
+
tool: 'ucn_search',
|
|
263
|
+
desc: 'regex special chars "[test"',
|
|
264
|
+
args: { project_dir: PROJECT_DIR, term: '[test' }
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
category: 'Input Validation',
|
|
268
|
+
tool: 'ucn_plan',
|
|
269
|
+
desc: 'no operation specified',
|
|
270
|
+
args: { project_dir: PROJECT_DIR, name: 'getIndex' }
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
category: 'Input Validation',
|
|
274
|
+
tool: 'ucn_stacktrace',
|
|
275
|
+
desc: 'empty stack',
|
|
276
|
+
args: { project_dir: PROJECT_DIR, stack: '' }
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
category: 'Input Validation',
|
|
280
|
+
tool: 'ucn_toc',
|
|
281
|
+
desc: 'nonexistent project_dir',
|
|
282
|
+
args: { project_dir: '/nonexistent/fake/directory/abc123' }
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
category: 'Input Validation',
|
|
286
|
+
tool: 'ucn_find',
|
|
287
|
+
desc: 'top=0',
|
|
288
|
+
args: { project_dir: PROJECT_DIR, name: 'getIndex', top: 0 }
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
category: 'Input Validation',
|
|
292
|
+
tool: 'ucn_find',
|
|
293
|
+
desc: 'top=-1',
|
|
294
|
+
args: { project_dir: PROJECT_DIR, name: 'getIndex', top: -1 }
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
// ========================================================================
|
|
298
|
+
// CATEGORY 3: Normal Operations (verify no crash)
|
|
299
|
+
// ========================================================================
|
|
300
|
+
{
|
|
301
|
+
category: 'Normal Operations',
|
|
302
|
+
tool: 'ucn_toc',
|
|
303
|
+
desc: 'project overview',
|
|
304
|
+
args: { project_dir: PROJECT_DIR }
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
category: 'Normal Operations',
|
|
308
|
+
tool: 'ucn_find',
|
|
309
|
+
desc: 'find "getIndex"',
|
|
310
|
+
args: { project_dir: PROJECT_DIR, name: 'getIndex' }
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
category: 'Normal Operations',
|
|
314
|
+
tool: 'ucn_deadcode',
|
|
315
|
+
desc: 'find dead code',
|
|
316
|
+
args: { project_dir: PROJECT_DIR }
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
category: 'Normal Operations',
|
|
320
|
+
tool: 'ucn_search',
|
|
321
|
+
desc: 'search for "TODO"',
|
|
322
|
+
args: { project_dir: PROJECT_DIR, term: 'TODO' }
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Test runner
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
async function run() {
|
|
331
|
+
const client = new McpClient();
|
|
332
|
+
const results = [];
|
|
333
|
+
|
|
334
|
+
console.log('Starting MCP server...');
|
|
335
|
+
await client.start();
|
|
336
|
+
|
|
337
|
+
console.log('Sending initialize...');
|
|
338
|
+
const initRes = await client.initialize();
|
|
339
|
+
if (initRes.error) {
|
|
340
|
+
console.error('Initialize failed:', JSON.stringify(initRes.error));
|
|
341
|
+
client.stop();
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
console.log('Server initialized successfully.\n');
|
|
345
|
+
console.log('Running ' + tests.length + ' edge case tests...\n');
|
|
346
|
+
|
|
347
|
+
// Run tests sequentially
|
|
348
|
+
for (let i = 0; i < tests.length; i++) {
|
|
349
|
+
const t = tests[i];
|
|
350
|
+
const label = `[${i + 1}/${tests.length}] ${t.tool} - ${t.desc}`;
|
|
351
|
+
process.stdout.write(` ${label} ... `);
|
|
352
|
+
|
|
353
|
+
const startTime = Date.now();
|
|
354
|
+
let status = 'FAIL';
|
|
355
|
+
let detail = '';
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const res = await client.callTool(t.tool, t.args);
|
|
359
|
+
const elapsed = Date.now() - startTime;
|
|
360
|
+
|
|
361
|
+
if (res.error) {
|
|
362
|
+
// JSON-RPC level error - server responded, not a crash
|
|
363
|
+
status = 'PASS';
|
|
364
|
+
detail = `RPC error: ${res.error.message || JSON.stringify(res.error)} (${elapsed}ms)`;
|
|
365
|
+
} else if (res.result) {
|
|
366
|
+
const content = res.result.content;
|
|
367
|
+
const isError = res.result.isError === true;
|
|
368
|
+
const text = content && content[0] && content[0].text || '';
|
|
369
|
+
const preview = text.substring(0, 120).replace(/\n/g, '\\n');
|
|
370
|
+
status = 'PASS';
|
|
371
|
+
detail = `${isError ? 'ERROR response' : 'OK'}: "${preview}" (${elapsed}ms)`;
|
|
372
|
+
} else {
|
|
373
|
+
status = 'PASS';
|
|
374
|
+
detail = `Empty result (${elapsed}ms)`;
|
|
375
|
+
}
|
|
376
|
+
} catch (e) {
|
|
377
|
+
const elapsed = Date.now() - startTime;
|
|
378
|
+
if (e.message === 'TIMEOUT') {
|
|
379
|
+
status = 'FAIL';
|
|
380
|
+
detail = `TIMEOUT after ${TIMEOUT_MS}ms`;
|
|
381
|
+
} else {
|
|
382
|
+
status = 'FAIL';
|
|
383
|
+
detail = `CRASH: ${e.message} (${elapsed}ms)`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log(status);
|
|
388
|
+
results.push({
|
|
389
|
+
num: i + 1,
|
|
390
|
+
category: t.category,
|
|
391
|
+
tool: t.tool,
|
|
392
|
+
desc: t.desc,
|
|
393
|
+
status,
|
|
394
|
+
detail
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
client.stop();
|
|
399
|
+
|
|
400
|
+
// ========================================================================
|
|
401
|
+
// Summary table
|
|
402
|
+
// ========================================================================
|
|
403
|
+
console.log('\n' + '='.repeat(140));
|
|
404
|
+
console.log('SUMMARY');
|
|
405
|
+
console.log('='.repeat(140));
|
|
406
|
+
|
|
407
|
+
const categories = [...new Set(results.map(r => r.category))];
|
|
408
|
+
|
|
409
|
+
for (const cat of categories) {
|
|
410
|
+
console.log(`\n--- ${cat} ---`);
|
|
411
|
+
console.log(
|
|
412
|
+
'#'.padEnd(5) +
|
|
413
|
+
'Tool'.padEnd(22) +
|
|
414
|
+
'Description'.padEnd(42) +
|
|
415
|
+
'Status'.padEnd(8) +
|
|
416
|
+
'Detail'
|
|
417
|
+
);
|
|
418
|
+
console.log('-'.repeat(140));
|
|
419
|
+
|
|
420
|
+
const catResults = results.filter(r => r.category === cat);
|
|
421
|
+
for (const r of catResults) {
|
|
422
|
+
console.log(
|
|
423
|
+
String(r.num).padEnd(5) +
|
|
424
|
+
r.tool.padEnd(22) +
|
|
425
|
+
r.desc.padEnd(42) +
|
|
426
|
+
r.status.padEnd(8) +
|
|
427
|
+
r.detail.substring(0, 100)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const passed = results.filter(r => r.status === 'PASS').length;
|
|
433
|
+
const failed = results.filter(r => r.status === 'FAIL').length;
|
|
434
|
+
const total = results.length;
|
|
435
|
+
|
|
436
|
+
console.log('\n' + '='.repeat(140));
|
|
437
|
+
console.log(`TOTAL: ${total} tests | PASS: ${passed} | FAIL: ${failed}`);
|
|
438
|
+
console.log('='.repeat(140));
|
|
439
|
+
|
|
440
|
+
if (failed > 0) {
|
|
441
|
+
console.log('\nFailed tests:');
|
|
442
|
+
for (const r of results.filter(r => r.status === 'FAIL')) {
|
|
443
|
+
console.log(` ${r.num}. ${r.tool} - ${r.desc}: ${r.detail}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
run().catch(e => {
|
|
451
|
+
console.error('Test runner crashed:', e);
|
|
452
|
+
process.exit(2);
|
|
453
|
+
});
|
package/test/parser.test.js
CHANGED
|
@@ -6188,6 +6188,164 @@ class Line:
|
|
|
6188
6188
|
});
|
|
6189
6189
|
});
|
|
6190
6190
|
|
|
6191
|
+
describe('Regression: JS this.method() same-class resolution', () => {
|
|
6192
|
+
it('findCallees should resolve this.method() to same-class methods', () => {
|
|
6193
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-jsthis-'));
|
|
6194
|
+
try {
|
|
6195
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}');
|
|
6196
|
+
fs.writeFileSync(path.join(tmpDir, 'service.js'), `
|
|
6197
|
+
class DataService {
|
|
6198
|
+
_fetchRemote(key, days) {
|
|
6199
|
+
return this._makeRequest(\`/api/\${key}\`);
|
|
6200
|
+
}
|
|
6201
|
+
|
|
6202
|
+
_makeRequest(url) {
|
|
6203
|
+
return null;
|
|
6204
|
+
}
|
|
6205
|
+
|
|
6206
|
+
getRecords(key, days = 365) {
|
|
6207
|
+
if (this._isValid(key)) {
|
|
6208
|
+
return this._fetchRemote(key, days);
|
|
6209
|
+
}
|
|
6210
|
+
return null;
|
|
6211
|
+
}
|
|
6212
|
+
|
|
6213
|
+
_isValid(key) {
|
|
6214
|
+
return key.length > 0;
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
`);
|
|
6218
|
+
const index = new ProjectIndex(tmpDir);
|
|
6219
|
+
index.build('**/*.js', { quiet: true });
|
|
6220
|
+
|
|
6221
|
+
// getRecords should have _fetchRemote and _isValid as callees
|
|
6222
|
+
const defs = index.symbols.get('getRecords');
|
|
6223
|
+
assert.ok(defs && defs.length > 0, 'Should find getRecords');
|
|
6224
|
+
const callees = index.findCallees(defs[0]);
|
|
6225
|
+
const calleeNames = callees.map(c => c.name);
|
|
6226
|
+
assert.ok(calleeNames.includes('_fetchRemote'),
|
|
6227
|
+
`Should resolve this._fetchRemote(), got: ${calleeNames.join(', ')}`);
|
|
6228
|
+
assert.ok(calleeNames.includes('_isValid'),
|
|
6229
|
+
`Should resolve this._isValid(), got: ${calleeNames.join(', ')}`);
|
|
6230
|
+
|
|
6231
|
+
// _fetchRemote should have getRecords as caller
|
|
6232
|
+
const callers = index.findCallers('_fetchRemote');
|
|
6233
|
+
const callerNames = callers.map(c => c.callerName);
|
|
6234
|
+
assert.ok(callerNames.includes('getRecords'),
|
|
6235
|
+
`Should find getRecords as caller of _fetchRemote, got: ${callerNames.join(', ')}`);
|
|
6236
|
+
} finally {
|
|
6237
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6238
|
+
}
|
|
6239
|
+
});
|
|
6240
|
+
});
|
|
6241
|
+
|
|
6242
|
+
describe('Regression: Java this.method() same-class resolution', () => {
|
|
6243
|
+
it('findCallees should resolve this.method() to same-class methods', () => {
|
|
6244
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-javathis-'));
|
|
6245
|
+
try {
|
|
6246
|
+
fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
|
|
6247
|
+
fs.writeFileSync(path.join(tmpDir, 'DataService.java'), `
|
|
6248
|
+
public class DataService {
|
|
6249
|
+
private Object fetchRemote(String key, int days) {
|
|
6250
|
+
return this.makeRequest("/api/" + key);
|
|
6251
|
+
}
|
|
6252
|
+
|
|
6253
|
+
private Object makeRequest(String url) {
|
|
6254
|
+
return null;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
public Object getRecords(String key) {
|
|
6258
|
+
if (this.isValid(key)) {
|
|
6259
|
+
return this.fetchRemote(key, 365);
|
|
6260
|
+
}
|
|
6261
|
+
return null;
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
private boolean isValid(String key) {
|
|
6265
|
+
return key.length() > 0;
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
`);
|
|
6269
|
+
const index = new ProjectIndex(tmpDir);
|
|
6270
|
+
index.build('**/*.java', { quiet: true });
|
|
6271
|
+
|
|
6272
|
+
// getRecords should have fetchRemote and isValid as callees
|
|
6273
|
+
const defs = index.symbols.get('getRecords');
|
|
6274
|
+
assert.ok(defs && defs.length > 0, 'Should find getRecords');
|
|
6275
|
+
const callees = index.findCallees(defs[0]);
|
|
6276
|
+
const calleeNames = callees.map(c => c.name);
|
|
6277
|
+
assert.ok(calleeNames.includes('fetchRemote'),
|
|
6278
|
+
`Should resolve this.fetchRemote(), got: ${calleeNames.join(', ')}`);
|
|
6279
|
+
assert.ok(calleeNames.includes('isValid'),
|
|
6280
|
+
`Should resolve this.isValid(), got: ${calleeNames.join(', ')}`);
|
|
6281
|
+
|
|
6282
|
+
// fetchRemote should have getRecords as caller
|
|
6283
|
+
const callers = index.findCallers('fetchRemote');
|
|
6284
|
+
const callerNames = callers.map(c => c.callerName);
|
|
6285
|
+
assert.ok(callerNames.includes('getRecords'),
|
|
6286
|
+
`Should find getRecords as caller of fetchRemote, got: ${callerNames.join(', ')}`);
|
|
6287
|
+
} finally {
|
|
6288
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6289
|
+
}
|
|
6290
|
+
});
|
|
6291
|
+
});
|
|
6292
|
+
|
|
6293
|
+
describe('Regression: Rust self.method() same-class resolution', () => {
|
|
6294
|
+
it('findCallees should resolve self.method() to same-class methods', () => {
|
|
6295
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-rustself-'));
|
|
6296
|
+
try {
|
|
6297
|
+
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"');
|
|
6298
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
6299
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'service.rs'), `
|
|
6300
|
+
struct DataService {
|
|
6301
|
+
base_url: String,
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6304
|
+
impl DataService {
|
|
6305
|
+
fn fetch_remote(&self, key: &str, days: i32) -> Option<String> {
|
|
6306
|
+
self.make_request(&format!("/api/{}", key))
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
fn make_request(&self, url: &str) -> Option<String> {
|
|
6310
|
+
None
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6313
|
+
fn get_records(&self, key: &str) -> Option<String> {
|
|
6314
|
+
if self.is_valid(key) {
|
|
6315
|
+
return self.fetch_remote(key, 365);
|
|
6316
|
+
}
|
|
6317
|
+
None
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6320
|
+
fn is_valid(&self, key: &str) -> bool {
|
|
6321
|
+
!key.is_empty()
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
6324
|
+
`);
|
|
6325
|
+
const index = new ProjectIndex(tmpDir);
|
|
6326
|
+
index.build('**/*.rs', { quiet: true });
|
|
6327
|
+
|
|
6328
|
+
// get_records should have fetch_remote and is_valid as callees
|
|
6329
|
+
const defs = index.symbols.get('get_records');
|
|
6330
|
+
assert.ok(defs && defs.length > 0, 'Should find get_records');
|
|
6331
|
+
const callees = index.findCallees(defs[0]);
|
|
6332
|
+
const calleeNames = callees.map(c => c.name);
|
|
6333
|
+
assert.ok(calleeNames.includes('fetch_remote'),
|
|
6334
|
+
`Should resolve self.fetch_remote(), got: ${calleeNames.join(', ')}`);
|
|
6335
|
+
assert.ok(calleeNames.includes('is_valid'),
|
|
6336
|
+
`Should resolve self.is_valid(), got: ${calleeNames.join(', ')}`);
|
|
6337
|
+
|
|
6338
|
+
// fetch_remote should have get_records as caller
|
|
6339
|
+
const callers = index.findCallers('fetch_remote');
|
|
6340
|
+
const callerNames = callers.map(c => c.callerName);
|
|
6341
|
+
assert.ok(callerNames.includes('get_records'),
|
|
6342
|
+
`Should find get_records as caller of fetch_remote, got: ${callerNames.join(', ')}`);
|
|
6343
|
+
} finally {
|
|
6344
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6345
|
+
}
|
|
6346
|
+
});
|
|
6347
|
+
});
|
|
6348
|
+
|
|
6191
6349
|
describe('Regression: Python self.method() same-class resolution', () => {
|
|
6192
6350
|
it('findCallees should resolve self.method() to same-class methods', () => {
|
|
6193
6351
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-selfmethod-'));
|