ucn 3.7.35 → 3.7.37
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 +215 -65
- package/core/stacktrace.js +31 -9
- package/core/verify.js +58 -7
- 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
|
@@ -1705,12 +1705,21 @@ class ProjectIndex {
|
|
|
1705
1705
|
|
|
1706
1706
|
/**
|
|
1707
1707
|
* Extract type names from a function definition
|
|
1708
|
-
* Finds all word-like type identifiers from param types
|
|
1709
|
-
*
|
|
1708
|
+
* Finds all word-like type identifiers from param types, return type,
|
|
1709
|
+
* class membership, and function body — filters to project-defined types only.
|
|
1710
1710
|
*/
|
|
1711
1711
|
extractTypeNames(def) {
|
|
1712
|
+
const TYPE_KINDS = ['type', 'interface', 'class', 'struct'];
|
|
1712
1713
|
const types = new Set();
|
|
1713
|
-
|
|
1714
|
+
|
|
1715
|
+
const addIfType = (name) => {
|
|
1716
|
+
const syms = this.symbols.get(name);
|
|
1717
|
+
if (syms && syms.some(s => TYPE_KINDS.includes(s.type))) {
|
|
1718
|
+
types.add(name);
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// 1. From param and return type annotations
|
|
1714
1723
|
const typeStrings = [];
|
|
1715
1724
|
if (def.paramsStructured) {
|
|
1716
1725
|
for (const param of def.paramsStructured) {
|
|
@@ -1718,17 +1727,29 @@ class ProjectIndex {
|
|
|
1718
1727
|
}
|
|
1719
1728
|
}
|
|
1720
1729
|
if (def.returnType) typeStrings.push(def.returnType);
|
|
1721
|
-
|
|
1722
|
-
// Extract all word-like identifiers from type annotations
|
|
1723
|
-
// Handles: Dict[str, Any], Optional[QuoteData], str | None, List[int], CustomType
|
|
1724
1730
|
for (const ts of typeStrings) {
|
|
1725
1731
|
const matches = ts.match(/\b([A-Za-z_]\w*)\b/g);
|
|
1726
1732
|
if (matches) {
|
|
1727
|
-
for (const m of matches)
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1733
|
+
for (const m of matches) addIfType(m);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// 2. From the class the method belongs to
|
|
1738
|
+
if (def.className) addIfType(def.className);
|
|
1739
|
+
|
|
1740
|
+
// 3. From function body — always scan for project-defined type references
|
|
1741
|
+
// (constructors, type annotations, isinstance checks)
|
|
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);
|
|
1732
1753
|
}
|
|
1733
1754
|
}
|
|
1734
1755
|
}
|
|
@@ -2621,11 +2642,17 @@ class ProjectIndex {
|
|
|
2621
2642
|
}
|
|
2622
2643
|
}
|
|
2623
2644
|
|
|
2624
|
-
// Add smart hint when resolved function has zero callees
|
|
2625
|
-
if (tree && tree.children && tree.children.length === 0
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
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
|
+
}
|
|
2629
2656
|
}
|
|
2630
2657
|
|
|
2631
2658
|
return {
|
|
@@ -2658,58 +2685,140 @@ class ProjectIndex {
|
|
|
2658
2685
|
if (!def) {
|
|
2659
2686
|
return null;
|
|
2660
2687
|
}
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
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
|
+
}
|
|
2677
2745
|
}
|
|
2678
2746
|
}
|
|
2679
2747
|
}
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
}
|
|
2684
|
-
// Cross-reference with findCallsInCode to filter local closures and built-ins
|
|
2685
|
-
// (findCallsInCode has scope-aware filtering that findUsagesInCode lacks)
|
|
2686
|
-
const parsedCalls = this.getCachedCalls(u.file);
|
|
2687
|
-
if (parsedCalls && Array.isArray(parsedCalls)) {
|
|
2688
|
-
const hasCall = parsedCalls.some(c => c.name === name && c.line === u.line);
|
|
2689
|
-
if (!hasCall) return false;
|
|
2690
|
-
}
|
|
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
|
+
});
|
|
2691
2752
|
}
|
|
2692
|
-
return true;
|
|
2693
|
-
});
|
|
2694
2753
|
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
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
|
+
});
|
|
2703
2767
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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;
|
|
2710
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();
|
|
2711
2821
|
}
|
|
2712
|
-
this._clearTreeCache();
|
|
2713
2822
|
|
|
2714
2823
|
// Apply exclude filter
|
|
2715
2824
|
let filteredSites = callSites;
|
|
@@ -2824,7 +2933,6 @@ class ProjectIndex {
|
|
|
2824
2933
|
try {
|
|
2825
2934
|
const maxCallers = options.all ? Infinity : (options.maxCallers || 10);
|
|
2826
2935
|
const maxCallees = options.all ? Infinity : (options.maxCallees || 10);
|
|
2827
|
-
const includeMethods = options.includeMethods ?? false;
|
|
2828
2936
|
|
|
2829
2937
|
// Find symbol definition(s) — skip counts since about() computes its own via usages()
|
|
2830
2938
|
const definitions = this.find(name, { exact: true, file: options.file, className: options.className, skipCounts: true });
|
|
@@ -2857,6 +2965,11 @@ class ProjectIndex {
|
|
|
2857
2965
|
// Use the actual symbol name (may differ from query if fuzzy matched)
|
|
2858
2966
|
const symbolName = primary.name;
|
|
2859
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
|
+
|
|
2860
2973
|
// Get usage counts by type
|
|
2861
2974
|
const usages = this.usages(symbolName, { codeOnly: true });
|
|
2862
2975
|
const usagesByType = {
|
|
@@ -2917,12 +3030,16 @@ class ProjectIndex {
|
|
|
2917
3030
|
// Get type definitions if requested
|
|
2918
3031
|
let types = [];
|
|
2919
3032
|
if (options.withTypes) {
|
|
2920
|
-
const
|
|
2921
|
-
|
|
3033
|
+
const TYPE_KINDS = ['type', 'interface', 'class', 'struct'];
|
|
3034
|
+
const seen = new Set();
|
|
3035
|
+
|
|
3036
|
+
const addType = (typeName) => {
|
|
3037
|
+
if (seen.has(typeName)) return;
|
|
3038
|
+
seen.add(typeName);
|
|
2922
3039
|
const typeSymbols = this.symbols.get(typeName);
|
|
2923
3040
|
if (typeSymbols) {
|
|
2924
3041
|
for (const sym of typeSymbols) {
|
|
2925
|
-
if (
|
|
3042
|
+
if (TYPE_KINDS.includes(sym.type)) {
|
|
2926
3043
|
types.push({
|
|
2927
3044
|
name: sym.name,
|
|
2928
3045
|
type: sym.type,
|
|
@@ -2932,6 +3049,18 @@ class ProjectIndex {
|
|
|
2932
3049
|
}
|
|
2933
3050
|
}
|
|
2934
3051
|
}
|
|
3052
|
+
};
|
|
3053
|
+
|
|
3054
|
+
// From signature annotations
|
|
3055
|
+
const typeNames = this.extractTypeNames(primary);
|
|
3056
|
+
for (const typeName of typeNames) addType(typeName);
|
|
3057
|
+
|
|
3058
|
+
// From callee signatures — types used by functions this function calls
|
|
3059
|
+
if (allCallees) {
|
|
3060
|
+
for (const callee of allCallees) {
|
|
3061
|
+
const calleeTypeNames = this.extractTypeNames(callee);
|
|
3062
|
+
for (const tn of calleeTypeNames) addType(tn);
|
|
3063
|
+
}
|
|
2935
3064
|
}
|
|
2936
3065
|
}
|
|
2937
3066
|
|
|
@@ -3103,7 +3232,28 @@ class ProjectIndex {
|
|
|
3103
3232
|
}
|
|
3104
3233
|
}
|
|
3105
3234
|
|
|
3106
|
-
|
|
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 };
|
|
3107
3257
|
return results;
|
|
3108
3258
|
} finally { this._endOp(); }
|
|
3109
3259
|
}
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.37",
|
|
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",
|