ucn 3.7.19 → 3.7.21

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/core/output.js CHANGED
@@ -8,6 +8,17 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
 
11
+ const FILE_ERROR_MESSAGES = {
12
+ 'file-not-found': 'File not found in project',
13
+ 'file-ambiguous': 'Ambiguous file match'
14
+ };
15
+
16
+ function formatFileError(errorObj, fallbackPath) {
17
+ const msg = FILE_ERROR_MESSAGES[errorObj.error] || errorObj.error;
18
+ const file = errorObj.filePath || fallbackPath || '';
19
+ return `Error: ${msg}: ${file}`;
20
+ }
21
+
11
22
  /**
12
23
  * Normalize parameters for display
13
24
  * Collapses multiline params to single line
@@ -381,7 +392,7 @@ function formatSearchJson(results, term) {
381
392
  * Format imports as JSON
382
393
  */
383
394
  function formatImportsJson(imports, filePath) {
384
- if (imports?.error === 'file-not-found') return JSON.stringify({ found: false, error: 'File not found', file: imports.filePath }, null, 2);
395
+ if (imports?.error) return JSON.stringify({ found: false, error: imports.error, file: imports.filePath || filePath }, null, 2);
385
396
  return JSON.stringify({
386
397
  file: filePath,
387
398
  importCount: imports.length,
@@ -406,7 +417,7 @@ function formatStatsJson(stats) {
406
417
  * Format dependency graph as JSON
407
418
  */
408
419
  function formatGraphJson(graph) {
409
- if (graph?.error === 'file-not-found') return JSON.stringify({ found: false, error: 'File not found', file: graph.filePath }, null, 2);
420
+ if (graph?.error) return JSON.stringify({ found: false, error: graph.error, file: graph.filePath }, null, 2);
410
421
  return JSON.stringify({
411
422
  file: graph.file,
412
423
  depth: graph.depth,
@@ -456,7 +467,7 @@ function formatSmartJson(result) {
456
467
  * Format imports command output - text
457
468
  */
458
469
  function formatImports(imports, filePath) {
459
- if (imports?.error === 'file-not-found') return `Error: File not found in project: ${imports.filePath}`;
470
+ if (imports?.error) return formatFileError(imports, filePath);
460
471
  const lines = [`Imports in ${filePath}:\n`];
461
472
 
462
473
  const internal = imports.filter(i => !i.isExternal && !i.isDynamic);
@@ -505,7 +516,7 @@ function formatImports(imports, filePath) {
505
516
  * Format exporters command output - text
506
517
  */
507
518
  function formatExporters(exporters, filePath) {
508
- if (exporters?.error === 'file-not-found') return `Error: File not found in project: ${exporters.filePath}`;
519
+ if (exporters?.error) return formatFileError(exporters, filePath);
509
520
  const lines = [`Files that import ${filePath}:\n`];
510
521
 
511
522
  if (exporters.length === 0) {
@@ -641,7 +652,7 @@ function formatDisambiguation(matches, name, command) {
641
652
  * Format exporters as JSON
642
653
  */
643
654
  function formatExportersJson(exporters, filePath) {
644
- if (exporters?.error === 'file-not-found') return JSON.stringify({ found: false, error: 'File not found', file: exporters.filePath }, null, 2);
655
+ if (exporters?.error) return JSON.stringify({ found: false, error: exporters.error, file: exporters.filePath || filePath }, null, 2);
645
656
  return JSON.stringify({
646
657
  file: filePath,
647
658
  importerCount: exporters.length,
@@ -1815,7 +1826,7 @@ function formatGraph(graph, options = {}) {
1815
1826
  if (typeof options === 'boolean') {
1816
1827
  options = { showAll: options };
1817
1828
  }
1818
- if (graph?.error === 'file-not-found') return `Error: File not found in project: ${graph.filePath}`;
1829
+ if (graph?.error) return formatFileError(graph);
1819
1830
  if (graph.nodes.length === 0) {
1820
1831
  const file = options.file || graph.root || '';
1821
1832
  return file ? `File not found: ${file}` : 'File not found.';
@@ -1985,7 +1996,7 @@ function formatSearch(results, term) {
1985
1996
  * Format file-exports command output
1986
1997
  */
1987
1998
  function formatFileExports(exports, filePath) {
1988
- if (exports?.error === 'file-not-found') return `Error: File not found in project: ${exports.filePath}`;
1999
+ if (exports?.error) return formatFileError(exports, filePath);
1989
2000
  if (exports.length === 0) return `No exports found in ${filePath}`;
1990
2001
 
1991
2002
  const lines = [];
package/core/parser.js CHANGED
@@ -67,6 +67,9 @@ const { detectLanguage, getParser, getLanguageModule, isSupported, PARSE_OPTIONS
67
67
  * @returns {ParseResult}
68
68
  */
69
69
  function parse(code, language) {
70
+ if (!language) {
71
+ throw new Error('Language parameter is required');
72
+ }
70
73
  // Detect language if file path provided
71
74
  if (language.includes('.') || language.includes('/')) {
72
75
  language = detectLanguage(language);
package/core/project.js CHANGED
@@ -364,6 +364,9 @@ class ProjectIndex {
364
364
  const seenModules = new Set();
365
365
 
366
366
  for (const importModule of fileEntry.imports) {
367
+ // Skip null modules (e.g., dynamic include! macros in Rust)
368
+ if (!importModule) continue;
369
+
367
370
  // Deduplicate: same module imported multiple times in one file
368
371
  // (e.g., lazy imports inside different functions)
369
372
  if (seenModules.has(importModule)) continue;
@@ -1511,6 +1514,7 @@ class ProjectIndex {
1511
1514
  }
1512
1515
 
1513
1516
  const tree = safeParse(parser, content);
1517
+ if (!tree) return false;
1514
1518
 
1515
1519
  // Find all occurrences of name in the line
1516
1520
  const nameRegex = new RegExp('(?<![a-zA-Z0-9_$])' + escapeRegExp(name) + '(?![a-zA-Z0-9_$])', 'g');
@@ -2117,6 +2121,7 @@ class ProjectIndex {
2117
2121
  }
2118
2122
 
2119
2123
  const tree = safeParse(parser, content);
2124
+ if (!tree) return false;
2120
2125
  const tokenType = getTokenTypeAtPosition(tree.rootNode, lineNum, column);
2121
2126
  return tokenType === 'comment' || tokenType === 'string';
2122
2127
  } catch (e) {
@@ -5006,6 +5011,7 @@ class ProjectIndex {
5006
5011
  const parser = getParser(language);
5007
5012
  const content = this._readFile(filePath);
5008
5013
  const tree = safeParse(parser, content);
5014
+ if (!tree) return result;
5009
5015
 
5010
5016
  const row = lineNum - 1;
5011
5017
  const node = tree.rootNode.descendantForPosition({ row, column: 0 });
package/languages/go.js CHANGED
@@ -448,6 +448,7 @@ function findCallsInCode(code, parser) {
448
448
  if (node.type === 'short_var_declaration' || node.type === 'var_declaration') {
449
449
  // Check if RHS contains a func_literal
450
450
  const hasFunc = (n) => {
451
+ if (!n) return false;
451
452
  if (n.type === 'func_literal') return true;
452
453
  for (let i = 0; i < n.childCount; i++) {
453
454
  if (hasFunc(n.child(i))) return true;
@@ -518,7 +519,7 @@ function findCallsInCode(code, parser) {
518
519
  onLeave: (node) => {
519
520
  if (isFunctionNode(node)) {
520
521
  const leaving = functionStack.pop();
521
- closureScopes.delete(leaving.startLine);
522
+ if (leaving) closureScopes.delete(leaving.startLine);
522
523
  }
523
524
  }
524
525
  });
@@ -1453,7 +1453,9 @@ function findImportsInCode(code, parser) {
1453
1453
  }
1454
1454
  }
1455
1455
 
1456
- imports.push({ module: modulePath, names, type: 'require', line, dynamic });
1456
+ if (modulePath) {
1457
+ imports.push({ module: modulePath, names, type: 'require', line, dynamic });
1458
+ }
1457
1459
  }
1458
1460
  }
1459
1461
 
@@ -1466,8 +1468,8 @@ function findImportsInCode(code, parser) {
1466
1468
  if (firstArg && firstArg.type === 'string') {
1467
1469
  const modulePath = firstArg.text.slice(1, -1);
1468
1470
  imports.push({ module: modulePath, names: [], type: 'dynamic', line, dynamic: false });
1469
- } else {
1470
- imports.push({ module: firstArg ? firstArg.text : null, names: [], type: 'dynamic', line, dynamic: true });
1471
+ } else if (firstArg) {
1472
+ imports.push({ module: firstArg.text, names: [], type: 'dynamic', line, dynamic: true });
1471
1473
  }
1472
1474
  }
1473
1475
  }
package/languages/rust.js CHANGED
@@ -845,16 +845,19 @@ function findImportsInCode(code, parser) {
845
845
  if (node.type === 'macro_invocation') {
846
846
  const nameNode = node.childForFieldName('macro');
847
847
  if (nameNode && /^include(_str|_bytes)?$/.test(nameNode.text)) {
848
- const argsNode = node.childForFieldName('argument_list');
848
+ const argsNode = node.namedChildren.find(c => c.type === 'token_tree');
849
849
  const arg = argsNode?.namedChild(0);
850
850
  const dynamic = !arg || arg.type !== 'string_literal';
851
- imports.push({
852
- module: arg ? arg.text.replace(/^["']|["']$/g, '') : null,
853
- names: [],
854
- type: 'include',
855
- dynamic,
856
- line: node.startPosition.row + 1
857
- });
851
+ const modulePath = arg ? arg.text.replace(/^["']|["']$/g, '') : null;
852
+ if (modulePath) {
853
+ imports.push({
854
+ module: modulePath,
855
+ names: [],
856
+ type: 'include',
857
+ dynamic,
858
+ line: node.startPosition.row + 1
859
+ });
860
+ }
858
861
  }
859
862
  }
860
863
  return true;
package/mcp/server.js CHANGED
@@ -108,6 +108,7 @@ const server = new McpServer({
108
108
  const MAX_OUTPUT_CHARS = 100000; // ~100KB, safe for all MCP clients
109
109
 
110
110
  function toolResult(text) {
111
+ if (!text) return { content: [{ type: 'text', text: '(no output)' }] };
111
112
  if (text.length > MAX_OUTPUT_CHARS) {
112
113
  const truncated = text.substring(0, MAX_OUTPUT_CHARS);
113
114
  // Cut at last newline to avoid breaking mid-line
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.19",
3
+ "version": "3.7.21",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
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.",
6
6
  "main": "index.js",