ucn 3.7.33 → 3.7.35

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,33 @@ 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 and return type,
1709
+ * then filters to only those that exist in the project symbol table.
1697
1710
  */
1698
1711
  extractTypeNames(def) {
1699
1712
  const types = new Set();
1700
-
1701
- // From params
1713
+ // Collect all type annotation strings
1714
+ const typeStrings = [];
1702
1715
  if (def.paramsStructured) {
1703
1716
  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
- }
1717
+ if (param.type) typeStrings.push(param.type);
1709
1718
  }
1710
1719
  }
1720
+ if (def.returnType) typeStrings.push(def.returnType);
1711
1721
 
1712
- // From return type
1713
- if (def.returnType) {
1714
- const match = def.returnType.match(/^([A-Z]\w*)/);
1715
- if (match) types.add(match[1]);
1722
+ // Extract all word-like identifiers from type annotations
1723
+ // Handles: Dict[str, Any], Optional[QuoteData], str | None, List[int], CustomType
1724
+ for (const ts of typeStrings) {
1725
+ const matches = ts.match(/\b([A-Za-z_]\w*)\b/g);
1726
+ if (matches) {
1727
+ for (const m of matches) {
1728
+ // Only include names that exist as type/interface/class/struct in the symbol table
1729
+ const syms = this.symbols.get(m);
1730
+ if (syms && syms.some(s => ['type', 'interface', 'class', 'struct'].includes(s.type))) {
1731
+ types.add(m);
1732
+ }
1733
+ }
1734
+ }
1716
1735
  }
1717
1736
 
1718
1737
  return types;
@@ -2716,6 +2735,24 @@ class ProjectIndex {
2716
2735
  // Identify patterns
2717
2736
  const patterns = this.identifyCallPatterns(filteredSites, name);
2718
2737
 
2738
+ // Detect scope pollution: multiple class definitions for the same method name
2739
+ let scopeWarning = null;
2740
+ if (defIsMethod) {
2741
+ const allDefs = this.symbols.get(name);
2742
+ if (allDefs && allDefs.length > 1) {
2743
+ const classNames = [...new Set(allDefs
2744
+ .filter(d => d.className && d.className !== def.className)
2745
+ .map(d => d.className))];
2746
+ if (classNames.length > 0) {
2747
+ scopeWarning = {
2748
+ targetClass: def.className || '(unknown)',
2749
+ otherClasses: classNames,
2750
+ hint: `Results may include calls to ${classNames.join(', ')}.${name}(). Use file= or className= to narrow scope.`
2751
+ };
2752
+ }
2753
+ }
2754
+ }
2755
+
2719
2756
  return {
2720
2757
  function: name,
2721
2758
  file: def.relativePath,
@@ -2730,7 +2767,8 @@ class ProjectIndex {
2730
2767
  count: sites.length,
2731
2768
  sites
2732
2769
  })),
2733
- patterns
2770
+ patterns,
2771
+ scopeWarning
2734
2772
  };
2735
2773
  } finally { this._endOp(); }
2736
2774
  }
@@ -2878,7 +2916,7 @@ class ProjectIndex {
2878
2916
 
2879
2917
  // Get type definitions if requested
2880
2918
  let types = [];
2881
- if (options.withTypes && (primary.params !== undefined || primary.returnType)) {
2919
+ if (options.withTypes) {
2882
2920
  const typeNames = this.extractTypeNames(primary);
2883
2921
  for (const typeName of typeNames) {
2884
2922
  const typeSymbols = this.symbols.get(typeName);
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
  }
@@ -982,6 +982,30 @@ function findInstanceAttributeTypes(code, parser) {
982
982
  const initBody = child.childForFieldName('body');
983
983
  if (!initBody) continue;
984
984
 
985
+ // Build parameter type map from __init__ annotations
986
+ // e.g. def __init__(self, market: MarketDataFetcher = None) → {market: MarketDataFetcher}
987
+ const paramTypes = new Map();
988
+ const params = child.childForFieldName('parameters');
989
+ if (params) {
990
+ for (let p = 0; p < params.childCount; p++) {
991
+ const param = params.child(p);
992
+ // typed_parameter or typed_default_parameter
993
+ if (param.type === 'typed_parameter' || param.type === 'typed_default_parameter') {
994
+ const pName = param.childForFieldName('name') || param.child(0);
995
+ const pType = param.childForFieldName('type');
996
+ if (pName && pType) {
997
+ const typeIdent = pType.type === 'type' ? pType.firstChild : pType;
998
+ if (typeIdent?.type === 'identifier') {
999
+ const tn = typeIdent.text;
1000
+ if (!PRIMITIVE_TYPES.has(tn) && tn[0] >= 'A' && tn[0] <= 'Z') {
1001
+ paramTypes.set(pName.text, tn);
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+ }
1008
+
985
1009
  traverseTree(initBody, (stmt) => {
986
1010
  if (stmt.type !== 'expression_statement') return true;
987
1011
 
@@ -1004,6 +1028,9 @@ function findInstanceAttributeTypes(code, parser) {
1004
1028
  const typeName = extractConstructorName(rhs);
1005
1029
  if (typeName) {
1006
1030
  attrTypes.set(attrName, typeName);
1031
+ } else if (rhs.type === 'identifier' && paramTypes.has(rhs.text)) {
1032
+ // self.X = param where param has type annotation
1033
+ attrTypes.set(attrName, paramTypes.get(rhs.text));
1007
1034
  }
1008
1035
 
1009
1036
  return true;
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.33",
3
+ "version": "3.7.35",
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",