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 +77 -0
- package/core/execute.js +6 -1
- package/core/imports.js +26 -0
- package/core/project.js +9 -2
- package/languages/python.js +16 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
//
|
|
762
|
+
// Bare wildcard: return all symbols
|
|
763
763
|
const stripped = name.replace(/[*?]/g, '');
|
|
764
764
|
if (stripped.length === 0) {
|
|
765
|
-
|
|
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 = [];
|
package/languages/python.js
CHANGED
|
@@ -487,10 +487,25 @@ function findCallsInCode(code, parser) {
|
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
|
-
// Track non-callable assignments
|
|
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.
|
|
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",
|