ucn 3.7.5 → 3.7.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.
- package/cli/index.js +178 -97
- package/core/discovery.js +8 -2
- package/core/output.js +158 -0
- package/core/project.js +121 -38
- package/core/shared.js +43 -0
- package/languages/go.js +61 -1
- package/languages/java.js +78 -2
- package/languages/rust.js +71 -2
- package/mcp/server.js +7 -26
- package/package.json +1 -1
- package/test/mcp-edge-cases.js +28 -0
- package/test/parser.test.js +506 -9
package/languages/java.js
CHANGED
|
@@ -279,7 +279,7 @@ function findClasses(code, parser) {
|
|
|
279
279
|
startLine,
|
|
280
280
|
endLine,
|
|
281
281
|
type: 'interface',
|
|
282
|
-
members:
|
|
282
|
+
members: extractClassMembers(node, code),
|
|
283
283
|
modifiers,
|
|
284
284
|
...(docstring && { docstring }),
|
|
285
285
|
...(generics && { generics }),
|
|
@@ -307,7 +307,7 @@ function findClasses(code, parser) {
|
|
|
307
307
|
startLine,
|
|
308
308
|
endLine,
|
|
309
309
|
type: 'enum',
|
|
310
|
-
members:
|
|
310
|
+
members: extractEnumConstants(node, code),
|
|
311
311
|
modifiers,
|
|
312
312
|
...(docstring && { docstring }),
|
|
313
313
|
...(annotations.length > 0 && { annotations })
|
|
@@ -405,6 +405,82 @@ function extractInterfaceExtends(interfaceNode) {
|
|
|
405
405
|
return [];
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Extract enum constants from enum body
|
|
410
|
+
*/
|
|
411
|
+
function extractEnumConstants(enumNode, code) {
|
|
412
|
+
const constants = [];
|
|
413
|
+
const bodyNode = enumNode.childForFieldName('body');
|
|
414
|
+
if (!bodyNode) return constants;
|
|
415
|
+
|
|
416
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
417
|
+
const child = bodyNode.namedChild(i);
|
|
418
|
+
if (child.type === 'enum_constant') {
|
|
419
|
+
const nameNode = child.childForFieldName('name');
|
|
420
|
+
if (nameNode) {
|
|
421
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
422
|
+
const argsNode = child.childForFieldName('arguments');
|
|
423
|
+
constants.push({
|
|
424
|
+
name: nameNode.text,
|
|
425
|
+
startLine,
|
|
426
|
+
endLine,
|
|
427
|
+
memberType: 'constant',
|
|
428
|
+
...(argsNode && { params: argsNode.text.slice(1, -1) })
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Also extract methods from enum_body_declarations
|
|
435
|
+
if (bodyNode) {
|
|
436
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
437
|
+
const child = bodyNode.namedChild(i);
|
|
438
|
+
if (child.type === 'enum_body_declarations') {
|
|
439
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
440
|
+
const member = child.namedChild(j);
|
|
441
|
+
if (member.type === 'method_declaration') {
|
|
442
|
+
const nameNode = member.childForFieldName('name');
|
|
443
|
+
const paramsNode = member.childForFieldName('parameters');
|
|
444
|
+
if (nameNode) {
|
|
445
|
+
const { startLine, endLine } = nodeToLocation(member, code);
|
|
446
|
+
const modifiers = extractModifiers(member);
|
|
447
|
+
const returnType = extractReturnType(member);
|
|
448
|
+
constants.push({
|
|
449
|
+
name: nameNode.text,
|
|
450
|
+
params: extractJavaParams(paramsNode),
|
|
451
|
+
startLine,
|
|
452
|
+
endLine,
|
|
453
|
+
memberType: modifiers.includes('static') ? 'static' : 'method',
|
|
454
|
+
modifiers,
|
|
455
|
+
isMethod: true,
|
|
456
|
+
...(returnType && { returnType })
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
} else if (member.type === 'constructor_declaration') {
|
|
460
|
+
const nameNode = member.childForFieldName('name');
|
|
461
|
+
const paramsNode = member.childForFieldName('parameters');
|
|
462
|
+
if (nameNode) {
|
|
463
|
+
const { startLine, endLine } = nodeToLocation(member, code);
|
|
464
|
+
const modifiers = extractModifiers(member);
|
|
465
|
+
constants.push({
|
|
466
|
+
name: nameNode.text,
|
|
467
|
+
params: extractJavaParams(paramsNode),
|
|
468
|
+
startLine,
|
|
469
|
+
endLine,
|
|
470
|
+
memberType: 'constructor',
|
|
471
|
+
modifiers,
|
|
472
|
+
isMethod: true
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return constants;
|
|
482
|
+
}
|
|
483
|
+
|
|
408
484
|
/**
|
|
409
485
|
* Extract class members (methods, constructors)
|
|
410
486
|
*/
|
package/languages/rust.js
CHANGED
|
@@ -232,7 +232,7 @@ function findClasses(code, parser) {
|
|
|
232
232
|
startLine,
|
|
233
233
|
endLine,
|
|
234
234
|
type: 'enum',
|
|
235
|
-
members:
|
|
235
|
+
members: extractEnumVariants(node, code),
|
|
236
236
|
modifiers: visibility ? [visibility] : [],
|
|
237
237
|
...(docstring && { docstring }),
|
|
238
238
|
...(generics && { generics })
|
|
@@ -258,7 +258,7 @@ function findClasses(code, parser) {
|
|
|
258
258
|
startLine,
|
|
259
259
|
endLine,
|
|
260
260
|
type: 'trait',
|
|
261
|
-
members:
|
|
261
|
+
members: extractTraitMembers(node, code),
|
|
262
262
|
modifiers: visibility ? [visibility] : [],
|
|
263
263
|
...(docstring && { docstring }),
|
|
264
264
|
...(generics && { generics })
|
|
@@ -443,6 +443,75 @@ function extractImplInfo(implNode) {
|
|
|
443
443
|
return { name, traitName, typeName };
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Extract enum variants
|
|
448
|
+
*/
|
|
449
|
+
function extractEnumVariants(enumNode, code) {
|
|
450
|
+
const variants = [];
|
|
451
|
+
const bodyNode = enumNode.childForFieldName('body');
|
|
452
|
+
if (!bodyNode) return variants;
|
|
453
|
+
|
|
454
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
455
|
+
const child = bodyNode.namedChild(i);
|
|
456
|
+
if (child.type === 'enum_variant') {
|
|
457
|
+
const nameNode = child.childForFieldName('name');
|
|
458
|
+
if (nameNode) {
|
|
459
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
460
|
+
// Check for tuple/struct variant data
|
|
461
|
+
let params = undefined;
|
|
462
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
463
|
+
const variantChild = child.namedChild(j);
|
|
464
|
+
if (variantChild.type === 'field_declaration_list' || variantChild.type === 'ordered_field_declaration_list') {
|
|
465
|
+
params = variantChild.text.slice(1, -1);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
variants.push({
|
|
469
|
+
name: nameNode.text,
|
|
470
|
+
startLine,
|
|
471
|
+
endLine,
|
|
472
|
+
memberType: 'variant',
|
|
473
|
+
...(params !== undefined && { params })
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return variants;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Extract trait method signatures
|
|
483
|
+
*/
|
|
484
|
+
function extractTraitMembers(traitNode, code) {
|
|
485
|
+
const members = [];
|
|
486
|
+
const bodyNode = traitNode.childForFieldName('body');
|
|
487
|
+
if (!bodyNode) return members;
|
|
488
|
+
|
|
489
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
490
|
+
const child = bodyNode.namedChild(i);
|
|
491
|
+
if (child.type === 'function_item' || child.type === 'function_signature_item') {
|
|
492
|
+
const nameNode = child.childForFieldName('name');
|
|
493
|
+
if (nameNode) {
|
|
494
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
495
|
+
const paramsNode = child.childForFieldName('parameters');
|
|
496
|
+
const returnType = extractReturnType(child);
|
|
497
|
+
const hasSelf = paramsNode && paramsNode.text.includes('self');
|
|
498
|
+
|
|
499
|
+
members.push({
|
|
500
|
+
name: nameNode.text,
|
|
501
|
+
startLine,
|
|
502
|
+
endLine,
|
|
503
|
+
memberType: 'method',
|
|
504
|
+
isMethod: true,
|
|
505
|
+
...(paramsNode && { params: extractRustParams(paramsNode) }),
|
|
506
|
+
...(returnType && { returnType }),
|
|
507
|
+
...(hasSelf && { receiver: 'self' })
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return members;
|
|
513
|
+
}
|
|
514
|
+
|
|
446
515
|
/**
|
|
447
516
|
* Extract impl block members (functions)
|
|
448
517
|
* @param {Node} implNode - The impl block AST node
|
package/mcp/server.js
CHANGED
|
@@ -34,6 +34,7 @@ const { ProjectIndex } = require('../core/project');
|
|
|
34
34
|
const { findProjectRoot, isTestFile } = require('../core/discovery');
|
|
35
35
|
const { detectLanguage } = require('../core/parser');
|
|
36
36
|
const output = require('../core/output');
|
|
37
|
+
const { pickBestDefinition, addTestExclusions } = require('../core/shared');
|
|
37
38
|
|
|
38
39
|
// ============================================================================
|
|
39
40
|
// INDEX CACHE
|
|
@@ -98,24 +99,6 @@ function getIndex(projectDir) {
|
|
|
98
99
|
return index;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function pickBestDefinition(matches) {
|
|
102
|
-
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
103
|
-
const scored = matches.map(m => {
|
|
104
|
-
let score = 0;
|
|
105
|
-
const rp = m.relativePath || '';
|
|
106
|
-
if (typeOrder.has(m.type)) score += 1000;
|
|
107
|
-
if (isTestFile(rp, detectLanguage(m.file))) score -= 500;
|
|
108
|
-
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
|
|
109
|
-
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
|
|
110
|
-
if (m.startLine && m.endLine) {
|
|
111
|
-
score += Math.min(m.endLine - m.startLine, 100);
|
|
112
|
-
}
|
|
113
|
-
return { match: m, score };
|
|
114
|
-
});
|
|
115
|
-
scored.sort((a, b) => b.score - a.score);
|
|
116
|
-
return scored[0].match;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
102
|
// ============================================================================
|
|
120
103
|
// SERVER SETUP
|
|
121
104
|
// ============================================================================
|
|
@@ -129,13 +112,6 @@ const server = new McpServer({
|
|
|
129
112
|
// TOOL HELPERS
|
|
130
113
|
// ============================================================================
|
|
131
114
|
|
|
132
|
-
function addTestExclusions(exclude) {
|
|
133
|
-
const testPatterns = ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'];
|
|
134
|
-
const existing = new Set((exclude || []).map(e => e.toLowerCase()));
|
|
135
|
-
const additions = testPatterns.filter(p => !existing.has(p));
|
|
136
|
-
return [...(exclude || []), ...additions];
|
|
137
|
-
}
|
|
138
|
-
|
|
139
115
|
function parseExclude(excludeStr) {
|
|
140
116
|
if (!excludeStr) return [];
|
|
141
117
|
return excludeStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
@@ -330,6 +306,7 @@ server.registerTool(
|
|
|
330
306
|
file,
|
|
331
307
|
exclude: parseExclude(exclude)
|
|
332
308
|
});
|
|
309
|
+
if (!ctx) return toolResult(`Symbol "${name}" not found.`);
|
|
333
310
|
const { text, expandable } = output.formatContext(ctx, {
|
|
334
311
|
expandHint: 'Use expand command with item number to see code for any item.'
|
|
335
312
|
});
|
|
@@ -371,6 +348,7 @@ server.registerTool(
|
|
|
371
348
|
includeMethods: include_methods,
|
|
372
349
|
includeUncertain: include_uncertain || false
|
|
373
350
|
});
|
|
351
|
+
if (!result) return toolResult(`Function "${name}" not found.`);
|
|
374
352
|
return toolResult(output.formatSmart(result));
|
|
375
353
|
}
|
|
376
354
|
|
|
@@ -389,7 +367,9 @@ server.registerTool(
|
|
|
389
367
|
const err = requireName(name);
|
|
390
368
|
if (err) return err;
|
|
391
369
|
const index = getIndex(project_dir);
|
|
392
|
-
|
|
370
|
+
const exResult = index.example(name);
|
|
371
|
+
if (!exResult) return toolResult(`No usage examples found for "${name}".`);
|
|
372
|
+
return toolResult(output.formatExample(exResult, name));
|
|
393
373
|
}
|
|
394
374
|
|
|
395
375
|
case 'related': {
|
|
@@ -397,6 +377,7 @@ server.registerTool(
|
|
|
397
377
|
if (err) return err;
|
|
398
378
|
const index = getIndex(project_dir);
|
|
399
379
|
const result = index.related(name, { file, top, all: top !== undefined });
|
|
380
|
+
if (!result) return toolResult(`Symbol "${name}" not found.`);
|
|
400
381
|
return toolResult(output.formatRelated(result, {
|
|
401
382
|
showAll: top !== undefined,
|
|
402
383
|
top,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.6",
|
|
4
4
|
"description": "Universal Code Navigator — AST-based call graph analysis for AI agents. Find callers, trace impact, detect dead code across JS/TS, Python, Go, Rust, Java, and HTML. CLI, MCP server, and agent skill.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/test/mcp-edge-cases.js
CHANGED
|
@@ -376,6 +376,34 @@ const tests = [
|
|
|
376
376
|
args: { command: 'api', project_dir: PROJECT_DIR, file: 'nonexistent.js' },
|
|
377
377
|
assert: (res, text, isError) => (isError && /not found/i.test(text)) || 'Expected file-not-found error message'
|
|
378
378
|
},
|
|
379
|
+
{
|
|
380
|
+
category: 'Correctness',
|
|
381
|
+
tool: 'ucn',
|
|
382
|
+
desc: 'smart(nonexistent) returns "not found" message',
|
|
383
|
+
args: { command: 'smart', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
384
|
+
assert: (res, text, isError) => (!isError && /not found/i.test(text)) || 'Expected "not found" message for nonexistent smart target'
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
category: 'Correctness',
|
|
388
|
+
tool: 'ucn',
|
|
389
|
+
desc: 'context(nonexistent) returns "not found" message',
|
|
390
|
+
args: { command: 'context', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
391
|
+
assert: (res, text, isError) => (!isError && /not found/i.test(text)) || 'Expected "not found" message for nonexistent context target'
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
category: 'Correctness',
|
|
395
|
+
tool: 'ucn',
|
|
396
|
+
desc: 'example(nonexistent) returns "no examples" message',
|
|
397
|
+
args: { command: 'example', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
398
|
+
assert: (res, text, isError) => (!isError && /no .* examples found|not found/i.test(text)) || 'Expected "no examples found" message for nonexistent example target'
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
category: 'Correctness',
|
|
402
|
+
tool: 'ucn',
|
|
403
|
+
desc: 'related(nonexistent) returns "not found" message',
|
|
404
|
+
args: { command: 'related', project_dir: PROJECT_DIR, name: 'zzz_nonexistent_symbol_xyz' },
|
|
405
|
+
assert: (res, text, isError) => (!isError && /not found/i.test(text)) || 'Expected "not found" message for nonexistent related target'
|
|
406
|
+
},
|
|
379
407
|
{
|
|
380
408
|
category: 'Correctness',
|
|
381
409
|
tool: 'ucn',
|