ucn 3.7.28 → 3.7.29

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/callers.js CHANGED
@@ -386,6 +386,13 @@ function findCallees(index, def, options = {}) {
386
386
  let selfAttrCalls = null; // collected for Python self.attr.method() resolution
387
387
  let selfMethodCalls = null; // collected for Python self.method() resolution
388
388
 
389
+ // Build local variable type map for receiver resolution
390
+ // Scans for patterns like: bt = Backtester(...) → bt maps to Backtester
391
+ let localTypes = null;
392
+ if (language === 'python' || language === 'javascript') {
393
+ localTypes = _buildLocalTypeMap(index, def, calls);
394
+ }
395
+
389
396
  for (const call of calls) {
390
397
  // Filter to calls within this function's scope
391
398
  // Method 1: Direct match via enclosingFunction (fast path for direct calls)
@@ -412,6 +419,22 @@ function findCallees(index, def, options = {}) {
412
419
  // self.method() / cls.method() / this.method() — resolve to same-class method below
413
420
  } else if (call.receiver === 'super') {
414
421
  // super().method() — resolve to parent class method below
422
+ } else if (localTypes && localTypes.has(call.receiver)) {
423
+ // Resolve method calls on locally-constructed objects:
424
+ // bt = Backtester(...); bt.run_backtest() → Backtester.run_backtest
425
+ const className = localTypes.get(call.receiver);
426
+ const symbols = index.symbols.get(call.name);
427
+ const match = symbols?.find(s => s.className === className);
428
+ if (match) {
429
+ const key = match.bindingId || `${className}.${call.name}`;
430
+ const existing = callees.get(key);
431
+ if (existing) {
432
+ existing.count += 1;
433
+ } else {
434
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1 });
435
+ }
436
+ }
437
+ continue;
415
438
  } else if (language !== 'go' && language !== 'java' && language !== 'rust' && !options.includeMethods) {
416
439
  continue;
417
440
  }
@@ -789,6 +812,60 @@ function getInstanceAttributeTypes(index, filePath, className) {
789
812
  return fileCache.get(className) || null;
790
813
  }
791
814
 
815
+ /**
816
+ * Build a local variable type map for a function body.
817
+ * Scans for constructor-call assignments: var = ClassName(...)
818
+ * Returns Map<varName, className> or null if none found.
819
+ * @param {object} index - ProjectIndex instance
820
+ * @param {object} def - Function definition with file, startLine, endLine
821
+ * @param {Array} calls - Cached call sites for the file
822
+ */
823
+ function _buildLocalTypeMap(index, def, calls) {
824
+ let content;
825
+ try {
826
+ content = index._readFile(def.file);
827
+ } catch {
828
+ return null;
829
+ }
830
+ const lines = content.split('\n');
831
+ const localTypes = new Map();
832
+
833
+ for (const call of calls) {
834
+ // Only look at calls within this function's scope
835
+ if (call.line < def.startLine || call.line > def.endLine) continue;
836
+ // Only direct calls (not method calls) — these are potential constructors
837
+ if (call.isMethod || call.isPotentialCallback) continue;
838
+
839
+ // Check if this call's name corresponds to a class in the symbol table
840
+ const symbols = index.symbols.get(call.name);
841
+ if (!symbols) continue;
842
+ const isClass = symbols.some(s => NON_CALLABLE_TYPES.has(s.type));
843
+ if (!isClass) continue;
844
+
845
+ // Check the source line for assignment pattern: var = ClassName(...)
846
+ const sourceLine = lines[call.line - 1];
847
+ if (!sourceLine) continue;
848
+
849
+ // Match: identifier = ClassName(...) or identifier: Type = ClassName(...)
850
+ const escapedName = call.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
851
+ const assignMatch = sourceLine.match(
852
+ new RegExp(`(\\w+)\\s*(?::\\s*\\w+)?\\s*=\\s*${escapedName}\\s*\\(`)
853
+ );
854
+ if (assignMatch) {
855
+ localTypes.set(assignMatch[1], call.name);
856
+ }
857
+ // Match: with ClassName(...) as identifier:
858
+ const withMatch = sourceLine.match(
859
+ new RegExp(`with\\s+${escapedName}\\s*\\([^)]*\\)\\s+as\\s+(\\w+)`)
860
+ );
861
+ if (withMatch) {
862
+ localTypes.set(withMatch[1], call.name);
863
+ }
864
+ }
865
+
866
+ return localTypes.size > 0 ? localTypes : null;
867
+ }
868
+
792
869
  /**
793
870
  * Check if a function is used as a callback anywhere in the codebase
794
871
  * @param {object} index - ProjectIndex instance
package/core/execute.js CHANGED
@@ -184,7 +184,12 @@ const HANDLERS = {
184
184
  find: (index, p) => {
185
185
  const err = requireName(p.name);
186
186
  if (err) return { ok: false, error: err };
187
- const exclude = applyTestExclusions(p.exclude, p.includeTests);
187
+ // Auto-include tests when pattern clearly targets test functions
188
+ let includeTests = p.includeTests;
189
+ if (!includeTests && p.name && /^test[_*?]/i.test(p.name)) {
190
+ includeTests = true;
191
+ }
192
+ const exclude = applyTestExclusions(p.exclude, includeTests);
188
193
  const result = index.find(p.name, {
189
194
  file: p.file,
190
195
  exact: p.exact || false,
package/core/imports.js CHANGED
@@ -507,6 +507,32 @@ function loadTsConfig(tsconfigPath, visited) {
507
507
  }
508
508
  }
509
509
 
510
+ // Follow project references to collect paths from referenced configs
511
+ // (tsconfig.json with "references" pointing to tsconfig.app.json, tsconfig.node.json, etc.)
512
+ if (config.references && Array.isArray(config.references)) {
513
+ for (const ref of config.references) {
514
+ if (!ref.path) continue;
515
+ let refPath = path.resolve(configDir, ref.path);
516
+ // If reference points to a directory, look for tsconfig.json inside it
517
+ if (fs.existsSync(refPath) && fs.statSync(refPath).isDirectory()) {
518
+ refPath = path.join(refPath, 'tsconfig.json');
519
+ }
520
+ // Add .json extension if not present
521
+ if (!refPath.endsWith('.json')) refPath += '.json';
522
+ if (fs.existsSync(refPath)) {
523
+ try {
524
+ const refResult = loadTsConfig(refPath, visited);
525
+ if (refResult) {
526
+ basePaths = { ...basePaths, ...refResult.paths };
527
+ if (refResult.baseUrl && !baseUrl) baseUrl = refResult.baseUrl;
528
+ }
529
+ } catch {
530
+ // Skip malformed reference config
531
+ }
532
+ }
533
+ }
534
+ }
535
+
510
536
  // Child config values override base config
511
537
  const mergedPaths = { ...basePaths, ...(config.compilerOptions?.paths || {}) };
512
538
  const compiledPaths = Object.entries(mergedPaths).map(([pattern, targets]) => ({
package/core/project.js CHANGED
@@ -759,10 +759,17 @@ class ProjectIndex {
759
759
  // Glob pattern matching (e.g., _update*, handle*Request, get?ata)
760
760
  const isGlob = name.includes('*') || name.includes('?');
761
761
  if (isGlob && !options.exact) {
762
- // Guard against bare wildcards that would match everything
762
+ // Bare wildcard: return all symbols
763
763
  const stripped = name.replace(/[*?]/g, '');
764
764
  if (stripped.length === 0) {
765
- return [];
765
+ const all = [];
766
+ for (const [, symbols] of this.symbols) {
767
+ for (const sym of symbols) {
768
+ all.push({ ...sym, _fuzzyScore: 800 });
769
+ }
770
+ }
771
+ all.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
772
+ return this._applyFindFilters(all, options);
766
773
  }
767
774
  const globRegex = new RegExp('^' + name.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
768
775
  const matches = [];
@@ -487,10 +487,25 @@ function findCallsInCode(code, parser) {
487
487
  }
488
488
  }
489
489
  }
490
- // Track non-callable assignments: count = 5, name = "hello"
490
+ // Track non-callable assignments.
491
+ // First: explicit literal check (handles dicts-with-lambdas correctly)
491
492
  if (right && isNonCallableInit(right)) {
492
493
  nonCallableNames.add(left.text);
493
494
  }
495
+ // Second: function call results are generally non-callable data
496
+ // (e.g., close = series.dropna(), result = db.query(...))
497
+ // Exception: partial() already handled above via alias tracking.
498
+ else if (right?.type === 'call' && !aliases.has(left.text)) {
499
+ nonCallableNames.add(left.text);
500
+ }
501
+ // Third: subscript/attribute access results are non-callable data
502
+ // (e.g., close = candles["close"].values, item = data[0])
503
+ else if (right && !aliases.has(left.text) &&
504
+ ['subscript', 'attribute', 'binary_operator', 'comparison_operator',
505
+ 'unary_operator', 'conditional_expression', 'await',
506
+ 'parenthesized_expression', 'not_operator', 'boolean_operator'].includes(right.type)) {
507
+ nonCallableNames.add(left.text);
508
+ }
494
509
  }
495
510
  }
496
511
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.28",
3
+ "version": "3.7.29",
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",