ucn 3.7.36 → 3.7.38
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 +10 -3
- package/core/deadcode.js +38 -0
- package/core/execute.js +1 -0
- package/core/output.js +4 -0
- package/core/project.js +176 -64
- package/core/registry.js +1 -0
- package/core/stacktrace.js +31 -9
- package/core/verify.js +58 -7
- package/mcp/server.js +11 -8
- package/package.json +1 -1
package/core/callers.js
CHANGED
|
@@ -140,7 +140,12 @@ function findCallers(index, name, options = {}) {
|
|
|
140
140
|
// Resolve binding within this file (without mutating cached call objects)
|
|
141
141
|
let bindingId = call.bindingId;
|
|
142
142
|
let isUncertain = call.uncertain;
|
|
143
|
-
|
|
143
|
+
// Skip binding resolution for method calls with non-self/this/cls receivers:
|
|
144
|
+
// e.g., analyzer.analyze_instrument() should NOT resolve to a local
|
|
145
|
+
// standalone function def `analyze_instrument` — they're different symbols.
|
|
146
|
+
const selfReceivers = new Set(['self', 'cls', 'this', 'super']);
|
|
147
|
+
const skipLocalBinding = call.isMethod && call.receiver && !selfReceivers.has(call.receiver);
|
|
148
|
+
if (!bindingId && !skipLocalBinding) {
|
|
144
149
|
let bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
|
|
145
150
|
// For Go, also check sibling files in same directory (same package scope)
|
|
146
151
|
if (bindings.length === 0 && fileEntry.language === 'go') {
|
|
@@ -615,7 +620,8 @@ function findCallees(index, def, options = {}) {
|
|
|
615
620
|
}
|
|
616
621
|
|
|
617
622
|
// Second pass: resolve Python self.attr.method() calls
|
|
618
|
-
|
|
623
|
+
// Respect includeMethods=false — skip self/this method resolution entirely
|
|
624
|
+
if (selfAttrCalls && def.className && options.includeMethods !== false) {
|
|
619
625
|
const attrTypes = getInstanceAttributeTypes(index, def.file, def.className);
|
|
620
626
|
if (attrTypes) {
|
|
621
627
|
for (const call of selfAttrCalls) {
|
|
@@ -646,7 +652,8 @@ function findCallees(index, def, options = {}) {
|
|
|
646
652
|
|
|
647
653
|
// Third pass: resolve self/this/super.method() calls to same-class or parent methods
|
|
648
654
|
// Falls back to walking the inheritance chain if not found in same class
|
|
649
|
-
|
|
655
|
+
// Respect includeMethods=false — skip self/this method resolution entirely
|
|
656
|
+
if (selfMethodCalls && def.className && options.includeMethods !== false) {
|
|
650
657
|
for (const call of selfMethodCalls) {
|
|
651
658
|
const symbols = index.symbols.get(call.name);
|
|
652
659
|
if (!symbols) continue;
|
package/core/deadcode.js
CHANGED
|
@@ -52,6 +52,44 @@ function buildUsageIndex(index) {
|
|
|
52
52
|
node.type === 'shorthand_property_identifier' ||
|
|
53
53
|
node.type === 'shorthand_property_identifier_pattern' ||
|
|
54
54
|
node.type === 'field_identifier') {
|
|
55
|
+
// Skip property_identifier/field_identifier when they're:
|
|
56
|
+
// 1. The property part of a member expression (e.g., obj.Separator)
|
|
57
|
+
// 2. An object literal key (e.g., { Separator: value })
|
|
58
|
+
// These are NOT references to standalone symbols.
|
|
59
|
+
// Shorthand properties ({ Separator }) use shorthand_property_identifier instead.
|
|
60
|
+
if ((node.type === 'property_identifier' || node.type === 'field_identifier') && node.parent) {
|
|
61
|
+
const parentType = node.parent.type;
|
|
62
|
+
// Object literal key: { Separator: 'value' } — not a reference
|
|
63
|
+
if (parentType === 'pair' || parentType === 'key_value_pair' ||
|
|
64
|
+
parentType === 'dictionary_entry' || parentType === 'field_declaration') {
|
|
65
|
+
// Check if this is the key (first child) of the pair
|
|
66
|
+
const firstChild = node.parent.child(0);
|
|
67
|
+
if (firstChild === node) {
|
|
68
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
69
|
+
traverse(node.child(i));
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Member expression property: obj.Separator — not a standalone reference
|
|
75
|
+
if (parentType === 'member_expression' ||
|
|
76
|
+
parentType === 'field_expression' ||
|
|
77
|
+
parentType === 'member_access_expression' ||
|
|
78
|
+
parentType === 'selector_expression' || // Go
|
|
79
|
+
parentType === 'field_access_expression' || // Rust
|
|
80
|
+
parentType === 'scoped_identifier') { // Rust
|
|
81
|
+
// Check if this is the property (right side) of the member expression
|
|
82
|
+
// by checking if it's NOT the object (left side)
|
|
83
|
+
const firstChild = node.parent.child(0);
|
|
84
|
+
if (firstChild !== node) {
|
|
85
|
+
// This is the property part — skip it for deadcode counting
|
|
86
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
87
|
+
traverse(node.child(i));
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
55
93
|
const name = node.text;
|
|
56
94
|
if (!usageIndex.has(name)) {
|
|
57
95
|
usageIndex.set(name, []);
|
package/core/execute.js
CHANGED
package/core/output.js
CHANGED
|
@@ -2048,6 +2048,10 @@ function formatSearch(results, term) {
|
|
|
2048
2048
|
}
|
|
2049
2049
|
}
|
|
2050
2050
|
|
|
2051
|
+
if (meta && meta.truncatedMatches > 0) {
|
|
2052
|
+
lines.push(`\n${results.reduce((s, r) => s + r.matches.length, 0)} shown of ${meta.totalMatches} total matches. Use top= to see more.`);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2051
2055
|
return lines.join('\n');
|
|
2052
2056
|
}
|
|
2053
2057
|
|
package/core/project.js
CHANGED
|
@@ -1737,20 +1737,19 @@ class ProjectIndex {
|
|
|
1737
1737
|
// 2. From the class the method belongs to
|
|
1738
1738
|
if (def.className) addIfType(def.className);
|
|
1739
1739
|
|
|
1740
|
-
// 3. From function body — scan for project-defined type references
|
|
1740
|
+
// 3. From function body — always scan for project-defined type references
|
|
1741
1741
|
// (constructors, type annotations, isinstance checks)
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
}
|
|
1742
|
+
// Not just a fallback — methods may reference types beyond their own class
|
|
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);
|
|
1754
1753
|
}
|
|
1755
1754
|
}
|
|
1756
1755
|
}
|
|
@@ -2643,11 +2642,17 @@ class ProjectIndex {
|
|
|
2643
2642
|
}
|
|
2644
2643
|
}
|
|
2645
2644
|
|
|
2646
|
-
// Add smart hint when resolved function has zero callees
|
|
2647
|
-
if (tree && tree.children && tree.children.length === 0
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2645
|
+
// Add smart hint when resolved function has zero callees
|
|
2646
|
+
if (tree && tree.children && tree.children.length === 0) {
|
|
2647
|
+
if (maxDepth === 0) {
|
|
2648
|
+
warnings.push({
|
|
2649
|
+
message: `depth=0: showing root function only. Increase depth to see callees.`
|
|
2650
|
+
});
|
|
2651
|
+
} else if (definitions.length > 1 && !options.file) {
|
|
2652
|
+
warnings.push({
|
|
2653
|
+
message: `Resolved to ${def.relativePath}:${def.startLine} which has no callees. ${definitions.length - 1} other definition(s) exist — specify a file to pick a different one.`
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2651
2656
|
}
|
|
2652
2657
|
|
|
2653
2658
|
return {
|
|
@@ -2680,58 +2685,140 @@ class ProjectIndex {
|
|
|
2680
2685
|
if (!def) {
|
|
2681
2686
|
return null;
|
|
2682
2687
|
}
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2688
|
+
const defIsMethod = def.isMethod || def.type === 'method' || def.className;
|
|
2689
|
+
|
|
2690
|
+
// Use findCallers for className-scoped or method queries (sophisticated binding resolution)
|
|
2691
|
+
// Fall back to usages-based approach for simple function queries (backward compatible)
|
|
2692
|
+
let callSites;
|
|
2693
|
+
if (options.className || defIsMethod) {
|
|
2694
|
+
// findCallers has proper method call resolution (self/this, binding IDs, receiver checks)
|
|
2695
|
+
let callerResults = this.findCallers(name, {
|
|
2696
|
+
includeMethods: true,
|
|
2697
|
+
includeUncertain: false,
|
|
2698
|
+
targetDefinitions: [def],
|
|
2699
|
+
});
|
|
2700
|
+
|
|
2701
|
+
// When className is explicitly provided, filter out method calls whose
|
|
2702
|
+
// receiver clearly belongs to a different type. This helps with common
|
|
2703
|
+
// method names like .close(), .get() etc. where many objects have the same method.
|
|
2704
|
+
if (options.className && def.className) {
|
|
2705
|
+
const targetClassName = def.className;
|
|
2706
|
+
callerResults = callerResults.filter(c => {
|
|
2707
|
+
// Keep non-method calls and self/this/cls calls (already resolved by findCallers)
|
|
2708
|
+
if (!c.isMethod) return true;
|
|
2709
|
+
const r = c.receiver;
|
|
2710
|
+
if (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
2711
|
+
// Check if receiver matches the target class name (case-insensitive camelCase convention)
|
|
2712
|
+
if (r.toLowerCase().includes(targetClassName.toLowerCase())) return true;
|
|
2713
|
+
// Check if receiver is an instance of the target class using local variable type inference
|
|
2714
|
+
if (c.callerFile) {
|
|
2715
|
+
const callerDef = c.callerStartLine ? { file: c.callerFile, startLine: c.callerStartLine, endLine: c.callerEndLine } : null;
|
|
2716
|
+
if (callerDef) {
|
|
2717
|
+
const callerCalls = this.getCachedCalls(c.callerFile);
|
|
2718
|
+
if (callerCalls && Array.isArray(callerCalls)) {
|
|
2719
|
+
const localTypes = new Map();
|
|
2720
|
+
for (const call of callerCalls) {
|
|
2721
|
+
if (call.line >= callerDef.startLine && call.line <= callerDef.endLine) {
|
|
2722
|
+
if (!call.isMethod && !call.receiver) {
|
|
2723
|
+
const syms = this.symbols.get(call.name);
|
|
2724
|
+
if (syms && syms.some(s => s.type === 'class')) {
|
|
2725
|
+
// Found a constructor call — check for assignment pattern
|
|
2726
|
+
const fileEntry = this.files.get(c.callerFile);
|
|
2727
|
+
if (fileEntry) {
|
|
2728
|
+
const content = this._readFile(c.callerFile);
|
|
2729
|
+
const lines = content.split('\n');
|
|
2730
|
+
const line = lines[call.line - 1] || '';
|
|
2731
|
+
// Match "var = ClassName(...)"
|
|
2732
|
+
const m = line.match(/^\s*(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/);
|
|
2733
|
+
if (m && m[2] === call.name) {
|
|
2734
|
+
localTypes.set(m[1], call.name);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
const receiverType = localTypes.get(r);
|
|
2742
|
+
if (receiverType) {
|
|
2743
|
+
return receiverType === targetClassName;
|
|
2744
|
+
}
|
|
2699
2745
|
}
|
|
2700
2746
|
}
|
|
2701
2747
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
}
|
|
2706
|
-
// Cross-reference with findCallsInCode to filter local closures and built-ins
|
|
2707
|
-
// (findCallsInCode has scope-aware filtering that findUsagesInCode lacks)
|
|
2708
|
-
const parsedCalls = this.getCachedCalls(u.file);
|
|
2709
|
-
if (parsedCalls && Array.isArray(parsedCalls)) {
|
|
2710
|
-
const hasCall = parsedCalls.some(c => c.name === name && c.line === u.line);
|
|
2711
|
-
if (!hasCall) return false;
|
|
2712
|
-
}
|
|
2748
|
+
// When className is explicitly set and we can't determine the receiver type,
|
|
2749
|
+
// still include the call (conservative: prefer false positives over false negatives)
|
|
2750
|
+
return true;
|
|
2751
|
+
});
|
|
2713
2752
|
}
|
|
2714
|
-
return true;
|
|
2715
|
-
});
|
|
2716
2753
|
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2754
|
+
callSites = [];
|
|
2755
|
+
for (const c of callerResults) {
|
|
2756
|
+
const analysis = this.analyzeCallSite(
|
|
2757
|
+
{ file: c.file, relativePath: c.relativePath, line: c.line, content: c.content },
|
|
2758
|
+
name
|
|
2759
|
+
);
|
|
2760
|
+
callSites.push({
|
|
2761
|
+
file: c.relativePath,
|
|
2762
|
+
line: c.line,
|
|
2763
|
+
expression: c.content.trim(),
|
|
2764
|
+
callerName: c.callerName,
|
|
2765
|
+
...analysis
|
|
2766
|
+
});
|
|
2725
2767
|
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2768
|
+
this._clearTreeCache();
|
|
2769
|
+
} else {
|
|
2770
|
+
const usages = this.usages(name, { codeOnly: true });
|
|
2771
|
+
const targetBindingId = def.bindingId;
|
|
2772
|
+
const calls = usages.filter(u => {
|
|
2773
|
+
if (u.usageType !== 'call' || u.isDefinition) return false;
|
|
2774
|
+
const fileEntry = this.files.get(u.file);
|
|
2775
|
+
if (fileEntry) {
|
|
2776
|
+
// Filter by binding: skip calls from files that define their own version of this function
|
|
2777
|
+
// For Go, also check sibling files in same directory (same package scope)
|
|
2778
|
+
if (targetBindingId) {
|
|
2779
|
+
let localBindings = (fileEntry.bindings || []).filter(b => b.name === name);
|
|
2780
|
+
if (localBindings.length === 0 && fileEntry.language === 'go') {
|
|
2781
|
+
const dir = path.dirname(u.file);
|
|
2782
|
+
for (const [fp, fe] of this.files) {
|
|
2783
|
+
if (fp !== u.file && path.dirname(fp) === dir) {
|
|
2784
|
+
const sibling = (fe.bindings || []).filter(b => b.name === name);
|
|
2785
|
+
localBindings = localBindings.concat(sibling);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
if (localBindings.length > 0 && !localBindings.some(b => b.id === targetBindingId)) {
|
|
2790
|
+
return false; // This file/package has its own definition — call is to that, not our target
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
// Cross-reference with findCallsInCode to filter local closures and built-ins
|
|
2794
|
+
// (findCallsInCode has scope-aware filtering that findUsagesInCode lacks)
|
|
2795
|
+
const parsedCalls = this.getCachedCalls(u.file);
|
|
2796
|
+
if (parsedCalls && Array.isArray(parsedCalls)) {
|
|
2797
|
+
const hasCall = parsedCalls.some(c => c.name === name && c.line === u.line);
|
|
2798
|
+
if (!hasCall) return false;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
return true;
|
|
2732
2802
|
});
|
|
2803
|
+
|
|
2804
|
+
// Analyze each call site, filtering out method calls for non-method definitions
|
|
2805
|
+
callSites = [];
|
|
2806
|
+
for (const call of calls) {
|
|
2807
|
+
const analysis = this.analyzeCallSite(call, name);
|
|
2808
|
+
// Skip method calls (obj.parse()) when target is a standalone function (parse())
|
|
2809
|
+
if (analysis.isMethodCall && !defIsMethod) {
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
callSites.push({
|
|
2813
|
+
file: call.relativePath,
|
|
2814
|
+
line: call.line,
|
|
2815
|
+
expression: call.content.trim(),
|
|
2816
|
+
callerName: this.findEnclosingFunction(call.file, call.line),
|
|
2817
|
+
...analysis
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
this._clearTreeCache();
|
|
2733
2821
|
}
|
|
2734
|
-
this._clearTreeCache();
|
|
2735
2822
|
|
|
2736
2823
|
// Apply exclude filter
|
|
2737
2824
|
let filteredSites = callSites;
|
|
@@ -2846,7 +2933,6 @@ class ProjectIndex {
|
|
|
2846
2933
|
try {
|
|
2847
2934
|
const maxCallers = options.all ? Infinity : (options.maxCallers || 10);
|
|
2848
2935
|
const maxCallees = options.all ? Infinity : (options.maxCallees || 10);
|
|
2849
|
-
const includeMethods = options.includeMethods ?? false;
|
|
2850
2936
|
|
|
2851
2937
|
// Find symbol definition(s) — skip counts since about() computes its own via usages()
|
|
2852
2938
|
const definitions = this.find(name, { exact: true, file: options.file, className: options.className, skipCounts: true });
|
|
@@ -2879,6 +2965,11 @@ class ProjectIndex {
|
|
|
2879
2965
|
// Use the actual symbol name (may differ from query if fuzzy matched)
|
|
2880
2966
|
const symbolName = primary.name;
|
|
2881
2967
|
|
|
2968
|
+
// Default includeMethods: true when target is a class method (method calls are the primary way
|
|
2969
|
+
// class methods are invoked), false for standalone functions (reduces noise from unrelated obj.fn() calls)
|
|
2970
|
+
const isMethod = !!(primary.isMethod || primary.type === 'method' || primary.className);
|
|
2971
|
+
const includeMethods = options.includeMethods ?? isMethod;
|
|
2972
|
+
|
|
2882
2973
|
// Get usage counts by type
|
|
2883
2974
|
const usages = this.usages(symbolName, { codeOnly: true });
|
|
2884
2975
|
const usagesByType = {
|
|
@@ -3141,7 +3232,28 @@ class ProjectIndex {
|
|
|
3141
3232
|
}
|
|
3142
3233
|
}
|
|
3143
3234
|
|
|
3144
|
-
|
|
3235
|
+
// Apply top limit (limits total matches across all files)
|
|
3236
|
+
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
3237
|
+
let truncatedMatches = 0;
|
|
3238
|
+
if (options.top && options.top > 0 && totalMatches > options.top) {
|
|
3239
|
+
let remaining = options.top;
|
|
3240
|
+
const truncated = [];
|
|
3241
|
+
for (const r of results) {
|
|
3242
|
+
if (remaining <= 0) break;
|
|
3243
|
+
if (r.matches.length <= remaining) {
|
|
3244
|
+
truncated.push(r);
|
|
3245
|
+
remaining -= r.matches.length;
|
|
3246
|
+
} else {
|
|
3247
|
+
truncated.push({ ...r, matches: r.matches.slice(0, remaining) });
|
|
3248
|
+
remaining = 0;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
truncatedMatches = totalMatches - options.top;
|
|
3252
|
+
results.length = 0;
|
|
3253
|
+
results.push(...truncated);
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
results.meta = { filesScanned, filesSkipped, totalFiles: this.files.size, regexFallback, totalMatches, truncatedMatches };
|
|
3145
3257
|
return results;
|
|
3146
3258
|
} finally { this._endOp(); }
|
|
3147
3259
|
}
|
package/core/registry.js
CHANGED
package/core/stacktrace.js
CHANGED
|
@@ -189,7 +189,9 @@ function createStackFrame(index, filePath, lineNum, funcName, col, rawLine) {
|
|
|
189
189
|
frame.context = contextLines;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
// Try to find function info
|
|
192
|
+
// Try to find function info — always use AST to find enclosing function at this line,
|
|
193
|
+
// then verify against the parsed function name from the stack trace
|
|
194
|
+
const enclosing = index.findEnclosingFunction(resolvedPath, lineNum, true);
|
|
193
195
|
if (funcName) {
|
|
194
196
|
const symbols = index.symbols.get(funcName);
|
|
195
197
|
if (symbols) {
|
|
@@ -205,9 +207,21 @@ function createStackFrame(index, filePath, lineNum, funcName, col, rawLine) {
|
|
|
205
207
|
params: funcMatch.params
|
|
206
208
|
};
|
|
207
209
|
frame.confidence = 1.0; // High confidence when function verified
|
|
210
|
+
} else if (enclosing) {
|
|
211
|
+
// Stack trace function name doesn't match — use AST-resolved function
|
|
212
|
+
// (source may have changed, or the name was from a transpiled/minified trace)
|
|
213
|
+
frame.functionInfo = {
|
|
214
|
+
name: enclosing.name,
|
|
215
|
+
startLine: enclosing.startLine,
|
|
216
|
+
endLine: enclosing.endLine,
|
|
217
|
+
params: enclosing.params,
|
|
218
|
+
inferred: true,
|
|
219
|
+
traceName: funcName // preserve original name from stack trace
|
|
220
|
+
};
|
|
221
|
+
frame.confidence = Math.min(frame.confidence, 0.7);
|
|
208
222
|
} else {
|
|
209
|
-
// Function
|
|
210
|
-
const anyMatch = symbols
|
|
223
|
+
// Function name doesn't match AND no enclosing function found
|
|
224
|
+
const anyMatch = symbols?.find(s => s.file === resolvedPath);
|
|
211
225
|
if (anyMatch) {
|
|
212
226
|
frame.functionInfo = {
|
|
213
227
|
name: anyMatch.name,
|
|
@@ -219,19 +233,27 @@ function createStackFrame(index, filePath, lineNum, funcName, col, rawLine) {
|
|
|
219
233
|
frame.confidence = Math.min(frame.confidence, 0.5);
|
|
220
234
|
}
|
|
221
235
|
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// No function name in stack - find enclosing function
|
|
225
|
-
const enclosing = index.findEnclosingFunction(resolvedPath, lineNum, true);
|
|
226
|
-
if (enclosing) {
|
|
236
|
+
} else if (enclosing) {
|
|
237
|
+
// funcName not in symbol table — use AST enclosing function
|
|
227
238
|
frame.functionInfo = {
|
|
228
239
|
name: enclosing.name,
|
|
229
240
|
startLine: enclosing.startLine,
|
|
230
241
|
endLine: enclosing.endLine,
|
|
231
242
|
params: enclosing.params,
|
|
232
|
-
inferred: true
|
|
243
|
+
inferred: true,
|
|
244
|
+
traceName: funcName
|
|
233
245
|
};
|
|
246
|
+
frame.confidence = Math.min(frame.confidence, 0.6);
|
|
234
247
|
}
|
|
248
|
+
} else if (enclosing) {
|
|
249
|
+
// No function name in stack - find enclosing function
|
|
250
|
+
frame.functionInfo = {
|
|
251
|
+
name: enclosing.name,
|
|
252
|
+
startLine: enclosing.startLine,
|
|
253
|
+
endLine: enclosing.endLine,
|
|
254
|
+
params: enclosing.params,
|
|
255
|
+
inferred: true
|
|
256
|
+
};
|
|
235
257
|
}
|
|
236
258
|
} catch (e) {
|
|
237
259
|
frame.error = e.message;
|
package/core/verify.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* as the first argument instead of using `this`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const path = require('path');
|
|
8
9
|
const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
|
|
9
10
|
const { escapeRegExp } = require('./shared');
|
|
11
|
+
const { extractImports } = require('./imports');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Find a call expression node at the target line matching funcName
|
|
@@ -199,24 +201,73 @@ function verify(index, name, options = {}) {
|
|
|
199
201
|
const optionalCount = nonRestParams.filter(p => p.optional || p.default !== undefined).length;
|
|
200
202
|
const minArgs = expectedParamCount - optionalCount;
|
|
201
203
|
|
|
202
|
-
// Get all call sites
|
|
203
|
-
|
|
204
|
-
const
|
|
204
|
+
// Get all call sites using findCallers for accurate resolution
|
|
205
|
+
// (usages-based approach misses calls when className is set or local names collide)
|
|
206
|
+
const callerResults = index.findCallers(name, {
|
|
207
|
+
includeMethods: true,
|
|
208
|
+
includeUncertain: false,
|
|
209
|
+
targetDefinitions: [def],
|
|
210
|
+
});
|
|
211
|
+
// Convert caller results to usage-like objects for analyzeCallSite
|
|
212
|
+
const calls = callerResults.map(c => ({
|
|
213
|
+
file: c.file,
|
|
214
|
+
relativePath: c.relativePath,
|
|
215
|
+
line: c.line,
|
|
216
|
+
content: c.content,
|
|
217
|
+
usageType: 'call',
|
|
218
|
+
receiver: c.receiver,
|
|
219
|
+
}));
|
|
205
220
|
|
|
206
221
|
const valid = [];
|
|
207
222
|
const mismatches = [];
|
|
208
223
|
const uncertain = [];
|
|
209
224
|
|
|
210
225
|
// If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
|
|
211
|
-
// This prevents false positives where a standalone function name matches method calls
|
|
212
|
-
|
|
226
|
+
// This prevents false positives where a standalone function name matches method calls.
|
|
227
|
+
// Exception: module-level calls (module.func()) are kept when the receiver matches the
|
|
228
|
+
// target module's name and is an imported name (e.g., jobs.submit() where jobs is imported
|
|
229
|
+
// and the function lives in jobs.py).
|
|
230
|
+
const defIsMethod = !!(def.isMethod || def.type === 'method' || def.className);
|
|
231
|
+
const targetBasename = path.basename(def.file, path.extname(def.file));
|
|
232
|
+
|
|
233
|
+
// Build import-name lookup for receiver checking (module.func() vs dict.get())
|
|
234
|
+
const importNameCache = new Map();
|
|
235
|
+
function getImportedNames(filePath) {
|
|
236
|
+
if (importNameCache.has(filePath)) return importNameCache.get(filePath);
|
|
237
|
+
const names = new Set();
|
|
238
|
+
const fe = index.files.get(filePath);
|
|
239
|
+
if (!fe) { importNameCache.set(filePath, names); return names; }
|
|
240
|
+
try {
|
|
241
|
+
const content = index._readFile(filePath);
|
|
242
|
+
const { imports: rawImports, importAliases } = extractImports(content, fe.language);
|
|
243
|
+
for (const imp of rawImports) {
|
|
244
|
+
if (imp.names) for (const n of imp.names) names.add(n);
|
|
245
|
+
}
|
|
246
|
+
if (importAliases) {
|
|
247
|
+
for (const alias of importAliases) names.add(alias.local);
|
|
248
|
+
}
|
|
249
|
+
} catch (e) { /* skip */ }
|
|
250
|
+
importNameCache.set(filePath, names);
|
|
251
|
+
return names;
|
|
252
|
+
}
|
|
213
253
|
|
|
214
254
|
for (const call of calls) {
|
|
215
255
|
const analysis = analyzeCallSite(index, call, name);
|
|
216
256
|
|
|
217
|
-
// Skip method calls when verifying a non-method definition
|
|
257
|
+
// Skip method calls when verifying a non-method definition.
|
|
258
|
+
// This prevents false positives (e.g., dict.get() vs standalone get()).
|
|
259
|
+
// Allow module-level calls only when:
|
|
260
|
+
// 1. Receiver matches target file's basename (e.g., jobs == jobs for jobs.py)
|
|
261
|
+
// 2. Receiver is an imported name (not a local variable)
|
|
218
262
|
if (analysis.isMethodCall && !defIsMethod) {
|
|
219
|
-
|
|
263
|
+
const callReceiver = call.receiver;
|
|
264
|
+
if (callReceiver && callReceiver === targetBasename) {
|
|
265
|
+
const importedNames = getImportedNames(call.file);
|
|
266
|
+
if (!importedNames.has(callReceiver)) continue;
|
|
267
|
+
// Receiver matches target module and is imported — keep it
|
|
268
|
+
} else {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
220
271
|
}
|
|
221
272
|
|
|
222
273
|
if (analysis.args === null) {
|
package/mcp/server.js
CHANGED
|
@@ -251,7 +251,9 @@ server.registerTool(
|
|
|
251
251
|
staged: z.boolean().optional().describe('Analyze staged changes (diff_impact command)'),
|
|
252
252
|
case_sensitive: z.boolean().optional().describe('Case-sensitive search (default: false, case-insensitive)'),
|
|
253
253
|
all: z.boolean().optional().describe('Show all results (expand truncated sections). Applies to about, toc, related, trace, and others.'),
|
|
254
|
-
top_level: z.boolean().optional().describe('Show only top-level functions in toc (exclude nested/indented)')
|
|
254
|
+
top_level: z.boolean().optional().describe('Show only top-level functions in toc (exclude nested/indented)'),
|
|
255
|
+
class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)')
|
|
256
|
+
|
|
255
257
|
})
|
|
256
258
|
},
|
|
257
259
|
async (args) => {
|
|
@@ -261,7 +263,7 @@ server.registerTool(
|
|
|
261
263
|
include_exported, include_decorated, calls_only, max_lines,
|
|
262
264
|
direction, term, add_param, remove_param, rename_to,
|
|
263
265
|
default_value, stack, item, range, base, staged,
|
|
264
|
-
case_sensitive, regex, functions, all, top_level } = args;
|
|
266
|
+
case_sensitive, regex, functions, all, top_level, class_name } = args;
|
|
265
267
|
|
|
266
268
|
try {
|
|
267
269
|
switch (command) {
|
|
@@ -274,7 +276,7 @@ server.registerTool(
|
|
|
274
276
|
|
|
275
277
|
case 'about': {
|
|
276
278
|
const index = getIndex(project_dir);
|
|
277
|
-
const ep = normalizeParams({ name, file, exclude, with_types, all, include_methods, include_uncertain, top });
|
|
279
|
+
const ep = normalizeParams({ name, file, exclude, with_types, all, include_methods, include_uncertain, top, class_name });
|
|
278
280
|
const { ok, result, error } = execute(index, 'about', ep);
|
|
279
281
|
if (!ok) return toolResult(error); // soft error — won't kill sibling calls
|
|
280
282
|
return toolResult(output.formatAbout(result, {
|
|
@@ -285,7 +287,7 @@ server.registerTool(
|
|
|
285
287
|
|
|
286
288
|
case 'context': {
|
|
287
289
|
const index = getIndex(project_dir);
|
|
288
|
-
const ep = normalizeParams({ name, file, exclude, include_methods, include_uncertain });
|
|
290
|
+
const ep = normalizeParams({ name, file, exclude, include_methods, include_uncertain, class_name });
|
|
289
291
|
const { ok, result: ctx, error } = execute(index, 'context', ep);
|
|
290
292
|
if (!ok) return toolResult(error); // context uses soft error (not toolError)
|
|
291
293
|
const { text, expandable } = output.formatContext(ctx, {
|
|
@@ -297,7 +299,7 @@ server.registerTool(
|
|
|
297
299
|
|
|
298
300
|
case 'impact': {
|
|
299
301
|
const index = getIndex(project_dir);
|
|
300
|
-
const ep = normalizeParams({ name, file, exclude, top });
|
|
302
|
+
const ep = normalizeParams({ name, file, exclude, top, class_name });
|
|
301
303
|
const { ok, result, error } = execute(index, 'impact', ep);
|
|
302
304
|
if (!ok) return toolResult(error); // soft error
|
|
303
305
|
return toolResult(output.formatImpact(result));
|
|
@@ -373,7 +375,7 @@ server.registerTool(
|
|
|
373
375
|
|
|
374
376
|
case 'search': {
|
|
375
377
|
const index = getIndex(project_dir);
|
|
376
|
-
const ep = normalizeParams({ term, exclude, include_tests, code_only, context: ctxLines, case_sensitive, in: inPath, regex });
|
|
378
|
+
const ep = normalizeParams({ term, exclude, include_tests, code_only, context: ctxLines, case_sensitive, in: inPath, regex, top });
|
|
377
379
|
const { ok, result, error } = execute(index, 'search', ep);
|
|
378
380
|
if (!ok) return toolResult(error); // soft error
|
|
379
381
|
return toolResult(output.formatSearch(result, term));
|
|
@@ -438,14 +440,15 @@ server.registerTool(
|
|
|
438
440
|
|
|
439
441
|
case 'verify': {
|
|
440
442
|
const index = getIndex(project_dir);
|
|
441
|
-
const
|
|
443
|
+
const ep = normalizeParams({ name, file, class_name });
|
|
444
|
+
const { ok, result, error } = execute(index, 'verify', ep);
|
|
442
445
|
if (!ok) return toolResult(error); // soft error
|
|
443
446
|
return toolResult(output.formatVerify(result));
|
|
444
447
|
}
|
|
445
448
|
|
|
446
449
|
case 'plan': {
|
|
447
450
|
const index = getIndex(project_dir);
|
|
448
|
-
const ep = normalizeParams({ name, add_param, remove_param, rename_to, default_value, file });
|
|
451
|
+
const ep = normalizeParams({ name, add_param, remove_param, rename_to, default_value, file, class_name });
|
|
449
452
|
const { ok, result, error } = execute(index, 'plan', ep);
|
|
450
453
|
if (!ok) return toolResult(error); // soft error
|
|
451
454
|
return toolResult(output.formatPlan(result));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.38",
|
|
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",
|