ucn 3.7.21 → 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 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 });
@@ -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
- const patternNode = param.childForFieldName('pattern');
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.21",
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",