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/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
- return toolResult(output.formatExample(index.example(name), name));
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.5",
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": {
@@ -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',