ucn 3.7.34 → 3.7.36

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/cli/index.js CHANGED
@@ -563,7 +563,7 @@ function runProjectCommand(rootDir, command, arg) {
563
563
  case 'related': {
564
564
  const { ok, result, error } = execute(index, 'related', { name: arg, ...flags });
565
565
  if (!ok) fail(error);
566
- printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, { showAll: flags.all, top: flags.top }));
566
+ printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, { all: flags.all, top: flags.top }));
567
567
  break;
568
568
  }
569
569
 
@@ -1421,7 +1421,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1421
1421
  case 'related': {
1422
1422
  const { ok, result, error } = execute(index, 'related', { name: arg, ...iflags });
1423
1423
  if (!ok) { console.log(error); return; }
1424
- console.log(output.formatRelated(result, { showAll: iflags.all, top: iflags.top }));
1424
+ console.log(output.formatRelated(result, { all: iflags.all, top: iflags.top }));
1425
1425
  break;
1426
1426
  }
1427
1427
 
package/core/output.js CHANGED
@@ -959,6 +959,11 @@ function formatImpact(impact, options = {}) {
959
959
  }
960
960
  }
961
961
 
962
+ // Scope pollution warning
963
+ if (impact.scopeWarning) {
964
+ lines.push(` Note: ${impact.scopeWarning.hint}`);
965
+ }
966
+
962
967
  // By file
963
968
  lines.push('');
964
969
  lines.push('BY FILE:');
@@ -1019,6 +1024,9 @@ function formatPlan(plan, options = {}) {
1019
1024
  // Summary
1020
1025
  lines.push(`CHANGES NEEDED: ${plan.totalChanges}`);
1021
1026
  lines.push(` Files affected: ${plan.filesAffected}`);
1027
+ if (plan.scopeWarning) {
1028
+ lines.push(` Note: ${plan.scopeWarning.hint}`);
1029
+ }
1022
1030
  lines.push('');
1023
1031
 
1024
1032
  // Group by file
@@ -1124,6 +1132,9 @@ function formatVerify(result, options = {}) {
1124
1132
  lines.push(` Valid: ${result.valid}`);
1125
1133
  lines.push(` Mismatches: ${result.mismatches}`);
1126
1134
  lines.push(` Uncertain: ${result.uncertain}`);
1135
+ if (result.scopeWarning) {
1136
+ lines.push(` Note: ${result.scopeWarning.hint}`);
1137
+ }
1127
1138
 
1128
1139
  // Show mismatches
1129
1140
  if (result.mismatchDetails.length > 0) {
package/core/project.js CHANGED
@@ -617,13 +617,24 @@ class ProjectIndex {
617
617
  }
618
618
  }
619
619
 
620
- // Check inclusion (must be within specified directory, path-boundary-aware)
620
+ // Check inclusion (directory or file path)
621
621
  if (filters.in) {
622
- const inPattern = filters.in;
623
- // Match at path boundaries: start of string or after /
624
- // e.g. --in=src matches "src/foo.js" and "lib/src/foo.js" but NOT "my-src-backup/foo.js"
625
- if (!(filePath.startsWith(inPattern + '/') || filePath.includes('/' + inPattern + '/'))) {
626
- return false;
622
+ const inPattern = filters.in.replace(/\/$/, ''); // strip trailing slash
623
+ // Detect if pattern looks like a file path (has an extension)
624
+ const looksLikeFile = /\.\w+$/.test(inPattern);
625
+ if (looksLikeFile) {
626
+ // File path matching: exact match or suffix match
627
+ // e.g. --in=tools/analyzer.py matches "tools/analyzer.py"
628
+ // e.g. --in=analyzer.py matches "tools/analyzer.py"
629
+ if (!(filePath === inPattern || filePath.endsWith('/' + inPattern))) {
630
+ return false;
631
+ }
632
+ } else {
633
+ // Directory matching: path-boundary-aware
634
+ // e.g. --in=src matches "src/foo.js" and "lib/src/foo.js" but NOT "my-src-backup/foo.js"
635
+ if (!(filePath.startsWith(inPattern + '/') || filePath.includes('/' + inPattern + '/'))) {
636
+ return false;
637
+ }
627
638
  }
628
639
  }
629
640
 
@@ -1694,25 +1705,55 @@ class ProjectIndex {
1694
1705
 
1695
1706
  /**
1696
1707
  * Extract type names from a function definition
1708
+ * Finds all word-like type identifiers from param types, return type,
1709
+ * class membership, and function body — filters to project-defined types only.
1697
1710
  */
1698
1711
  extractTypeNames(def) {
1712
+ const TYPE_KINDS = ['type', 'interface', 'class', 'struct'];
1699
1713
  const types = new Set();
1700
1714
 
1701
- // From params
1715
+ const addIfType = (name) => {
1716
+ const syms = this.symbols.get(name);
1717
+ if (syms && syms.some(s => TYPE_KINDS.includes(s.type))) {
1718
+ types.add(name);
1719
+ }
1720
+ };
1721
+
1722
+ // 1. From param and return type annotations
1723
+ const typeStrings = [];
1702
1724
  if (def.paramsStructured) {
1703
1725
  for (const param of def.paramsStructured) {
1704
- if (param.type) {
1705
- // Extract base type name (before < or [)
1706
- const match = param.type.match(/^([A-Z]\w*)/);
1707
- if (match) types.add(match[1]);
1708
- }
1726
+ if (param.type) typeStrings.push(param.type);
1727
+ }
1728
+ }
1729
+ if (def.returnType) typeStrings.push(def.returnType);
1730
+ for (const ts of typeStrings) {
1731
+ const matches = ts.match(/\b([A-Za-z_]\w*)\b/g);
1732
+ if (matches) {
1733
+ for (const m of matches) addIfType(m);
1709
1734
  }
1710
1735
  }
1711
1736
 
1712
- // From return type
1713
- if (def.returnType) {
1714
- const match = def.returnType.match(/^([A-Z]\w*)/);
1715
- if (match) types.add(match[1]);
1737
+ // 2. From the class the method belongs to
1738
+ if (def.className) addIfType(def.className);
1739
+
1740
+ // 3. From function body — scan for project-defined type references
1741
+ // (constructors, type annotations, isinstance checks)
1742
+ if (types.size === 0) {
1743
+ const code = this.extractCode(def);
1744
+ if (code) {
1745
+ // Find capitalized identifiers that match project types
1746
+ const bodyMatches = code.match(/\b([A-Z][A-Za-z0-9_]*)\b/g);
1747
+ if (bodyMatches) {
1748
+ const seen = new Set();
1749
+ for (const m of bodyMatches) {
1750
+ if (!seen.has(m)) {
1751
+ seen.add(m);
1752
+ addIfType(m);
1753
+ }
1754
+ }
1755
+ }
1756
+ }
1716
1757
  }
1717
1758
 
1718
1759
  return types;
@@ -2716,6 +2757,24 @@ class ProjectIndex {
2716
2757
  // Identify patterns
2717
2758
  const patterns = this.identifyCallPatterns(filteredSites, name);
2718
2759
 
2760
+ // Detect scope pollution: multiple class definitions for the same method name
2761
+ let scopeWarning = null;
2762
+ if (defIsMethod) {
2763
+ const allDefs = this.symbols.get(name);
2764
+ if (allDefs && allDefs.length > 1) {
2765
+ const classNames = [...new Set(allDefs
2766
+ .filter(d => d.className && d.className !== def.className)
2767
+ .map(d => d.className))];
2768
+ if (classNames.length > 0) {
2769
+ scopeWarning = {
2770
+ targetClass: def.className || '(unknown)',
2771
+ otherClasses: classNames,
2772
+ hint: `Results may include calls to ${classNames.join(', ')}.${name}(). Use file= or className= to narrow scope.`
2773
+ };
2774
+ }
2775
+ }
2776
+ }
2777
+
2719
2778
  return {
2720
2779
  function: name,
2721
2780
  file: def.relativePath,
@@ -2730,7 +2789,8 @@ class ProjectIndex {
2730
2789
  count: sites.length,
2731
2790
  sites
2732
2791
  })),
2733
- patterns
2792
+ patterns,
2793
+ scopeWarning
2734
2794
  };
2735
2795
  } finally { this._endOp(); }
2736
2796
  }
@@ -2878,13 +2938,17 @@ class ProjectIndex {
2878
2938
 
2879
2939
  // Get type definitions if requested
2880
2940
  let types = [];
2881
- if (options.withTypes && (primary.params !== undefined || primary.returnType)) {
2882
- const typeNames = this.extractTypeNames(primary);
2883
- for (const typeName of typeNames) {
2941
+ if (options.withTypes) {
2942
+ const TYPE_KINDS = ['type', 'interface', 'class', 'struct'];
2943
+ const seen = new Set();
2944
+
2945
+ const addType = (typeName) => {
2946
+ if (seen.has(typeName)) return;
2947
+ seen.add(typeName);
2884
2948
  const typeSymbols = this.symbols.get(typeName);
2885
2949
  if (typeSymbols) {
2886
2950
  for (const sym of typeSymbols) {
2887
- if (['type', 'interface', 'class', 'struct'].includes(sym.type)) {
2951
+ if (TYPE_KINDS.includes(sym.type)) {
2888
2952
  types.push({
2889
2953
  name: sym.name,
2890
2954
  type: sym.type,
@@ -2894,6 +2958,18 @@ class ProjectIndex {
2894
2958
  }
2895
2959
  }
2896
2960
  }
2961
+ };
2962
+
2963
+ // From signature annotations
2964
+ const typeNames = this.extractTypeNames(primary);
2965
+ for (const typeName of typeNames) addType(typeName);
2966
+
2967
+ // From callee signatures — types used by functions this function calls
2968
+ if (allCallees) {
2969
+ for (const callee of allCallees) {
2970
+ const calleeTypeNames = this.extractTypeNames(callee);
2971
+ for (const tn of calleeTypeNames) addType(tn);
2972
+ }
2897
2973
  }
2898
2974
  }
2899
2975
 
package/core/verify.js CHANGED
@@ -278,6 +278,24 @@ function verify(index, name, options = {}) {
278
278
  }
279
279
  clearTreeCache(index);
280
280
 
281
+ // Detect scope pollution for methods
282
+ let scopeWarning = null;
283
+ if (defIsMethod) {
284
+ const allDefs = index.symbols.get(name);
285
+ if (allDefs && allDefs.length > 1) {
286
+ const classNames = [...new Set(allDefs
287
+ .filter(d => d.className && d.className !== def.className)
288
+ .map(d => d.className))];
289
+ if (classNames.length > 0) {
290
+ scopeWarning = {
291
+ targetClass: def.className || '(unknown)',
292
+ otherClasses: classNames,
293
+ hint: `Results may include calls to ${classNames.join(', ')}.${name}(). Use file= or className= to narrow scope.`
294
+ };
295
+ }
296
+ }
297
+ }
298
+
281
299
  return {
282
300
  found: true,
283
301
  function: name,
@@ -295,7 +313,8 @@ function verify(index, name, options = {}) {
295
313
  mismatches: mismatches.length,
296
314
  uncertain: uncertain.length,
297
315
  mismatchDetails: mismatches,
298
- uncertainDetails: uncertain
316
+ uncertainDetails: uncertain,
317
+ scopeWarning
299
318
  };
300
319
  } finally { index._endOp(); }
301
320
  }
@@ -482,7 +501,8 @@ function plan(index, name, options = {}) {
482
501
  },
483
502
  totalChanges: changes.length,
484
503
  filesAffected: new Set(changes.map(c => c.file)).size,
485
- changes
504
+ changes,
505
+ scopeWarning: impact?.scopeWarning || null
486
506
  };
487
507
  } finally { index._endOp(); }
488
508
  }
@@ -212,6 +212,54 @@ function findFunctions(code, parser) {
212
212
  ...(docstring && { docstring })
213
213
  });
214
214
  }
215
+
216
+ // React wrapper patterns: React.forwardRef(...), React.memo(...), forwardRef(...), memo(...)
217
+ // const Button = React.forwardRef<Props, Ref>((props, ref) => ...)
218
+ // const Memoized = memo((props) => ...)
219
+ if (!isArrow && !isFnExpr && valueNode.type === 'call_expression') {
220
+ const funcNode = valueNode.childForFieldName('function');
221
+ if (funcNode) {
222
+ let wrapperName = null;
223
+ if (funcNode.type === 'member_expression') {
224
+ const prop = funcNode.childForFieldName('property');
225
+ wrapperName = prop?.text;
226
+ } else if (funcNode.type === 'identifier') {
227
+ wrapperName = funcNode.text;
228
+ }
229
+ if (wrapperName === 'forwardRef' || wrapperName === 'memo') {
230
+ const argsNode = valueNode.childForFieldName('arguments');
231
+ if (argsNode && argsNode.namedChildCount > 0) {
232
+ const innerFn = argsNode.namedChild(0);
233
+ if (innerFn && (innerFn.type === 'arrow_function' || innerFn.type === 'function_expression')) {
234
+ processedRanges.add(rangeKey);
235
+ const paramsNode = innerFn.childForFieldName('parameters');
236
+ const { startLine, endLine, indent } = nodeToLocation(node, code);
237
+ const returnType = extractReturnType(innerFn);
238
+ const generics = extractGenerics(innerFn);
239
+ const docstring = extractJSDocstring(code, startLine);
240
+ const modifiers = node.parent && node.parent.type === 'export_statement'
241
+ ? extractModifiers(node.parent.text)
242
+ : extractModifiers(node.text);
243
+
244
+ functions.push({
245
+ name: nameNode.text,
246
+ params: extractParams(paramsNode),
247
+ paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
248
+ startLine,
249
+ endLine,
250
+ indent,
251
+ isArrow: innerFn.type === 'arrow_function',
252
+ isGenerator: false,
253
+ modifiers,
254
+ ...(returnType && { returnType }),
255
+ ...(generics && { generics }),
256
+ ...(docstring && { docstring })
257
+ });
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
215
263
  }
216
264
  }
217
265
  }
package/mcp/server.js CHANGED
@@ -336,7 +336,7 @@ server.registerTool(
336
336
  if (!ok) return toolResult(error);
337
337
  if (!result) return toolResult(`Symbol "${name}" not found.`);
338
338
  return toolResult(output.formatRelated(result, {
339
- showAll: all || false, top,
339
+ all: all || false, top,
340
340
  allHint: 'Repeat with all=true to show all.'
341
341
  }));
342
342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.34",
3
+ "version": "3.7.36",
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",