ucn 3.7.44 → 3.7.45

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
@@ -99,6 +99,24 @@ function checkFileError(result, file) {
99
99
  return null;
100
100
  }
101
101
 
102
+ /**
103
+ * Validate that className filter actually matches a definition.
104
+ * Returns error string if className is invalid, null if OK.
105
+ */
106
+ function validateClassName(index, name, className) {
107
+ if (!className) return null;
108
+ const allDefs = index.symbols.get(name);
109
+ if (!allDefs || allDefs.length === 0) return null; // no defs at all — let the command handle "not found"
110
+ const matching = allDefs.filter(d => d.className === className);
111
+ if (matching.length > 0) return null; // className matched
112
+ // className specified but no definitions match
113
+ const available = [...new Set(allDefs.filter(d => d.className).map(d => d.className))];
114
+ if (available.length > 0) {
115
+ return `Symbol "${name}" not found in class "${className}". Available in: ${available.join(', ')}.`;
116
+ }
117
+ return `Symbol "${name}" is not a method of any class. Defined in: ${allDefs[0].relativePath}:${allDefs[0].startLine}.`;
118
+ }
119
+
102
120
  /** Parse a number param (handles string from CLI, number from MCP). */
103
121
  function num(val, fallback) {
104
122
  if (val == null) return fallback;
@@ -162,6 +180,8 @@ const HANDLERS = {
162
180
  const err = requireName(p.name);
163
181
  if (err) return { ok: false, error: err };
164
182
  applyClassMethodSyntax(p);
183
+ const classErr = validateClassName(index, p.name, p.className);
184
+ if (classErr) return { ok: false, error: classErr };
165
185
  const result = index.context(p.name, {
166
186
  includeMethods: p.includeMethods,
167
187
  includeUncertain: p.includeUncertain || false,
@@ -177,6 +197,8 @@ const HANDLERS = {
177
197
  const err = requireName(p.name);
178
198
  if (err) return { ok: false, error: err };
179
199
  applyClassMethodSyntax(p);
200
+ const classErr = validateClassName(index, p.name, p.className);
201
+ if (classErr) return { ok: false, error: classErr };
180
202
  const result = index.impact(p.name, {
181
203
  file: p.file,
182
204
  className: p.className,
@@ -291,6 +313,7 @@ const HANDLERS = {
291
313
  topLevel: p.topLevel,
292
314
  all: p.all,
293
315
  top: num(p.top, undefined),
316
+ file: p.file,
294
317
  });
295
318
  return { ok: true, result };
296
319
  },
@@ -562,6 +585,8 @@ const HANDLERS = {
562
585
  const err = requireName(p.name);
563
586
  if (err) return { ok: false, error: err };
564
587
  applyClassMethodSyntax(p);
588
+ const classErr = validateClassName(index, p.name, p.className);
589
+ if (classErr) return { ok: false, error: classErr };
565
590
  const result = index.verify(p.name, { file: p.file, className: p.className });
566
591
  return { ok: true, result };
567
592
  },
@@ -570,6 +595,8 @@ const HANDLERS = {
570
595
  const err = requireName(p.name);
571
596
  if (err) return { ok: false, error: err };
572
597
  applyClassMethodSyntax(p);
598
+ const classErr = validateClassName(index, p.name, p.className);
599
+ if (classErr) return { ok: false, error: classErr };
573
600
  if (!p.addParam && !p.removeParam && !p.renameTo) {
574
601
  return { ok: false, error: 'Plan requires an operation: add_param, remove_param, or rename_to.' };
575
602
  }
package/core/project.js CHANGED
@@ -919,6 +919,21 @@ class ProjectIndex {
919
919
  }
920
920
  }
921
921
 
922
+ // For methods (symbols with className), objects can be passed as parameters
923
+ // to files with no import relationship to the definition file.
924
+ // Expand to all project files that mention the name (fast text pre-check).
925
+ if (symbol.className) {
926
+ for (const filePath of this.files.keys()) {
927
+ if (relevantFiles.has(filePath)) continue;
928
+ try {
929
+ const content = this._readFile(filePath);
930
+ if (content.includes(name)) {
931
+ relevantFiles.add(filePath);
932
+ }
933
+ } catch (e) { /* skip unreadable */ }
934
+ }
935
+ }
936
+
922
937
  let calls = 0;
923
938
  let definitions = 0;
924
939
  let imports = 0;
@@ -3393,7 +3408,38 @@ class ProjectIndex {
3393
3408
  let totalDynamic = 0;
3394
3409
  let totalTests = 0;
3395
3410
 
3411
+ // When file= is specified, scope to matching files only
3412
+ let fileFilter = null;
3413
+ if (options.file) {
3414
+ const resolved = this.findFile(options.file);
3415
+ if (resolved) {
3416
+ fileFilter = new Set([resolved]);
3417
+ } else {
3418
+ // Try substring match for partial paths
3419
+ const matching = [];
3420
+ for (const fp of this.files.keys()) {
3421
+ const rp = path.relative(this.root, fp);
3422
+ if (rp.includes(options.file) || fp.includes(options.file)) {
3423
+ matching.push(fp);
3424
+ }
3425
+ }
3426
+ if (matching.length > 0) {
3427
+ fileFilter = new Set(matching);
3428
+ } else {
3429
+ return {
3430
+ meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
3431
+ totals: { files: 0, lines: 0, functions: 0, classes: 0, state: 0, testFiles: 0 },
3432
+ summary: { topFunctionFiles: [], topLineFiles: [], entryFiles: [] },
3433
+ files: [],
3434
+ hiddenFiles: 0,
3435
+ error: `File not found in project: ${options.file}`
3436
+ };
3437
+ }
3438
+ }
3439
+ }
3440
+
3396
3441
  for (const [filePath, fileEntry] of this.files) {
3442
+ if (fileFilter && !fileFilter.has(filePath)) continue;
3397
3443
  let functions = fileEntry.symbols.filter(s =>
3398
3444
  s.type === 'function' || s.type === 'method' || s.type === 'static' ||
3399
3445
  s.type === 'constructor' || s.type === 'public' || s.type === 'abstract'
package/core/verify.js CHANGED
@@ -482,7 +482,23 @@ function plan(index, name, options = {}) {
482
482
  name: options.addParam,
483
483
  ...(options.defaultValue && { default: options.defaultValue })
484
484
  };
485
- newParams.push(newParam);
485
+
486
+ // When adding a required param (no default), insert before the first
487
+ // optional param to avoid producing invalid signatures in Python/TS
488
+ // (required params must precede optional ones).
489
+ if (!options.defaultValue) {
490
+ const firstOptIdx = newParams.findIndex(p => p.optional || p.default !== undefined);
491
+ if (firstOptIdx !== -1) {
492
+ // Also skip self/cls/&self/&mut self at position 0
493
+ const insertIdx = Math.max(firstOptIdx,
494
+ (newParams.length > 0 && ['self', 'cls', '&self', '&mut self'].includes(newParams[0].name)) ? 1 : 0);
495
+ newParams.splice(insertIdx, 0, newParam);
496
+ } else {
497
+ newParams.push(newParam);
498
+ }
499
+ } else {
500
+ newParams.push(newParam);
501
+ }
486
502
 
487
503
  // Generate new signature
488
504
  const paramsList = newParams.map(p => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.44",
3
+ "version": "3.7.45",
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",