ucn 3.7.43 → 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/callers.js +15 -4
- package/core/execute.js +27 -0
- package/core/project.js +46 -0
- package/core/verify.js +17 -1
- package/package.json +1 -1
package/core/callers.js
CHANGED
|
@@ -646,9 +646,21 @@ function findCallees(index, def, options = {}) {
|
|
|
646
646
|
// Respect includeMethods=false — skip self/this method resolution entirely
|
|
647
647
|
if (selfAttrCalls && def.className && options.includeMethods !== false) {
|
|
648
648
|
const attrTypes = getInstanceAttributeTypes(index, def.file, def.className);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
649
|
+
for (const call of selfAttrCalls) {
|
|
650
|
+
let targetClass = attrTypes ? attrTypes.get(call.selfAttribute) : null;
|
|
651
|
+
// Unique method heuristic: if attr type unknown but method exists on exactly one class
|
|
652
|
+
if (!targetClass) {
|
|
653
|
+
const methodSyms = index.symbols.get(call.name);
|
|
654
|
+
if (methodSyms) {
|
|
655
|
+
const classNames = new Set();
|
|
656
|
+
for (const s of methodSyms) {
|
|
657
|
+
if (s.className) classNames.add(s.className);
|
|
658
|
+
}
|
|
659
|
+
if (classNames.size === 1) {
|
|
660
|
+
targetClass = classNames.values().next().value;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
652
664
|
if (!targetClass) continue;
|
|
653
665
|
|
|
654
666
|
// Find method in symbol table where className matches
|
|
@@ -670,7 +682,6 @@ function findCallees(index, def, options = {}) {
|
|
|
670
682
|
});
|
|
671
683
|
}
|
|
672
684
|
}
|
|
673
|
-
}
|
|
674
685
|
}
|
|
675
686
|
|
|
676
687
|
// Third pass: resolve self/this/super.method() calls to same-class or parent methods
|
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
|
-
|
|
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.
|
|
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",
|