ucn 3.7.27 → 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/README.md +7 -7
- package/core/callers.js +89 -1
- package/core/discovery.js +7 -0
- package/core/execute.js +6 -1
- package/core/imports.js +26 -0
- package/core/output.js +4 -0
- package/core/project.js +118 -8
- package/languages/python.js +16 -1
- package/mcp/server.js +23 -23
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -20,13 +20,13 @@ Instead of reading full files, UCN gives precise, AST-verified answers.
|
|
|
20
20
|
```bash
|
|
21
21
|
npm install -g ucn
|
|
22
22
|
|
|
23
|
-
ucn about handleRequest
|
|
24
|
-
ucn impact handleRequest
|
|
25
|
-
ucn trace main --depth=3
|
|
26
|
-
ucn deadcode
|
|
27
|
-
ucn fn handleRequest
|
|
28
|
-
ucn toc
|
|
29
|
-
ucn --interactive
|
|
23
|
+
ucn about handleRequest # full picture: definition, callers, callees, tests
|
|
24
|
+
ucn impact handleRequest # all call sites with arguments
|
|
25
|
+
ucn trace main --depth=3 # call tree, no file reads
|
|
26
|
+
ucn deadcode # unused functions, AST-verified
|
|
27
|
+
ucn fn handleRequest # extract a function without reading the file
|
|
28
|
+
ucn toc # project overview
|
|
29
|
+
ucn --interactive # REPL mode, index stays in memory
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Parses JS/TS, Python, Go, Rust, Java, and HTML with tree-sitter. Runs locally.
|
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
|
}
|
|
@@ -704,7 +727,18 @@ function findCallees(index, def, options = {}) {
|
|
|
704
727
|
}
|
|
705
728
|
}
|
|
706
729
|
}
|
|
707
|
-
// Priority 4: If default
|
|
730
|
+
// Priority 4: If default is from a bundled/minified file, prefer non-bundled
|
|
731
|
+
if (!bindingId) {
|
|
732
|
+
const calleeFileEntry = index.files.get(callee.file);
|
|
733
|
+
if (calleeFileEntry && calleeFileEntry.isBundled) {
|
|
734
|
+
const nonBundled = symbols.find(s => {
|
|
735
|
+
const fe = index.files.get(s.file);
|
|
736
|
+
return fe && !fe.isBundled;
|
|
737
|
+
});
|
|
738
|
+
if (nonBundled) callee = nonBundled;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Priority 5: If default is a test file, prefer non-test
|
|
708
742
|
if (!bindingId) {
|
|
709
743
|
const calleeFileEntry = index.files.get(callee.file);
|
|
710
744
|
if (calleeFileEntry && isTestFile(calleeFileEntry.relativePath, calleeFileEntry.language)) {
|
|
@@ -778,6 +812,60 @@ function getInstanceAttributeTypes(index, filePath, className) {
|
|
|
778
812
|
return fileCache.get(className) || null;
|
|
779
813
|
}
|
|
780
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
|
+
|
|
781
869
|
/**
|
|
782
870
|
* Check if a function is used as a callback anywhere in the codebase
|
|
783
871
|
* @param {object} index - ProjectIndex instance
|
package/core/discovery.js
CHANGED
|
@@ -30,6 +30,7 @@ const DEFAULT_IGNORES = [
|
|
|
30
30
|
|
|
31
31
|
// Build outputs
|
|
32
32
|
'dist',
|
|
33
|
+
'*-dist',
|
|
33
34
|
'build',
|
|
34
35
|
'out',
|
|
35
36
|
'.next',
|
|
@@ -38,6 +39,12 @@ const DEFAULT_IGNORES = [
|
|
|
38
39
|
'.output',
|
|
39
40
|
'.vercel',
|
|
40
41
|
'.netlify',
|
|
42
|
+
'.turbo',
|
|
43
|
+
'.parcel-cache',
|
|
44
|
+
'.svelte-kit',
|
|
45
|
+
'.docusaurus',
|
|
46
|
+
'storybook-static',
|
|
47
|
+
'_site',
|
|
41
48
|
|
|
42
49
|
// Test/coverage
|
|
43
50
|
'coverage',
|
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/output.js
CHANGED
|
@@ -613,6 +613,10 @@ function formatApi(symbols, filePath) {
|
|
|
613
613
|
|
|
614
614
|
if (symbols.length === 0) {
|
|
615
615
|
lines.push(' (none found)');
|
|
616
|
+
if (filePath && filePath.endsWith('.py')) {
|
|
617
|
+
lines.push('');
|
|
618
|
+
lines.push('Note: Python requires __all__ for export detection. Use \'toc\' command to see all functions/classes.');
|
|
619
|
+
}
|
|
616
620
|
} else {
|
|
617
621
|
// Group by file
|
|
618
622
|
const byFile = new Map();
|
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 = [];
|
|
@@ -1477,42 +1484,145 @@ class ProjectIndex {
|
|
|
1477
1484
|
*/
|
|
1478
1485
|
isKeyword(name, language) {
|
|
1479
1486
|
if (!LANGUAGE_KEYWORDS) {
|
|
1480
|
-
// Initialize on first use
|
|
1487
|
+
// Initialize on first use — includes both keywords AND builtins
|
|
1488
|
+
// to prevent cross-language false positives (e.g. Python set() → JS bundle)
|
|
1481
1489
|
LANGUAGE_KEYWORDS = {
|
|
1482
1490
|
javascript: new Set([
|
|
1491
|
+
// Keywords
|
|
1483
1492
|
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
|
|
1484
1493
|
'continue', 'return', 'function', 'class', 'const', 'let', 'var',
|
|
1485
1494
|
'new', 'this', 'super', 'import', 'export', 'default', 'from',
|
|
1486
1495
|
'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
|
|
1487
|
-
'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'with'
|
|
1496
|
+
'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'with',
|
|
1497
|
+
// Global builtins
|
|
1498
|
+
'undefined', 'NaN', 'Infinity', 'globalThis',
|
|
1499
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
1500
|
+
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
|
|
1501
|
+
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
1502
|
+
'console', 'JSON', 'Math', 'Date', 'RegExp',
|
|
1503
|
+
'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
|
|
1504
|
+
'Map', 'Set', 'WeakMap', 'WeakSet', 'WeakRef',
|
|
1505
|
+
'Promise', 'Proxy', 'Reflect',
|
|
1506
|
+
'Error', 'TypeError', 'RangeError', 'ReferenceError', 'SyntaxError',
|
|
1507
|
+
'URIError', 'EvalError', 'AggregateError',
|
|
1508
|
+
'ArrayBuffer', 'SharedArrayBuffer', 'DataView',
|
|
1509
|
+
'Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
|
|
1510
|
+
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array',
|
|
1511
|
+
'BigInt64Array', 'BigUint64Array',
|
|
1512
|
+
'TextEncoder', 'TextDecoder', 'URL', 'URLSearchParams',
|
|
1513
|
+
'fetch', 'Request', 'Response', 'Headers',
|
|
1514
|
+
'atob', 'btoa', 'structuredClone', 'queueMicrotask',
|
|
1515
|
+
'require'
|
|
1488
1516
|
]),
|
|
1489
1517
|
python: new Set([
|
|
1518
|
+
// Keywords
|
|
1490
1519
|
'if', 'else', 'elif', 'for', 'while', 'def', 'class', 'return',
|
|
1491
1520
|
'import', 'from', 'try', 'except', 'finally', 'raise', 'async',
|
|
1492
1521
|
'await', 'yield', 'with', 'as', 'lambda', 'pass', 'break',
|
|
1493
1522
|
'continue', 'del', 'global', 'nonlocal', 'assert', 'is', 'not',
|
|
1494
|
-
'and', 'or', 'in', 'True', 'False', 'None', 'self', 'cls'
|
|
1523
|
+
'and', 'or', 'in', 'True', 'False', 'None', 'self', 'cls',
|
|
1524
|
+
// Type constructors
|
|
1525
|
+
'int', 'float', 'str', 'bool', 'list', 'dict', 'set', 'tuple',
|
|
1526
|
+
'bytes', 'bytearray', 'frozenset', 'complex', 'memoryview',
|
|
1527
|
+
'object', 'type',
|
|
1528
|
+
// Common builtins
|
|
1529
|
+
'print', 'len', 'range', 'abs', 'round', 'min', 'max', 'sum',
|
|
1530
|
+
'sorted', 'reversed', 'enumerate', 'zip', 'map', 'filter',
|
|
1531
|
+
'any', 'all', 'iter', 'next', 'hash', 'id', 'repr', 'format',
|
|
1532
|
+
'chr', 'ord', 'hex', 'oct', 'bin', 'pow', 'divmod',
|
|
1533
|
+
'input', 'open', 'super',
|
|
1534
|
+
// Introspection
|
|
1535
|
+
'isinstance', 'issubclass', 'hasattr', 'getattr', 'setattr',
|
|
1536
|
+
'delattr', 'callable', 'dir', 'vars', 'globals', 'locals', 'help',
|
|
1537
|
+
// Decorators / descriptors
|
|
1538
|
+
'property', 'staticmethod', 'classmethod',
|
|
1539
|
+
// Exception types
|
|
1540
|
+
'Exception', 'BaseException', 'ValueError', 'TypeError',
|
|
1541
|
+
'KeyError', 'IndexError', 'AttributeError', 'RuntimeError',
|
|
1542
|
+
'NotImplementedError', 'StopIteration', 'StopAsyncIteration',
|
|
1543
|
+
'GeneratorExit', 'OSError', 'IOError', 'FileNotFoundError',
|
|
1544
|
+
'FileExistsError', 'PermissionError', 'IsADirectoryError',
|
|
1545
|
+
'ImportError', 'ModuleNotFoundError', 'NameError',
|
|
1546
|
+
'UnboundLocalError', 'ZeroDivisionError', 'OverflowError',
|
|
1547
|
+
'FloatingPointError', 'ArithmeticError', 'LookupError',
|
|
1548
|
+
'RecursionError', 'MemoryError', 'SystemExit',
|
|
1549
|
+
'KeyboardInterrupt', 'AssertionError',
|
|
1550
|
+
'UnicodeError', 'UnicodeDecodeError', 'UnicodeEncodeError',
|
|
1551
|
+
'Warning', 'DeprecationWarning', 'FutureWarning',
|
|
1552
|
+
'UserWarning', 'SyntaxWarning', 'RuntimeWarning',
|
|
1553
|
+
'ConnectionError', 'TimeoutError', 'BrokenPipeError',
|
|
1554
|
+
// Other builtins
|
|
1555
|
+
'NotImplemented', 'Ellipsis', '__import__', '__name__',
|
|
1556
|
+
'__file__', '__doc__', '__all__', '__init__', '__new__',
|
|
1557
|
+
'__del__', '__repr__', '__str__', '__len__', '__iter__'
|
|
1495
1558
|
]),
|
|
1496
1559
|
go: new Set([
|
|
1560
|
+
// Keywords
|
|
1497
1561
|
'if', 'else', 'for', 'switch', 'case', 'break', 'continue',
|
|
1498
1562
|
'return', 'func', 'type', 'struct', 'interface', 'package',
|
|
1499
1563
|
'import', 'go', 'defer', 'select', 'chan', 'map', 'range',
|
|
1500
|
-
'fallthrough', 'goto', 'var', 'const', 'default'
|
|
1564
|
+
'fallthrough', 'goto', 'var', 'const', 'default',
|
|
1565
|
+
// Builtins
|
|
1566
|
+
'append', 'cap', 'close', 'copy', 'delete', 'len', 'make',
|
|
1567
|
+
'new', 'panic', 'recover', 'print', 'println', 'complex',
|
|
1568
|
+
'real', 'imag', 'clear', 'min', 'max',
|
|
1569
|
+
// Builtin types (prevent cross-language matches)
|
|
1570
|
+
'error', 'string', 'bool', 'byte', 'rune',
|
|
1571
|
+
'int', 'int8', 'int16', 'int32', 'int64',
|
|
1572
|
+
'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr',
|
|
1573
|
+
'float32', 'float64', 'complex64', 'complex128',
|
|
1574
|
+
'nil', 'true', 'false', 'iota'
|
|
1501
1575
|
]),
|
|
1502
1576
|
rust: new Set([
|
|
1577
|
+
// Keywords
|
|
1503
1578
|
'if', 'else', 'for', 'while', 'loop', 'fn', 'impl', 'pub',
|
|
1504
1579
|
'mod', 'use', 'crate', 'self', 'super', 'match', 'unsafe',
|
|
1505
1580
|
'move', 'ref', 'mut', 'where', 'let', 'const', 'struct',
|
|
1506
1581
|
'enum', 'trait', 'async', 'await', 'return', 'break',
|
|
1507
|
-
'continue', 'type', 'as', 'in', 'dyn', 'static'
|
|
1582
|
+
'continue', 'type', 'as', 'in', 'dyn', 'static',
|
|
1583
|
+
// Macros (common calls that aren't project functions)
|
|
1584
|
+
'println', 'print', 'eprintln', 'eprint', 'format',
|
|
1585
|
+
'vec', 'panic', 'assert', 'assert_eq', 'assert_ne',
|
|
1586
|
+
'debug_assert', 'debug_assert_eq', 'debug_assert_ne',
|
|
1587
|
+
'todo', 'unimplemented', 'unreachable',
|
|
1588
|
+
'cfg', 'derive', 'include', 'include_str', 'include_bytes',
|
|
1589
|
+
'env', 'concat', 'stringify', 'file', 'line', 'column',
|
|
1590
|
+
// Std prelude types/traits
|
|
1591
|
+
'Some', 'None', 'Ok', 'Err', 'Box', 'Vec', 'String',
|
|
1592
|
+
'Option', 'Result', 'Clone', 'Copy', 'Drop',
|
|
1593
|
+
'Default', 'Debug', 'Display', 'Iterator',
|
|
1594
|
+
'From', 'Into', 'TryFrom', 'TryInto',
|
|
1595
|
+
'AsRef', 'AsMut', 'Deref', 'DerefMut',
|
|
1596
|
+
'Send', 'Sync', 'Sized', 'Unpin',
|
|
1597
|
+
'Fn', 'FnMut', 'FnOnce',
|
|
1598
|
+
'PartialEq', 'Eq', 'PartialOrd', 'Ord', 'Hash'
|
|
1508
1599
|
]),
|
|
1509
1600
|
java: new Set([
|
|
1601
|
+
// Keywords
|
|
1510
1602
|
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
|
|
1511
1603
|
'continue', 'return', 'class', 'interface', 'enum', 'extends',
|
|
1512
1604
|
'implements', 'new', 'this', 'super', 'import', 'package',
|
|
1513
1605
|
'try', 'catch', 'finally', 'throw', 'throws', 'abstract',
|
|
1514
1606
|
'static', 'final', 'synchronized', 'volatile', 'transient',
|
|
1515
|
-
'native', 'void', 'instanceof', 'default'
|
|
1607
|
+
'native', 'void', 'instanceof', 'default',
|
|
1608
|
+
// Primitive types
|
|
1609
|
+
'boolean', 'byte', 'char', 'short', 'int', 'long',
|
|
1610
|
+
'float', 'double', 'null', 'true', 'false',
|
|
1611
|
+
// java.lang builtins (auto-imported)
|
|
1612
|
+
'System', 'String', 'Object', 'Class', 'Integer', 'Long',
|
|
1613
|
+
'Double', 'Float', 'Boolean', 'Character', 'Byte', 'Short',
|
|
1614
|
+
'Math', 'StringBuilder', 'StringBuffer',
|
|
1615
|
+
'Thread', 'Runnable', 'Throwable',
|
|
1616
|
+
'Exception', 'RuntimeException', 'Error',
|
|
1617
|
+
'NullPointerException', 'IllegalArgumentException',
|
|
1618
|
+
'IllegalStateException', 'IndexOutOfBoundsException',
|
|
1619
|
+
'ClassCastException', 'UnsupportedOperationException',
|
|
1620
|
+
'ArithmeticException', 'SecurityException',
|
|
1621
|
+
'StackOverflowError', 'OutOfMemoryError',
|
|
1622
|
+
'Override', 'Deprecated', 'SuppressWarnings',
|
|
1623
|
+
'FunctionalInterface', 'SafeVarargs',
|
|
1624
|
+
'Iterable', 'Comparable', 'AutoCloseable', 'Cloneable',
|
|
1625
|
+
'Enum', 'Record', 'Void'
|
|
1516
1626
|
])
|
|
1517
1627
|
};
|
|
1518
1628
|
// TypeScript/TSX share JavaScript keywords
|
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/mcp/server.js
CHANGED
|
@@ -276,7 +276,7 @@ server.registerTool(
|
|
|
276
276
|
const index = getIndex(project_dir);
|
|
277
277
|
const ep = normalizeParams({ name, file, exclude, with_types, all, include_methods, include_uncertain, top });
|
|
278
278
|
const { ok, result, error } = execute(index, 'about', ep);
|
|
279
|
-
if (!ok) return
|
|
279
|
+
if (!ok) return toolResult(error); // soft error — won't kill sibling calls
|
|
280
280
|
return toolResult(output.formatAbout(result, {
|
|
281
281
|
allHint: 'Repeat with all=true to show all.',
|
|
282
282
|
methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.'
|
|
@@ -299,7 +299,7 @@ server.registerTool(
|
|
|
299
299
|
const index = getIndex(project_dir);
|
|
300
300
|
const ep = normalizeParams({ name, file, exclude });
|
|
301
301
|
const { ok, result, error } = execute(index, 'impact', ep);
|
|
302
|
-
if (!ok) return
|
|
302
|
+
if (!ok) return toolResult(error); // soft error
|
|
303
303
|
return toolResult(output.formatImpact(result));
|
|
304
304
|
}
|
|
305
305
|
|
|
@@ -315,7 +315,7 @@ server.registerTool(
|
|
|
315
315
|
const index = getIndex(project_dir);
|
|
316
316
|
const ep = normalizeParams({ name, file, depth, all, include_methods, include_uncertain });
|
|
317
317
|
const { ok, result, error } = execute(index, 'trace', ep);
|
|
318
|
-
if (!ok) return
|
|
318
|
+
if (!ok) return toolResult(error); // soft error
|
|
319
319
|
return toolResult(output.formatTrace(result, {
|
|
320
320
|
allHint: 'Set depth to expand all children.',
|
|
321
321
|
methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
|
|
@@ -347,7 +347,7 @@ server.registerTool(
|
|
|
347
347
|
const index = getIndex(project_dir);
|
|
348
348
|
const ep = normalizeParams({ name, file, exclude, include_tests, exact, in: inPath });
|
|
349
349
|
const { ok, result, error } = execute(index, 'find', ep);
|
|
350
|
-
if (!ok) return
|
|
350
|
+
if (!ok) return toolResult(error); // soft error
|
|
351
351
|
return toolResult(output.formatFind(result, name, top));
|
|
352
352
|
}
|
|
353
353
|
|
|
@@ -355,7 +355,7 @@ server.registerTool(
|
|
|
355
355
|
const index = getIndex(project_dir);
|
|
356
356
|
const ep = normalizeParams({ name, exclude, include_tests, code_only, context: ctxLines, in: inPath });
|
|
357
357
|
const { ok, result, error } = execute(index, 'usages', ep);
|
|
358
|
-
if (!ok) return
|
|
358
|
+
if (!ok) return toolResult(error); // soft error
|
|
359
359
|
return toolResult(output.formatUsages(result, name));
|
|
360
360
|
}
|
|
361
361
|
|
|
@@ -363,7 +363,7 @@ server.registerTool(
|
|
|
363
363
|
const index = getIndex(project_dir);
|
|
364
364
|
const ep = normalizeParams({ detailed, top_level, all, top });
|
|
365
365
|
const { ok, result, error } = execute(index, 'toc', ep);
|
|
366
|
-
if (!ok) return
|
|
366
|
+
if (!ok) return toolResult(error); // soft error
|
|
367
367
|
return toolResult(output.formatToc(result, {
|
|
368
368
|
topHint: 'Set top=N or use detailed=false for compact view.'
|
|
369
369
|
}));
|
|
@@ -373,7 +373,7 @@ server.registerTool(
|
|
|
373
373
|
const index = getIndex(project_dir);
|
|
374
374
|
const ep = normalizeParams({ term, exclude, include_tests, code_only, context: ctxLines, case_sensitive, in: inPath, regex });
|
|
375
375
|
const { ok, result, error } = execute(index, 'search', ep);
|
|
376
|
-
if (!ok) return
|
|
376
|
+
if (!ok) return toolResult(error); // soft error
|
|
377
377
|
return toolResult(output.formatSearch(result, term));
|
|
378
378
|
}
|
|
379
379
|
|
|
@@ -381,7 +381,7 @@ server.registerTool(
|
|
|
381
381
|
const index = getIndex(project_dir);
|
|
382
382
|
const ep = normalizeParams({ name, calls_only });
|
|
383
383
|
const { ok, result, error } = execute(index, 'tests', ep);
|
|
384
|
-
if (!ok) return
|
|
384
|
+
if (!ok) return toolResult(error); // soft error
|
|
385
385
|
return toolResult(output.formatTests(result, name));
|
|
386
386
|
}
|
|
387
387
|
|
|
@@ -389,7 +389,7 @@ server.registerTool(
|
|
|
389
389
|
const index = getIndex(project_dir);
|
|
390
390
|
const ep = normalizeParams({ exclude, in: inPath, include_exported, include_decorated, include_tests });
|
|
391
391
|
const { ok, result, error } = execute(index, 'deadcode', ep);
|
|
392
|
-
if (!ok) return
|
|
392
|
+
if (!ok) return toolResult(error); // soft error
|
|
393
393
|
return toolResult(output.formatDeadcode(result, {
|
|
394
394
|
top: top || 0,
|
|
395
395
|
decoratedHint: !include_decorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.` : undefined,
|
|
@@ -402,28 +402,28 @@ server.registerTool(
|
|
|
402
402
|
case 'imports': {
|
|
403
403
|
const index = getIndex(project_dir);
|
|
404
404
|
const { ok, result, error } = execute(index, 'imports', { file });
|
|
405
|
-
if (!ok) return
|
|
405
|
+
if (!ok) return toolResult(error); // soft error
|
|
406
406
|
return toolResult(output.formatImports(result, file));
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
case 'exporters': {
|
|
410
410
|
const index = getIndex(project_dir);
|
|
411
411
|
const { ok, result, error } = execute(index, 'exporters', { file });
|
|
412
|
-
if (!ok) return
|
|
412
|
+
if (!ok) return toolResult(error); // soft error
|
|
413
413
|
return toolResult(output.formatExporters(result, file));
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
case 'file_exports': {
|
|
417
417
|
const index = getIndex(project_dir);
|
|
418
418
|
const { ok, result, error } = execute(index, 'fileExports', { file });
|
|
419
|
-
if (!ok) return
|
|
419
|
+
if (!ok) return toolResult(error); // soft error
|
|
420
420
|
return toolResult(output.formatFileExports(result, file));
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
case 'graph': {
|
|
424
424
|
const index = getIndex(project_dir);
|
|
425
425
|
const { ok, result, error } = execute(index, 'graph', { file, direction, depth, all });
|
|
426
|
-
if (!ok) return
|
|
426
|
+
if (!ok) return toolResult(error); // soft error
|
|
427
427
|
return toolResult(output.formatGraph(result, {
|
|
428
428
|
showAll: all || depth !== undefined,
|
|
429
429
|
maxDepth: depth ?? 2, file,
|
|
@@ -437,7 +437,7 @@ server.registerTool(
|
|
|
437
437
|
case 'verify': {
|
|
438
438
|
const index = getIndex(project_dir);
|
|
439
439
|
const { ok, result, error } = execute(index, 'verify', { name, file });
|
|
440
|
-
if (!ok) return
|
|
440
|
+
if (!ok) return toolResult(error); // soft error
|
|
441
441
|
return toolResult(output.formatVerify(result));
|
|
442
442
|
}
|
|
443
443
|
|
|
@@ -445,14 +445,14 @@ server.registerTool(
|
|
|
445
445
|
const index = getIndex(project_dir);
|
|
446
446
|
const ep = normalizeParams({ name, add_param, remove_param, rename_to, default_value, file });
|
|
447
447
|
const { ok, result, error } = execute(index, 'plan', ep);
|
|
448
|
-
if (!ok) return
|
|
448
|
+
if (!ok) return toolResult(error); // soft error
|
|
449
449
|
return toolResult(output.formatPlan(result));
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
case 'diff_impact': {
|
|
453
453
|
const index = getIndex(project_dir);
|
|
454
454
|
const { ok, result, error } = execute(index, 'diffImpact', { base, staged, file });
|
|
455
|
-
if (!ok) return
|
|
455
|
+
if (!ok) return toolResult(error); // soft error — e.g. "not a git repo"
|
|
456
456
|
return toolResult(output.formatDiffImpact(result));
|
|
457
457
|
}
|
|
458
458
|
|
|
@@ -461,28 +461,28 @@ server.registerTool(
|
|
|
461
461
|
case 'typedef': {
|
|
462
462
|
const index = getIndex(project_dir);
|
|
463
463
|
const { ok, result, error } = execute(index, 'typedef', { name, exact });
|
|
464
|
-
if (!ok) return
|
|
464
|
+
if (!ok) return toolResult(error); // soft error
|
|
465
465
|
return toolResult(output.formatTypedef(result, name));
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
case 'stacktrace': {
|
|
469
469
|
const index = getIndex(project_dir);
|
|
470
470
|
const { ok, result, error } = execute(index, 'stacktrace', { stack });
|
|
471
|
-
if (!ok) return
|
|
471
|
+
if (!ok) return toolResult(error); // soft error
|
|
472
472
|
return toolResult(output.formatStackTrace(result));
|
|
473
473
|
}
|
|
474
474
|
|
|
475
475
|
case 'api': {
|
|
476
476
|
const index = getIndex(project_dir);
|
|
477
477
|
const { ok, result, error } = execute(index, 'api', { file });
|
|
478
|
-
if (!ok) return
|
|
478
|
+
if (!ok) return toolResult(error); // soft error
|
|
479
479
|
return toolResult(output.formatApi(result, file || '.'));
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
case 'stats': {
|
|
483
483
|
const index = getIndex(project_dir);
|
|
484
484
|
const { ok, result, error } = execute(index, 'stats', { functions });
|
|
485
|
-
if (!ok) return
|
|
485
|
+
if (!ok) return toolResult(error); // soft error
|
|
486
486
|
return toolResult(output.formatStats(result, { top: top || 0 }));
|
|
487
487
|
}
|
|
488
488
|
|
|
@@ -494,7 +494,7 @@ server.registerTool(
|
|
|
494
494
|
const index = getIndex(project_dir);
|
|
495
495
|
const ep = normalizeParams({ name, file, all });
|
|
496
496
|
const { ok, result, error } = execute(index, 'fn', ep);
|
|
497
|
-
if (!ok) return
|
|
497
|
+
if (!ok) return toolResult(error); // soft error
|
|
498
498
|
// MCP path security: validate all result files are within project root
|
|
499
499
|
for (const entry of result.entries) {
|
|
500
500
|
const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
|
|
@@ -527,7 +527,7 @@ server.registerTool(
|
|
|
527
527
|
const index = getIndex(project_dir);
|
|
528
528
|
const ep = normalizeParams({ file, range });
|
|
529
529
|
const { ok, result, error } = execute(index, 'lines', ep);
|
|
530
|
-
if (!ok) return
|
|
530
|
+
if (!ok) return toolResult(error); // soft error
|
|
531
531
|
// MCP path security: validate file is within project root
|
|
532
532
|
const check = resolveAndValidatePath(index, result.relativePath);
|
|
533
533
|
if (typeof check !== 'string') return check;
|
|
@@ -545,7 +545,7 @@ server.registerTool(
|
|
|
545
545
|
itemCount: lookup.itemCount, symbolName: lookup.symbolName,
|
|
546
546
|
validateRoot: true
|
|
547
547
|
});
|
|
548
|
-
if (!ok) return
|
|
548
|
+
if (!ok) return toolResult(error); // soft error
|
|
549
549
|
return toolResult(result.text);
|
|
550
550
|
}
|
|
551
551
|
|
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",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"ucn-mcp": "mcp/server.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/accuracy.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js"
|
|
12
|
+
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/accuracy.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js",
|
|
13
|
+
"benchmark:agent": "node test/agent-understanding-benchmark.js"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"mcp",
|