ucn 3.7.41 → 3.7.43

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/execute.js CHANGED
@@ -298,6 +298,7 @@ const HANDLERS = {
298
298
  search: (index, p) => {
299
299
  const err = requireTerm(p.term);
300
300
  if (err) return { ok: false, error: err };
301
+ const testsExcluded = !p.includeTests;
301
302
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
302
303
  const result = index.search(p.term, {
303
304
  codeOnly: p.codeOnly || false,
@@ -308,6 +309,7 @@ const HANDLERS = {
308
309
  regex: p.regex,
309
310
  top: num(p.top, undefined),
310
311
  });
312
+ if (result.meta) result.meta.testsExcluded = testsExcluded;
311
313
  return { ok: true, result };
312
314
  },
313
315
 
package/core/output.js CHANGED
@@ -2052,6 +2052,10 @@ function formatSearch(results, term) {
2052
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
2053
  }
2054
2054
 
2055
+ if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
2056
+ lines.push(`\nNote: ${meta.filesSkipped} file${meta.filesSkipped === 1 ? '' : 's'} excluded by filters (test files hidden by default; use include_tests=true to include).`);
2057
+ }
2058
+
2055
2059
  return lines.join('\n');
2056
2060
  }
2057
2061
 
package/core/project.js CHANGED
@@ -1018,12 +1018,38 @@ class ProjectIndex {
1018
1018
  // Try AST-based detection first (with per-operation cache)
1019
1019
  const astUsages = this._getCachedUsages(filePath, name);
1020
1020
  if (astUsages !== null) {
1021
+ // Pre-compute: does any imported project file define this name?
1022
+ // Used to filter namespace member expressions (e.g., DropdownMenuPrimitive.Separator)
1023
+ // while keeping module access patterns (e.g., output.formatExample())
1024
+ let _importedHasDef = null;
1025
+ const importedFileHasDef = () => {
1026
+ if (_importedHasDef !== null) return _importedHasDef;
1027
+ const importedFiles = this.importGraph.get(filePath) || [];
1028
+ _importedHasDef = importedFiles.some(imp => {
1029
+ const impEntry = this.files.get(imp);
1030
+ return impEntry?.symbols?.some(s => s.name === name);
1031
+ });
1032
+ return _importedHasDef;
1033
+ };
1034
+
1021
1035
  for (const u of astUsages) {
1022
1036
  // Skip if this is a definition line (already added above)
1023
1037
  if (definitions.some(d => d.file === filePath && d.startLine === u.line)) {
1024
1038
  continue;
1025
1039
  }
1026
1040
 
1041
+ // Filter member expressions with unrelated receivers in JS/TS/Python.
1042
+ // Keeps: standalone usages, self/this/cls/super, method calls on known types,
1043
+ // and module access (output.fn()) when the imported file defines the name.
1044
+ // Filters: namespace access to external packages (DropdownMenuPrimitive.Separator).
1045
+ if (u.receiver && !['self', 'this', 'cls', 'super'].includes(u.receiver) &&
1046
+ fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
1047
+ const hasMethodDef = allDefinitions.some(d => d.className);
1048
+ if (!hasMethodDef && !importedFileHasDef()) {
1049
+ continue;
1050
+ }
1051
+ }
1052
+
1027
1053
  const lineContent = lines[u.line - 1] || '';
1028
1054
 
1029
1055
  const usage = {
@@ -2746,6 +2772,35 @@ class ProjectIndex {
2746
2772
  }
2747
2773
  }
2748
2774
  }
2775
+ // Check parameter type annotations: def foo(tracker: SourceTracker) → tracker.record()
2776
+ if (c.callerFile && c.callerStartLine) {
2777
+ const callerSymbol = this.findEnclosingFunction(c.callerFile, c.line, true);
2778
+ if (callerSymbol && callerSymbol.paramsStructured) {
2779
+ for (const param of callerSymbol.paramsStructured) {
2780
+ if (param.name === r && param.type) {
2781
+ // Check if the type annotation contains the target class name
2782
+ const typeMatches = param.type.match(/\b([A-Za-z_]\w*)\b/g);
2783
+ if (typeMatches && typeMatches.some(t => t === targetClassName)) {
2784
+ return true;
2785
+ }
2786
+ // Type annotation exists but doesn't match target class — filter out
2787
+ return false;
2788
+ }
2789
+ }
2790
+ }
2791
+ }
2792
+ // Unique method heuristic: if the called method exists on exactly one class
2793
+ // and it matches the target, include the call (no other class could match)
2794
+ const methodDefs = this.symbols.get(name);
2795
+ if (methodDefs) {
2796
+ const classNames = new Set();
2797
+ for (const d of methodDefs) {
2798
+ if (d.className) classNames.add(d.className);
2799
+ }
2800
+ if (classNames.size === 1 && classNames.has(targetClassName)) {
2801
+ return true;
2802
+ }
2803
+ }
2749
2804
  // className explicitly set but receiver type unknown — filter it out.
2750
2805
  // User asked for a specific class; unknown receivers are likely unrelated.
2751
2806
  return false;
package/core/verify.js CHANGED
@@ -203,11 +203,83 @@ function verify(index, name, options = {}) {
203
203
 
204
204
  // Get all call sites using findCallers for accurate resolution
205
205
  // (usages-based approach misses calls when className is set or local names collide)
206
- const callerResults = index.findCallers(name, {
206
+ let callerResults = index.findCallers(name, {
207
207
  includeMethods: true,
208
208
  includeUncertain: false,
209
209
  targetDefinitions: [def],
210
210
  });
211
+
212
+ // When className is explicitly provided, filter out method calls whose
213
+ // receiver clearly belongs to a different type (same logic as impact()).
214
+ if (options.className && def.className) {
215
+ const targetClassName = def.className;
216
+ callerResults = callerResults.filter(c => {
217
+ if (!c.isMethod) return true;
218
+ const r = c.receiver;
219
+ if (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
220
+ if (r.toLowerCase().includes(targetClassName.toLowerCase())) return true;
221
+ // Check local variable type inference from constructor assignments
222
+ if (c.callerFile) {
223
+ const callerDef = c.callerStartLine ? { file: c.callerFile, startLine: c.callerStartLine, endLine: c.callerEndLine } : null;
224
+ if (callerDef) {
225
+ const callerCalls = index.getCachedCalls(c.callerFile);
226
+ if (callerCalls && Array.isArray(callerCalls)) {
227
+ const localTypes = new Map();
228
+ for (const call of callerCalls) {
229
+ if (call.line >= callerDef.startLine && call.line <= callerDef.endLine) {
230
+ if (!call.isMethod && !call.receiver) {
231
+ const syms = index.symbols.get(call.name);
232
+ if (syms && syms.some(s => s.type === 'class')) {
233
+ const content = index._readFile(c.callerFile);
234
+ const clines = content.split('\n');
235
+ const cline = clines[call.line - 1] || '';
236
+ const m = cline.match(/^\s*(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/);
237
+ if (m && m[2] === call.name) {
238
+ localTypes.set(m[1], call.name);
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ const receiverType = localTypes.get(r);
245
+ if (receiverType) {
246
+ return receiverType === targetClassName;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ // Check parameter type annotations: def foo(tracker: SourceTracker) → tracker.record()
252
+ if (c.callerFile && c.callerStartLine) {
253
+ const callerSymbol = index.findEnclosingFunction(c.callerFile, c.line, true);
254
+ if (callerSymbol && callerSymbol.paramsStructured) {
255
+ for (const param of callerSymbol.paramsStructured) {
256
+ if (param.name === r && param.type) {
257
+ const typeMatches = param.type.match(/\b([A-Za-z_]\w*)\b/g);
258
+ if (typeMatches && typeMatches.some(t => t === targetClassName)) {
259
+ return true;
260
+ }
261
+ return false;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ // Unique method heuristic: if the called method exists on exactly one class
267
+ // and it matches the target, include the call (no other class could match)
268
+ const methodDefs = index.symbols.get(name);
269
+ if (methodDefs) {
270
+ const classNames = new Set();
271
+ for (const d of methodDefs) {
272
+ if (d.className) classNames.add(d.className);
273
+ }
274
+ if (classNames.size === 1 && classNames.has(targetClassName)) {
275
+ return true;
276
+ }
277
+ }
278
+ // className explicitly set but receiver type unknown — filter it out
279
+ return false;
280
+ });
281
+ }
282
+
211
283
  // Convert caller results to usage-like objects for analyzeCallSite
212
284
  const calls = callerResults.map(c => ({
213
285
  file: c.file,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.41",
3
+ "version": "3.7.43",
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",