ucn 3.7.45 → 3.7.47

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
@@ -623,12 +623,57 @@ function findCallsInCode(code, parser) {
623
623
  const tree = parseTree(parser, code);
624
624
  const calls = [];
625
625
  const functionStack = []; // Stack of { name, startLine, endLine }
626
+ // Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
627
+ const scopeTypes = new Map();
626
628
 
627
629
  // Helper to check if a node creates a function scope
628
630
  const isFunctionNode = (node) => {
629
631
  return ['method_declaration', 'constructor_declaration', 'lambda_expression'].includes(node.type);
630
632
  };
631
633
 
634
+ // Extract type name from a Java type node (strips generics, qualified names)
635
+ const extractTypeName = (typeNode) => {
636
+ if (!typeNode) return null;
637
+ if (typeNode.type === 'type_identifier') return typeNode.text;
638
+ if (typeNode.type === 'generic_type') {
639
+ // List<String> -> List (first named child is the base type)
640
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
641
+ const r = extractTypeName(typeNode.namedChild(i));
642
+ if (r) return r;
643
+ }
644
+ }
645
+ if (typeNode.type === 'scoped_type_identifier') {
646
+ // pkg.Type -> Type (last identifier)
647
+ const nameNode = typeNode.childForFieldName('name') ||
648
+ typeNode.namedChild(typeNode.namedChildCount - 1);
649
+ return nameNode?.text || null;
650
+ }
651
+ if (typeNode.type === 'array_type') {
652
+ return extractTypeName(typeNode.namedChild(0));
653
+ }
654
+ return null;
655
+ };
656
+
657
+ // Build type map from method/constructor parameters
658
+ const buildScopeTypeMap = (node) => {
659
+ const typeMap = new Map();
660
+ const paramsNode = node.childForFieldName('parameters');
661
+ if (paramsNode) {
662
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
663
+ const param = paramsNode.namedChild(i);
664
+ if (param.type === 'formal_parameter' || param.type === 'spread_parameter') {
665
+ const nameNode = param.childForFieldName('name');
666
+ const typeNode = param.childForFieldName('type');
667
+ const typeName = extractTypeName(typeNode);
668
+ if (nameNode && typeName) {
669
+ typeMap.set(nameNode.text, typeName);
670
+ }
671
+ }
672
+ }
673
+ }
674
+ return typeMap;
675
+ };
676
+
632
677
  // Helper to extract function name from a function node
633
678
  const extractFunctionName = (node) => {
634
679
  if (node.type === 'method_declaration') {
@@ -652,14 +697,25 @@ function findCallsInCode(code, parser) {
652
697
  : null;
653
698
  };
654
699
 
700
+ // Look up variable type from scope chain
701
+ const getReceiverType = (varName) => {
702
+ for (let i = functionStack.length - 1; i >= 0; i--) {
703
+ const typeMap = scopeTypes.get(functionStack[i].startLine);
704
+ if (typeMap?.has(varName)) return typeMap.get(varName);
705
+ }
706
+ return undefined;
707
+ };
708
+
655
709
  traverseTree(tree.rootNode, (node) => {
656
710
  // Track function entry
657
711
  if (isFunctionNode(node)) {
658
- functionStack.push({
712
+ const entry = {
659
713
  name: extractFunctionName(node),
660
714
  startLine: node.startPosition.row + 1,
661
715
  endLine: node.endPosition.row + 1
662
- });
716
+ };
717
+ functionStack.push(entry);
718
+ scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
663
719
  }
664
720
 
665
721
  // Handle method invocations: foo(), obj.foo(), this.foo()
@@ -669,11 +725,14 @@ function findCallsInCode(code, parser) {
669
725
 
670
726
  if (nameNode) {
671
727
  const enclosingFunction = getCurrentEnclosingFunction();
728
+ const receiver = (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined;
729
+ const receiverType = (receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined;
672
730
  calls.push({
673
731
  name: nameNode.text,
674
732
  line: node.startPosition.row + 1,
675
733
  isMethod: !!objNode,
676
- receiver: (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined,
734
+ receiver,
735
+ ...(receiverType && { receiverType }),
677
736
  enclosingFunction
678
737
  });
679
738
  }
@@ -708,11 +767,57 @@ function findCallsInCode(code, parser) {
708
767
  return true;
709
768
  }
710
769
 
770
+ // Detect method references passed as arguments: this::worker, obj::method
771
+ if (node.type === 'method_reference') {
772
+ const nameNode = node.namedChild(node.namedChildCount - 1);
773
+ const objNode = node.namedChild(0);
774
+ if (nameNode && nameNode.type === 'identifier') {
775
+ const receiver = objNode ? (objNode.type === 'identifier' || objNode.type === 'this' ? objNode.text : undefined) : undefined;
776
+ const receiverType = (receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined;
777
+ const enclosingFunction = getCurrentEnclosingFunction();
778
+ calls.push({
779
+ name: nameNode.text,
780
+ line: node.startPosition.row + 1,
781
+ isMethod: !!receiver,
782
+ receiver,
783
+ ...(receiverType && { receiverType }),
784
+ isFunctionReference: true,
785
+ isPotentialCallback: true,
786
+ enclosingFunction
787
+ });
788
+ }
789
+ return true;
790
+ }
791
+
792
+ // Track local variable types from new Type() assignments
793
+ // e.g., Foo f = new Foo(); or var f = new Foo();
794
+ if (node.type === 'local_variable_declaration' && functionStack.length > 0) {
795
+ for (let i = 0; i < node.namedChildCount; i++) {
796
+ const child = node.namedChild(i);
797
+ if (child.type === 'variable_declarator') {
798
+ const nameNode = child.childForFieldName('name');
799
+ const valueNode = child.childForFieldName('value');
800
+ if (nameNode && valueNode && valueNode.type === 'object_creation_expression') {
801
+ const typeNode = valueNode.childForFieldName('type');
802
+ const typeName = extractTypeName(typeNode);
803
+ if (typeName) {
804
+ const scopeKey = functionStack[functionStack.length - 1].startLine;
805
+ const typeMap = scopeTypes.get(scopeKey);
806
+ if (typeMap) typeMap.set(nameNode.text, typeName);
807
+ }
808
+ }
809
+ }
810
+ }
811
+ }
812
+
711
813
  return true;
712
814
  }, {
713
815
  onLeave: (node) => {
714
816
  if (isFunctionNode(node)) {
715
- functionStack.pop();
817
+ const leaving = functionStack.pop();
818
+ if (leaving) {
819
+ scopeTypes.delete(leaving.startLine);
820
+ }
716
821
  }
717
822
  }
718
823
  });
package/languages/rust.js CHANGED
@@ -631,12 +631,59 @@ function findCallsInCode(code, parser) {
631
631
  const tree = parseTree(parser, code);
632
632
  const calls = [];
633
633
  const functionStack = []; // Stack of { name, startLine, endLine }
634
+ // Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
635
+ const scopeTypes = new Map();
634
636
 
635
637
  // Helper to check if a node creates a function scope
636
638
  const isFunctionNode = (node) => {
637
639
  return ['function_item', 'closure_expression'].includes(node.type);
638
640
  };
639
641
 
642
+ // Extract the base type name from a Rust type node (strips &, &mut, Box<>, etc.)
643
+ const extractTypeName = (typeNode) => {
644
+ if (!typeNode) return null;
645
+ if (typeNode.type === 'type_identifier') return typeNode.text;
646
+ if (typeNode.type === 'reference_type') {
647
+ // &Filter or &mut Filter -> Filter
648
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
649
+ const r = extractTypeName(typeNode.namedChild(i));
650
+ if (r) return r;
651
+ }
652
+ }
653
+ if (typeNode.type === 'generic_type') {
654
+ // Box<Filter> -> Filter (or get the outer type)
655
+ return extractTypeName(typeNode.namedChild(0));
656
+ }
657
+ if (typeNode.type === 'scoped_type_identifier') {
658
+ // module::Type -> Type
659
+ const nameNode = typeNode.childForFieldName('name');
660
+ return nameNode?.text || null;
661
+ }
662
+ return null;
663
+ };
664
+
665
+ // Build type map from function parameters (including self receiver for impl methods)
666
+ const buildScopeTypeMap = (node) => {
667
+ const typeMap = new Map();
668
+ const paramsNode = node.childForFieldName('parameters');
669
+ if (paramsNode) {
670
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
671
+ const param = paramsNode.namedChild(i);
672
+ if (param.type === 'parameter') {
673
+ const patternNode = param.childForFieldName('pattern');
674
+ const typeNode = param.childForFieldName('type');
675
+ const typeName = extractTypeName(typeNode);
676
+ if (patternNode && typeName) {
677
+ // Pattern can be identifier or _
678
+ const name = patternNode.type === 'identifier' ? patternNode.text : null;
679
+ if (name) typeMap.set(name, typeName);
680
+ }
681
+ }
682
+ }
683
+ }
684
+ return typeMap;
685
+ };
686
+
640
687
  // Helper to extract function name from a function node
641
688
  const extractFunctionName = (node) => {
642
689
  if (node.type === 'function_item') {
@@ -656,14 +703,25 @@ function findCallsInCode(code, parser) {
656
703
  : null;
657
704
  };
658
705
 
706
+ // Look up variable type from scope chain
707
+ const getReceiverType = (varName) => {
708
+ for (let i = functionStack.length - 1; i >= 0; i--) {
709
+ const typeMap = scopeTypes.get(functionStack[i].startLine);
710
+ if (typeMap?.has(varName)) return typeMap.get(varName);
711
+ }
712
+ return undefined;
713
+ };
714
+
659
715
  traverseTree(tree.rootNode, (node) => {
660
716
  // Track function entry
661
717
  if (isFunctionNode(node)) {
662
- functionStack.push({
718
+ const entry = {
663
719
  name: extractFunctionName(node),
664
720
  startLine: node.startPosition.row + 1,
665
721
  endLine: node.endPosition.row + 1
666
- });
722
+ };
723
+ functionStack.push(entry);
724
+ scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
667
725
  }
668
726
 
669
727
  // Handle function calls: foo(), obj.method(), Type::func(), foo::<T>()
@@ -692,11 +750,14 @@ function findCallsInCode(code, parser) {
692
750
  const valueNode = funcNode.childForFieldName('value');
693
751
 
694
752
  if (fieldNode) {
753
+ const receiver = (valueNode?.type === 'identifier' || valueNode?.type === 'self') ? valueNode.text : undefined;
754
+ const receiverType = (receiver && receiver !== 'self') ? getReceiverType(receiver) : undefined;
695
755
  calls.push({
696
756
  name: fieldNode.text,
697
757
  line: node.startPosition.row + 1,
698
758
  isMethod: true,
699
- receiver: (valueNode?.type === 'identifier' || valueNode?.type === 'self') ? valueNode.text : undefined,
759
+ receiver,
760
+ ...(receiverType && { receiverType }),
700
761
  enclosingFunction
701
762
  });
702
763
  }
@@ -738,11 +799,39 @@ function findCallsInCode(code, parser) {
738
799
  return true;
739
800
  }
740
801
 
802
+ // Detect function/method references passed as arguments:
803
+ // field_expression inside arguments (obj.method as callback)
804
+ if (node.type === 'field_expression' && node.parent?.type === 'arguments') {
805
+ const grandparent = node.parent?.parent;
806
+ if (!grandparent || grandparent.type !== 'call_expression' || grandparent.childForFieldName('function') !== node) {
807
+ const fieldNode = node.childForFieldName('field');
808
+ const valueNode = node.childForFieldName('value');
809
+ if (fieldNode) {
810
+ const receiver = (valueNode?.type === 'identifier' || valueNode?.type === 'self') ? valueNode.text : undefined;
811
+ const receiverType = (receiver && receiver !== 'self') ? getReceiverType(receiver) : undefined;
812
+ const enclosingFunction = getCurrentEnclosingFunction();
813
+ calls.push({
814
+ name: fieldNode.text,
815
+ line: node.startPosition.row + 1,
816
+ isMethod: true,
817
+ receiver,
818
+ ...(receiverType && { receiverType }),
819
+ isFunctionReference: true,
820
+ isPotentialCallback: true,
821
+ enclosingFunction
822
+ });
823
+ }
824
+ }
825
+ }
826
+
741
827
  return true;
742
828
  }, {
743
829
  onLeave: (node) => {
744
830
  if (isFunctionNode(node)) {
745
- functionStack.pop();
831
+ const leaving = functionStack.pop();
832
+ if (leaving) {
833
+ scopeTypes.delete(leaving.startLine);
834
+ }
746
835
  }
747
836
  }
748
837
  });
package/mcp/server.js CHANGED
@@ -52,11 +52,17 @@ function getIndex(projectDir) {
52
52
  }
53
53
  const root = findProjectRoot(absDir);
54
54
  const cached = indexCache.get(root);
55
+ const STALE_CHECK_INTERVAL_MS = 2000;
55
56
 
56
- // Always check staleness — isCacheStale() is cheap (mtime/size checks)
57
- if (cached && !cached.index.isCacheStale()) {
58
- cached.checkedAt = Date.now(); // True LRU: refresh on access
59
- return cached.index;
57
+ // Throttle staleness checks — isCacheStale() re-globs and stats all files
58
+ if (cached) {
59
+ if (Date.now() - cached.checkedAt < STALE_CHECK_INTERVAL_MS) {
60
+ return cached.index; // Recently verified fresh
61
+ }
62
+ if (!cached.index.isCacheStale()) {
63
+ cached.checkedAt = Date.now();
64
+ return cached.index;
65
+ }
60
66
  }
61
67
 
62
68
  // Build new index (or rebuild stale one)
@@ -158,7 +164,7 @@ function requireName(name) {
158
164
  // CONSOLIDATED TOOL REGISTRATION
159
165
  // ============================================================================
160
166
 
161
- const TOOL_DESCRIPTION = `Universal Code Navigator powered by tree-sitter ASTs. Analyzes code structure — functions, callers, callees, dependencies across JavaScript/TypeScript, Python, Go, Rust, Java, and HTML (inline scripts and event handlers). Use instead of grep/read for code relationships.
167
+ const TOOL_DESCRIPTION = `Code intelligence toolkit for AI agents. Extract specific functions, trace call chains, find all callers, and detect dead code without reading entire files or scanning full projects. Use instead of grep/read for code relationships. Supports JavaScript/TypeScript, Python, Go, Rust, Java, and HTML.
162
168
 
163
169
  TOP 5 (covers 90% of tasks): about, impact, trace, find, deadcode
164
170
 
@@ -252,7 +258,9 @@ server.registerTool(
252
258
  case_sensitive: z.boolean().optional().describe('Case-sensitive search (default: false, case-insensitive)'),
253
259
  all: z.boolean().optional().describe('Show all results (expand truncated sections). Applies to about, toc, related, trace, and others.'),
254
260
  top_level: z.boolean().optional().describe('Show only top-level functions in toc (exclude nested/indented)'),
255
- class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)')
261
+ class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
262
+ limit: z.number().optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, toc --detailed.'),
263
+ max_files: z.number().optional().describe('Max files to index (default: 10000). Use for very large codebases.')
256
264
 
257
265
  })
258
266
  },
@@ -350,18 +358,22 @@ server.registerTool(
350
358
 
351
359
  case 'usages': {
352
360
  const index = getIndex(project_dir);
353
- const { ok, result, error } = execute(index, 'usages', ep);
361
+ const { ok, result, error, note } = execute(index, 'usages', ep);
354
362
  if (!ok) return toolResult(error); // soft error
355
- return toolResult(output.formatUsages(result, ep.name));
363
+ let text = output.formatUsages(result, ep.name);
364
+ if (note) text += '\n\n' + note;
365
+ return toolResult(text);
356
366
  }
357
367
 
358
368
  case 'toc': {
359
369
  const index = getIndex(project_dir);
360
- const { ok, result, error } = execute(index, 'toc', ep);
370
+ const { ok, result, error, note } = execute(index, 'toc', ep);
361
371
  if (!ok) return toolResult(error); // soft error
362
- return toolResult(output.formatToc(result, {
372
+ let text = output.formatToc(result, {
363
373
  topHint: 'Set top=N or use detailed=false for compact view.'
364
- }));
374
+ });
375
+ if (note) text += '\n\n' + note;
376
+ return toolResult(text);
365
377
  }
366
378
 
367
379
  case 'search': {
@@ -380,13 +392,16 @@ server.registerTool(
380
392
 
381
393
  case 'deadcode': {
382
394
  const index = getIndex(project_dir);
383
- const { ok, result, error } = execute(index, 'deadcode', ep);
395
+ const { ok, result, error, note } = execute(index, 'deadcode', ep);
384
396
  if (!ok) return toolResult(error); // soft error
385
- return toolResult(output.formatDeadcode(result, {
397
+ const dcNote = note;
398
+ let dcText = output.formatDeadcode(result, {
386
399
  top: ep.top || 0,
387
400
  decoratedHint: !ep.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.` : undefined,
388
401
  exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use include_exported=true to audit them.` : undefined
389
- }));
402
+ });
403
+ if (dcNote) dcText += '\n\n' + dcNote;
404
+ return toolResult(dcText);
390
405
  }
391
406
 
392
407
  // ── File Dependencies ───────────────────────────────────────
@@ -465,9 +480,11 @@ server.registerTool(
465
480
 
466
481
  case 'api': {
467
482
  const index = getIndex(project_dir);
468
- const { ok, result, error } = execute(index, 'api', ep);
483
+ const { ok, result, error, note } = execute(index, 'api', ep);
469
484
  if (!ok) return toolResult(error); // soft error
470
- return toolResult(output.formatApi(result, ep.file || '.'));
485
+ let apiText = output.formatApi(result, ep.file || '.');
486
+ if (note) apiText += '\n\n' + note;
487
+ return toolResult(apiText);
471
488
  }
472
489
 
473
490
  case 'stats': {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.45",
3
+ "version": "3.7.47",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
- "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
+ "description": "Code intelligence toolkit for AI agents extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
6
6
  "main": "index.js",
7
7
  "bin": {
8
8
  "ucn": "cli/index.js",
@@ -16,28 +16,29 @@
16
16
  "mcp",
17
17
  "mcp-server",
18
18
  "model-context-protocol",
19
+ "ai-agent",
20
+ "ai-coding",
21
+ "code-intelligence",
19
22
  "code-navigation",
20
23
  "code-analysis",
21
- "static-analysis",
24
+ "code-extraction",
22
25
  "call-graph",
23
26
  "callers",
24
27
  "impact-analysis",
25
28
  "dead-code",
26
29
  "deadcode",
27
- "ast",
28
- "tree-sitter",
29
- "parser",
30
- "skill",
31
30
  "agent-skill",
31
+ "skill",
32
32
  "cli",
33
- "ai-agent",
33
+ "tree-sitter",
34
+ "ast",
35
+ "static-analysis",
34
36
  "javascript",
35
37
  "typescript",
36
38
  "python",
37
39
  "go",
38
40
  "rust",
39
- "java",
40
- "html"
41
+ "java"
41
42
  ],
42
43
  "author": "Constantin-Mihail Leoca (https://github.com/mleoca)",
43
44
  "repository": {