ucn 3.7.21 → 3.7.23

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
@@ -754,10 +754,13 @@ function formatTrace(trace, options = {}) {
754
754
  if (node.callCount) {
755
755
  label += ` ${node.callCount}x`;
756
756
  }
757
+ if (node.alreadyShown) {
758
+ label += ' (see above)';
759
+ }
757
760
 
758
761
  lines.push(prefix + connector + label);
759
762
 
760
- if (node.children) {
763
+ if (node.children && !node.alreadyShown) {
761
764
  const hasMore = node.truncatedChildren > 0;
762
765
  for (let i = 0; i < node.children.length; i++) {
763
766
  const isChildLast = !hasMore && i === node.children.length - 1;
package/core/project.js CHANGED
@@ -3007,10 +3007,30 @@ class ProjectIndex {
3007
3007
  // Filter out usages that are at the definition location
3008
3008
  // nameLine: when decorators/annotations are present, startLine is the decorator line
3009
3009
  // but the name identifier is on a different line (nameLine). Check both.
3010
- const nonDefUsages = allUsages.filter(u =>
3010
+ let nonDefUsages = allUsages.filter(u =>
3011
3011
  !(u.file === symbol.file && (u.line === symbol.startLine || u.line === symbol.nameLine))
3012
3012
  );
3013
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
+
3014
3034
  // Total includes all usage types (calls, references, callbacks, re-exports)
3015
3035
  const totalUsages = nonDefUsages.length;
3016
3036
 
@@ -3376,10 +3396,22 @@ class ProjectIndex {
3376
3396
 
3377
3397
  const buildTree = (funcDef, currentDepth, dir) => {
3378
3398
  const funcName = funcDef.name;
3379
- if (currentDepth > maxDepth || visited.has(`${funcDef.file}:${funcDef.startLine}`)) {
3399
+ const key = `${funcDef.file}:${funcDef.startLine}`;
3400
+ if (currentDepth > maxDepth) {
3380
3401
  return null;
3381
3402
  }
3382
- visited.add(`${funcDef.file}:${funcDef.startLine}`);
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);
3383
3415
 
3384
3416
  const node = {
3385
3417
  name: funcName,
@@ -4011,10 +4043,12 @@ class ProjectIndex {
4011
4043
  params = params.slice(1);
4012
4044
  }
4013
4045
  }
4014
- const expectedParamCount = params.length;
4015
- const optionalCount = params.filter(p => p.optional || p.default !== undefined).length;
4016
- const minArgs = expectedParamCount - optionalCount;
4017
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;
4018
4052
 
4019
4053
  // Get all call sites
4020
4054
  const usages = this.usages(name, { codeOnly: true });
@@ -81,6 +81,13 @@ function parseStructuredParams(paramsNode, language) {
81
81
 
82
82
  if (paramInfo.name) {
83
83
  params.push(paramInfo);
84
+ // Go multi-name declarations: `a, b int` → expand additional params
85
+ if (paramInfo._additionalNames) {
86
+ for (const extraName of paramInfo._additionalNames) {
87
+ params.push({ name: extraName, type: paramInfo.type });
88
+ }
89
+ delete paramInfo._additionalNames;
90
+ }
84
91
  }
85
92
  }
86
93
 
@@ -96,8 +103,9 @@ function parseJSParam(param, info) {
96
103
  if (patternNode) info.name = patternNode.text;
97
104
  if (typeNode) info.type = typeNode.text.replace(/^:\s*/, '');
98
105
  if (param.type === 'optional_parameter') info.optional = true;
99
- } else if (param.type === 'rest_parameter') {
100
- const patternNode = param.childForFieldName('pattern');
106
+ } else if (param.type === 'rest_parameter' || param.type === 'rest_pattern') {
107
+ // rest_parameter = TypeScript, rest_pattern = JavaScript
108
+ const patternNode = param.childForFieldName('pattern') || param.namedChild(0);
101
109
  if (patternNode) info.name = patternNode.text;
102
110
  info.rest = true;
103
111
  } else if (param.type === 'assignment_pattern') {
@@ -105,6 +113,9 @@ function parseJSParam(param, info) {
105
113
  const rightNode = param.childForFieldName('right');
106
114
  if (leftNode) info.name = leftNode.text;
107
115
  if (rightNode) info.default = rightNode.text;
116
+ } else if (param.type === 'object_pattern' || param.type === 'array_pattern') {
117
+ // Destructured params: { name, value } or [a, b]
118
+ info.name = param.text;
108
119
  }
109
120
  }
110
121
 
@@ -130,10 +141,30 @@ function parsePythonParam(param, info) {
130
141
 
131
142
  function parseGoParam(param, info) {
132
143
  if (param.type === 'parameter_declaration') {
144
+ const typeNode = param.childForFieldName('type');
145
+ // Go allows multiple names per declaration: `a, b int`
146
+ // Collect all identifier children (names come before the type)
147
+ const names = [];
148
+ for (let i = 0; i < param.namedChildCount; i++) {
149
+ const child = param.namedChild(i);
150
+ if (child && child.type === 'identifier') {
151
+ names.push(child.text);
152
+ }
153
+ }
154
+ if (names.length > 0) info.name = names[0];
155
+ if (typeNode) info.type = typeNode.text;
156
+ // Store additional names for multi-param declarations (handled by parseStructuredParams)
157
+ if (names.length > 1) {
158
+ info._additionalNames = names.slice(1);
159
+ }
160
+ } else if (param.type === 'variadic_parameter_declaration') {
161
+ // Go variadic: `args ...int`
133
162
  const nameNode = param.childForFieldName('name');
134
163
  const typeNode = param.childForFieldName('type');
135
164
  if (nameNode) info.name = nameNode.text;
136
- if (typeNode) info.type = typeNode.text;
165
+ else info.name = '...';
166
+ if (typeNode) info.type = '...' + typeNode.text;
167
+ info.rest = true;
137
168
  }
138
169
  }
139
170
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.21",
3
+ "version": "3.7.23",
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",