ucn 3.7.20 → 3.7.22
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 +22 -8
- package/core/parser.js +3 -0
- package/core/project.js +43 -6
- package/languages/go.js +2 -1
- package/languages/javascript.js +5 -3
- package/languages/rust.js +1 -1
- package/languages/utils.js +3 -2
- package/mcp/server.js +1 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
@@ -743,10 +754,13 @@ function formatTrace(trace, options = {}) {
|
|
|
743
754
|
if (node.callCount) {
|
|
744
755
|
label += ` ${node.callCount}x`;
|
|
745
756
|
}
|
|
757
|
+
if (node.alreadyShown) {
|
|
758
|
+
label += ' (see above)';
|
|
759
|
+
}
|
|
746
760
|
|
|
747
761
|
lines.push(prefix + connector + label);
|
|
748
762
|
|
|
749
|
-
if (node.children) {
|
|
763
|
+
if (node.children && !node.alreadyShown) {
|
|
750
764
|
const hasMore = node.truncatedChildren > 0;
|
|
751
765
|
for (let i = 0; i < node.children.length; i++) {
|
|
752
766
|
const isChildLast = !hasMore && i === node.children.length - 1;
|
|
@@ -1815,7 +1829,7 @@ function formatGraph(graph, options = {}) {
|
|
|
1815
1829
|
if (typeof options === 'boolean') {
|
|
1816
1830
|
options = { showAll: options };
|
|
1817
1831
|
}
|
|
1818
|
-
if (graph?.error
|
|
1832
|
+
if (graph?.error) return formatFileError(graph);
|
|
1819
1833
|
if (graph.nodes.length === 0) {
|
|
1820
1834
|
const file = options.file || graph.root || '';
|
|
1821
1835
|
return file ? `File not found: ${file}` : 'File not found.';
|
|
@@ -1985,7 +1999,7 @@ function formatSearch(results, term) {
|
|
|
1985
1999
|
* Format file-exports command output
|
|
1986
2000
|
*/
|
|
1987
2001
|
function formatFileExports(exports, filePath) {
|
|
1988
|
-
if (exports?.error
|
|
2002
|
+
if (exports?.error) return formatFileError(exports, filePath);
|
|
1989
2003
|
if (exports.length === 0) return `No exports found in ${filePath}`;
|
|
1990
2004
|
|
|
1991
2005
|
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
|
@@ -1514,6 +1514,7 @@ class ProjectIndex {
|
|
|
1514
1514
|
}
|
|
1515
1515
|
|
|
1516
1516
|
const tree = safeParse(parser, content);
|
|
1517
|
+
if (!tree) return false;
|
|
1517
1518
|
|
|
1518
1519
|
// Find all occurrences of name in the line
|
|
1519
1520
|
const nameRegex = new RegExp('(?<![a-zA-Z0-9_$])' + escapeRegExp(name) + '(?![a-zA-Z0-9_$])', 'g');
|
|
@@ -2120,6 +2121,7 @@ class ProjectIndex {
|
|
|
2120
2121
|
}
|
|
2121
2122
|
|
|
2122
2123
|
const tree = safeParse(parser, content);
|
|
2124
|
+
if (!tree) return false;
|
|
2123
2125
|
const tokenType = getTokenTypeAtPosition(tree.rootNode, lineNum, column);
|
|
2124
2126
|
return tokenType === 'comment' || tokenType === 'string';
|
|
2125
2127
|
} catch (e) {
|
|
@@ -3005,10 +3007,30 @@ class ProjectIndex {
|
|
|
3005
3007
|
// Filter out usages that are at the definition location
|
|
3006
3008
|
// nameLine: when decorators/annotations are present, startLine is the decorator line
|
|
3007
3009
|
// but the name identifier is on a different line (nameLine). Check both.
|
|
3008
|
-
|
|
3010
|
+
let nonDefUsages = allUsages.filter(u =>
|
|
3009
3011
|
!(u.file === symbol.file && (u.line === symbol.startLine || u.line === symbol.nameLine))
|
|
3010
3012
|
);
|
|
3011
3013
|
|
|
3014
|
+
// For exported symbols in --include-exported mode, also filter out export-site
|
|
3015
|
+
// references (e.g., `module.exports = { helperC }` or `export { helperC }`).
|
|
3016
|
+
// These are just re-statements of the export, not actual consumption.
|
|
3017
|
+
if (isExported && options.includeExported) {
|
|
3018
|
+
nonDefUsages = nonDefUsages.filter(u => {
|
|
3019
|
+
if (u.file !== symbol.file) return true; // cross-file usage always counts
|
|
3020
|
+
// Check if same-file usage is on an export line
|
|
3021
|
+
const content = this._readFile(u.file);
|
|
3022
|
+
if (!content) return true;
|
|
3023
|
+
const lines = content.split('\n');
|
|
3024
|
+
const line = lines[u.line - 1] || '';
|
|
3025
|
+
const trimmed = line.trim();
|
|
3026
|
+
// CJS: module.exports = { ... } or exports.name = ...
|
|
3027
|
+
if (trimmed.startsWith('module.exports') || /^exports\.\w+\s*=/.test(trimmed)) return false;
|
|
3028
|
+
// ESM: export { ... } or export default
|
|
3029
|
+
if (/^export\s*\{/.test(trimmed) || /^export\s+default\s/.test(trimmed)) return false;
|
|
3030
|
+
return true;
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3012
3034
|
// Total includes all usage types (calls, references, callbacks, re-exports)
|
|
3013
3035
|
const totalUsages = nonDefUsages.length;
|
|
3014
3036
|
|
|
@@ -3374,10 +3396,22 @@ class ProjectIndex {
|
|
|
3374
3396
|
|
|
3375
3397
|
const buildTree = (funcDef, currentDepth, dir) => {
|
|
3376
3398
|
const funcName = funcDef.name;
|
|
3377
|
-
|
|
3399
|
+
const key = `${funcDef.file}:${funcDef.startLine}`;
|
|
3400
|
+
if (currentDepth > maxDepth) {
|
|
3378
3401
|
return null;
|
|
3379
3402
|
}
|
|
3380
|
-
visited.
|
|
3403
|
+
if (visited.has(key)) {
|
|
3404
|
+
// Already explored — show as leaf node without recursing (prevents infinite loops)
|
|
3405
|
+
return {
|
|
3406
|
+
name: funcName,
|
|
3407
|
+
file: funcDef.relativePath,
|
|
3408
|
+
line: funcDef.startLine,
|
|
3409
|
+
type: funcDef.type,
|
|
3410
|
+
children: [],
|
|
3411
|
+
alreadyShown: true
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
visited.add(key);
|
|
3381
3415
|
|
|
3382
3416
|
const node = {
|
|
3383
3417
|
name: funcName,
|
|
@@ -4009,10 +4043,12 @@ class ProjectIndex {
|
|
|
4009
4043
|
params = params.slice(1);
|
|
4010
4044
|
}
|
|
4011
4045
|
}
|
|
4012
|
-
const expectedParamCount = params.length;
|
|
4013
|
-
const optionalCount = params.filter(p => p.optional || p.default !== undefined).length;
|
|
4014
|
-
const minArgs = expectedParamCount - optionalCount;
|
|
4015
4046
|
const hasRest = params.some(p => p.rest);
|
|
4047
|
+
// Rest params don't count toward expected/min — they accept 0+ extra args
|
|
4048
|
+
const nonRestParams = params.filter(p => !p.rest);
|
|
4049
|
+
const expectedParamCount = nonRestParams.length;
|
|
4050
|
+
const optionalCount = nonRestParams.filter(p => p.optional || p.default !== undefined).length;
|
|
4051
|
+
const minArgs = expectedParamCount - optionalCount;
|
|
4016
4052
|
|
|
4017
4053
|
// Get all call sites
|
|
4018
4054
|
const usages = this.usages(name, { codeOnly: true });
|
|
@@ -5009,6 +5045,7 @@ class ProjectIndex {
|
|
|
5009
5045
|
const parser = getParser(language);
|
|
5010
5046
|
const content = this._readFile(filePath);
|
|
5011
5047
|
const tree = safeParse(parser, content);
|
|
5048
|
+
if (!tree) return result;
|
|
5012
5049
|
|
|
5013
5050
|
const row = lineNum - 1;
|
|
5014
5051
|
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
|
});
|
package/languages/javascript.js
CHANGED
|
@@ -1453,7 +1453,9 @@ function findImportsInCode(code, parser) {
|
|
|
1453
1453
|
}
|
|
1454
1454
|
}
|
|
1455
1455
|
|
|
1456
|
-
|
|
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
|
|
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,7 +845,7 @@ 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.
|
|
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
851
|
const modulePath = arg ? arg.text.replace(/^["']|["']$/g, '') : null;
|
package/languages/utils.js
CHANGED
|
@@ -96,8 +96,9 @@ function parseJSParam(param, info) {
|
|
|
96
96
|
if (patternNode) info.name = patternNode.text;
|
|
97
97
|
if (typeNode) info.type = typeNode.text.replace(/^:\s*/, '');
|
|
98
98
|
if (param.type === 'optional_parameter') info.optional = true;
|
|
99
|
-
} else if (param.type === 'rest_parameter') {
|
|
100
|
-
|
|
99
|
+
} else if (param.type === 'rest_parameter' || param.type === 'rest_pattern') {
|
|
100
|
+
// rest_parameter = TypeScript, rest_pattern = JavaScript
|
|
101
|
+
const patternNode = param.childForFieldName('pattern') || param.namedChild(0);
|
|
101
102
|
if (patternNode) info.name = patternNode.text;
|
|
102
103
|
info.rest = true;
|
|
103
104
|
} else if (param.type === 'assignment_pattern') {
|
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.
|
|
3
|
+
"version": "3.7.22",
|
|
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",
|