ucn 3.7.36 → 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 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
- if (!bindingId) {
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
- if (selfAttrCalls && def.className) {
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
- if (selfMethodCalls && def.className) {
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
@@ -306,6 +306,7 @@ const HANDLERS = {
306
306
  exclude,
307
307
  in: p.in,
308
308
  regex: p.regex,
309
+ top: num(p.top, undefined),
309
310
  });
310
311
  return { ok: true, result };
311
312
  },
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
- if (types.size === 0) {
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);
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 and alternatives exist
2647
- if (tree && tree.children && tree.children.length === 0 && definitions.length > 1 && !options.file) {
2648
- warnings.push({
2649
- 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.`
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 usages = this.usages(name, { codeOnly: true });
2684
- const targetBindingId = def.bindingId;
2685
- const calls = usages.filter(u => {
2686
- if (u.usageType !== 'call' || u.isDefinition) return false;
2687
- const fileEntry = this.files.get(u.file);
2688
- if (fileEntry) {
2689
- // Filter by binding: skip calls from files that define their own version of this function
2690
- // For Go, also check sibling files in same directory (same package scope)
2691
- if (targetBindingId) {
2692
- let localBindings = (fileEntry.bindings || []).filter(b => b.name === name);
2693
- if (localBindings.length === 0 && fileEntry.language === 'go') {
2694
- const dir = path.dirname(u.file);
2695
- for (const [fp, fe] of this.files) {
2696
- if (fp !== u.file && path.dirname(fp) === dir) {
2697
- const sibling = (fe.bindings || []).filter(b => b.name === name);
2698
- localBindings = localBindings.concat(sibling);
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
- if (localBindings.length > 0 && !localBindings.some(b => b.id === targetBindingId)) {
2703
- return false; // This file/package has its own definition call is to that, not our target
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
- // Analyze each call site, filtering out method calls for non-method definitions
2718
- const defIsMethod = def.isMethod || def.type === 'method' || def.className;
2719
- const callSites = [];
2720
- for (const call of calls) {
2721
- const analysis = this.analyzeCallSite(call, name);
2722
- // Skip method calls (obj.parse()) when target is a standalone function (parse())
2723
- if (analysis.isMethodCall && !defIsMethod) {
2724
- continue;
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
- callSites.push({
2727
- file: call.relativePath,
2728
- line: call.line,
2729
- expression: call.content.trim(),
2730
- callerName: this.findEnclosingFunction(call.file, call.line),
2731
- ...analysis
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
- results.meta = { filesScanned, filesSkipped, totalFiles: this.files.size, regexFallback };
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
  }
@@ -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 (verify it contains the line)
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 exists but line doesn't match - lower confidence
210
- const anyMatch = symbols.find(s => s.file === resolvedPath);
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
- } else {
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
- const usages = index.usages(name, { codeOnly: true });
204
- const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
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
- const defIsMethod = def.isMethod || def.type === 'method' || def.className;
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
- continue;
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.36",
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",