ucn 3.7.30 → 3.7.32
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/cli/index.js +6 -3
- package/core/callers.js +27 -0
- package/core/execute.js +97 -7
- package/core/project.js +29 -12
- package/core/verify.js +11 -3
- package/mcp/server.js +5 -3
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -306,8 +306,9 @@ function runFileCommand(filePath, command, arg) {
|
|
|
306
306
|
api: { file: relativePath },
|
|
307
307
|
};
|
|
308
308
|
|
|
309
|
-
const { ok, result, error } = execute(index, canonical, paramsByCommand[canonical]);
|
|
309
|
+
const { ok, result, error, note } = execute(index, canonical, paramsByCommand[canonical]);
|
|
310
310
|
if (!ok) fail(error);
|
|
311
|
+
if (note) console.error(note);
|
|
311
312
|
|
|
312
313
|
// Format output using same formatters as project mode
|
|
313
314
|
switch (canonical) {
|
|
@@ -416,8 +417,9 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
416
417
|
}
|
|
417
418
|
|
|
418
419
|
case 'find': {
|
|
419
|
-
const { ok, result, error } = execute(index, 'find', { name: arg, ...flags });
|
|
420
|
+
const { ok, result, error, note } = execute(index, 'find', { name: arg, ...flags });
|
|
420
421
|
if (!ok) fail(error);
|
|
422
|
+
if (note) console.error(note);
|
|
421
423
|
printOutput(result,
|
|
422
424
|
r => output.formatSymbolJson(r, arg),
|
|
423
425
|
r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
|
|
@@ -1257,8 +1259,9 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1257
1259
|
}
|
|
1258
1260
|
|
|
1259
1261
|
case 'find': {
|
|
1260
|
-
const { ok, result, error } = execute(index, 'find', { name: arg, ...iflags });
|
|
1262
|
+
const { ok, result, error, note } = execute(index, 'find', { name: arg, ...iflags });
|
|
1261
1263
|
if (!ok) { console.log(error); return; }
|
|
1264
|
+
if (note) console.log(note);
|
|
1262
1265
|
console.log(output.formatFindDetailed(result, arg, { depth: iflags.depth, top: iflags.top, all: iflags.all }));
|
|
1263
1266
|
break;
|
|
1264
1267
|
}
|
package/core/callers.js
CHANGED
|
@@ -758,6 +758,33 @@ function findCallees(index, def, options = {}) {
|
|
|
758
758
|
if (nonTest) callee = nonTest;
|
|
759
759
|
}
|
|
760
760
|
}
|
|
761
|
+
// Priority 6: Usage-based tiebreaker for cross-language/cross-directory ambiguity
|
|
762
|
+
// Matches resolveSymbol() scoring logic in project.js
|
|
763
|
+
if (!bindingId && callee === symbols[0] && symbols.length > 1) {
|
|
764
|
+
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
765
|
+
const scored = symbols.map(s => {
|
|
766
|
+
let score = 0;
|
|
767
|
+
const fe = index.files.get(s.file);
|
|
768
|
+
const rp = fe ? fe.relativePath : (s.relativePath || '');
|
|
769
|
+
if (typeOrder.has(s.type)) score += 1000;
|
|
770
|
+
if (isTestFile(rp, detectLanguage(s.file))) score -= 500;
|
|
771
|
+
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
|
|
772
|
+
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
|
|
773
|
+
return { symbol: s, score };
|
|
774
|
+
});
|
|
775
|
+
scored.sort((a, b) => b.score - a.score);
|
|
776
|
+
if (scored.length > 1 && scored[0].score === scored[1].score) {
|
|
777
|
+
const tiedScore = scored[0].score;
|
|
778
|
+
const tiedCandidates = scored.filter(s => s.score === tiedScore);
|
|
779
|
+
for (const c of tiedCandidates) {
|
|
780
|
+
c.usageCount = index.countSymbolUsages(c.symbol).total;
|
|
781
|
+
}
|
|
782
|
+
tiedCandidates.sort((a, b) => b.usageCount - a.usageCount);
|
|
783
|
+
callee = tiedCandidates[0].symbol;
|
|
784
|
+
} else {
|
|
785
|
+
callee = scored[0].symbol;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
761
788
|
}
|
|
762
789
|
|
|
763
790
|
// Skip test-file callees when caller is production code and
|
package/core/execute.js
CHANGED
|
@@ -41,6 +41,38 @@ function requireTerm(term) {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Split Class.method syntax into className and methodName.
|
|
46
|
+
* Returns { className, methodName } or null if not applicable.
|
|
47
|
+
* Handles: "Class.method" → { className: "Class", methodName: "method" }
|
|
48
|
+
* Skips: ".method", "a.b.c" (multi-dot), names without dots
|
|
49
|
+
*/
|
|
50
|
+
function splitClassMethod(name) {
|
|
51
|
+
if (!name || typeof name !== 'string') return null;
|
|
52
|
+
const dotIndex = name.indexOf('.');
|
|
53
|
+
if (dotIndex <= 0 || dotIndex === name.length - 1) return null;
|
|
54
|
+
// Only split on first dot, and only if there's exactly one dot
|
|
55
|
+
if (name.indexOf('.', dotIndex + 1) !== -1) return null;
|
|
56
|
+
return {
|
|
57
|
+
className: name.substring(0, dotIndex),
|
|
58
|
+
methodName: name.substring(dotIndex + 1)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply Class.method syntax to params object.
|
|
64
|
+
* If name contains ".", splits it and sets p.name and p.className.
|
|
65
|
+
* Only applies if p.className is not already set.
|
|
66
|
+
*/
|
|
67
|
+
function applyClassMethodSyntax(p) {
|
|
68
|
+
if (p.className) return; // already set explicitly
|
|
69
|
+
const split = splitClassMethod(p.name);
|
|
70
|
+
if (split) {
|
|
71
|
+
p.name = split.methodName;
|
|
72
|
+
p.className = split.className;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
44
76
|
/** Normalize exclude to an array (accepts string CSV, array, or falsy). */
|
|
45
77
|
function toExcludeArray(exclude) {
|
|
46
78
|
if (!exclude) return [];
|
|
@@ -93,9 +125,11 @@ const HANDLERS = {
|
|
|
93
125
|
about: (index, p) => {
|
|
94
126
|
const err = requireName(p.name);
|
|
95
127
|
if (err) return { ok: false, error: err };
|
|
128
|
+
applyClassMethodSyntax(p);
|
|
96
129
|
const result = index.about(p.name, {
|
|
97
130
|
withTypes: p.withTypes || false,
|
|
98
131
|
file: p.file,
|
|
132
|
+
className: p.className,
|
|
99
133
|
all: p.all,
|
|
100
134
|
includeMethods: p.includeMethods,
|
|
101
135
|
includeUncertain: p.includeUncertain || false,
|
|
@@ -103,17 +137,36 @@ const HANDLERS = {
|
|
|
103
137
|
maxCallers: num(p.top, undefined),
|
|
104
138
|
maxCallees: num(p.top, undefined),
|
|
105
139
|
});
|
|
106
|
-
if (!result)
|
|
140
|
+
if (!result) {
|
|
141
|
+
// Give better error if file/className filter is the problem
|
|
142
|
+
if (p.file || p.className) {
|
|
143
|
+
const unfiltered = index.about(p.name, {
|
|
144
|
+
withTypes: p.withTypes || false,
|
|
145
|
+
all: false,
|
|
146
|
+
includeMethods: p.includeMethods,
|
|
147
|
+
includeUncertain: p.includeUncertain || false,
|
|
148
|
+
exclude: toExcludeArray(p.exclude),
|
|
149
|
+
});
|
|
150
|
+
if (unfiltered && unfiltered.found !== false && unfiltered.symbol) {
|
|
151
|
+
const loc = `${unfiltered.symbol.file}:${unfiltered.symbol.startLine}`;
|
|
152
|
+
const filterDesc = p.className ? `class "${p.className}"` : `file "${p.file}"`;
|
|
153
|
+
return { ok: false, error: `Symbol "${p.name}" not found in ${filterDesc}. Found in: ${loc}. Use the correct --file or Class.method syntax.` };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
157
|
+
}
|
|
107
158
|
return { ok: true, result };
|
|
108
159
|
},
|
|
109
160
|
|
|
110
161
|
context: (index, p) => {
|
|
111
162
|
const err = requireName(p.name);
|
|
112
163
|
if (err) return { ok: false, error: err };
|
|
164
|
+
applyClassMethodSyntax(p);
|
|
113
165
|
const result = index.context(p.name, {
|
|
114
166
|
includeMethods: p.includeMethods,
|
|
115
167
|
includeUncertain: p.includeUncertain || false,
|
|
116
168
|
file: p.file,
|
|
169
|
+
className: p.className,
|
|
117
170
|
exclude: toExcludeArray(p.exclude),
|
|
118
171
|
});
|
|
119
172
|
if (!result) return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
@@ -123,8 +176,10 @@ const HANDLERS = {
|
|
|
123
176
|
impact: (index, p) => {
|
|
124
177
|
const err = requireName(p.name);
|
|
125
178
|
if (err) return { ok: false, error: err };
|
|
179
|
+
applyClassMethodSyntax(p);
|
|
126
180
|
const result = index.impact(p.name, {
|
|
127
181
|
file: p.file,
|
|
182
|
+
className: p.className,
|
|
128
183
|
exclude: toExcludeArray(p.exclude),
|
|
129
184
|
top: num(p.top, undefined),
|
|
130
185
|
});
|
|
@@ -135,8 +190,10 @@ const HANDLERS = {
|
|
|
135
190
|
smart: (index, p) => {
|
|
136
191
|
const err = requireName(p.name);
|
|
137
192
|
if (err) return { ok: false, error: err };
|
|
193
|
+
applyClassMethodSyntax(p);
|
|
138
194
|
const result = index.smart(p.name, {
|
|
139
195
|
file: p.file,
|
|
196
|
+
className: p.className,
|
|
140
197
|
withTypes: p.withTypes || false,
|
|
141
198
|
includeMethods: p.includeMethods,
|
|
142
199
|
includeUncertain: p.includeUncertain || false,
|
|
@@ -148,10 +205,12 @@ const HANDLERS = {
|
|
|
148
205
|
trace: (index, p) => {
|
|
149
206
|
const err = requireName(p.name);
|
|
150
207
|
if (err) return { ok: false, error: err };
|
|
208
|
+
applyClassMethodSyntax(p);
|
|
151
209
|
const depthVal = num(p.depth, undefined);
|
|
152
210
|
const result = index.trace(p.name, {
|
|
153
211
|
depth: depthVal ?? 3,
|
|
154
212
|
file: p.file,
|
|
213
|
+
className: p.className,
|
|
155
214
|
all: p.all || depthVal !== undefined,
|
|
156
215
|
includeMethods: p.includeMethods,
|
|
157
216
|
includeUncertain: p.includeUncertain || false,
|
|
@@ -163,7 +222,8 @@ const HANDLERS = {
|
|
|
163
222
|
example: (index, p) => {
|
|
164
223
|
const err = requireName(p.name);
|
|
165
224
|
if (err) return { ok: false, error: err };
|
|
166
|
-
|
|
225
|
+
applyClassMethodSyntax(p);
|
|
226
|
+
const result = index.example(p.name, { file: p.file, className: p.className });
|
|
167
227
|
if (!result) return { ok: false, error: `No examples found for "${p.name}".` };
|
|
168
228
|
return { ok: true, result };
|
|
169
229
|
},
|
|
@@ -171,8 +231,10 @@ const HANDLERS = {
|
|
|
171
231
|
related: (index, p) => {
|
|
172
232
|
const err = requireName(p.name);
|
|
173
233
|
if (err) return { ok: false, error: err };
|
|
234
|
+
applyClassMethodSyntax(p);
|
|
174
235
|
const result = index.related(p.name, {
|
|
175
236
|
file: p.file,
|
|
237
|
+
className: p.className,
|
|
176
238
|
top: num(p.top, undefined),
|
|
177
239
|
all: p.all,
|
|
178
240
|
});
|
|
@@ -185,6 +247,7 @@ const HANDLERS = {
|
|
|
185
247
|
find: (index, p) => {
|
|
186
248
|
const err = requireName(p.name);
|
|
187
249
|
if (err) return { ok: false, error: err };
|
|
250
|
+
applyClassMethodSyntax(p);
|
|
188
251
|
// Auto-include tests when pattern clearly targets test functions
|
|
189
252
|
// But only if the user didn't explicitly set include_tests=false
|
|
190
253
|
let includeTests = p.includeTests;
|
|
@@ -194,20 +257,28 @@ const HANDLERS = {
|
|
|
194
257
|
const exclude = applyTestExclusions(p.exclude, includeTests);
|
|
195
258
|
const result = index.find(p.name, {
|
|
196
259
|
file: p.file,
|
|
260
|
+
className: p.className,
|
|
197
261
|
exact: p.exact || false,
|
|
198
262
|
exclude,
|
|
199
263
|
in: p.in,
|
|
200
264
|
});
|
|
201
|
-
|
|
265
|
+
// Warn if exact mode silently disables glob expansion
|
|
266
|
+
let note;
|
|
267
|
+
if (p.exact && p.name && (p.name.includes('*') || p.name.includes('?'))) {
|
|
268
|
+
note = `Note: exact=true treats "${p.name}" as a literal name (glob expansion disabled).`;
|
|
269
|
+
}
|
|
270
|
+
return { ok: true, result, note };
|
|
202
271
|
},
|
|
203
272
|
|
|
204
273
|
usages: (index, p) => {
|
|
205
274
|
const err = requireName(p.name);
|
|
206
275
|
if (err) return { ok: false, error: err };
|
|
276
|
+
applyClassMethodSyntax(p);
|
|
207
277
|
const exclude = applyTestExclusions(p.exclude, p.includeTests);
|
|
208
278
|
const result = index.usages(p.name, {
|
|
209
279
|
codeOnly: p.codeOnly || false,
|
|
210
280
|
context: num(p.context, 0),
|
|
281
|
+
className: p.className,
|
|
211
282
|
exclude,
|
|
212
283
|
in: p.in,
|
|
213
284
|
});
|
|
@@ -242,8 +313,10 @@ const HANDLERS = {
|
|
|
242
313
|
tests: (index, p) => {
|
|
243
314
|
const err = requireName(p.name);
|
|
244
315
|
if (err) return { ok: false, error: err };
|
|
316
|
+
applyClassMethodSyntax(p);
|
|
245
317
|
const result = index.tests(p.name, {
|
|
246
318
|
callsOnly: p.callsOnly || false,
|
|
319
|
+
className: p.className,
|
|
247
320
|
});
|
|
248
321
|
return { ok: true, result };
|
|
249
322
|
},
|
|
@@ -264,6 +337,7 @@ const HANDLERS = {
|
|
|
264
337
|
fn: (index, p) => {
|
|
265
338
|
const err = requireName(p.name);
|
|
266
339
|
if (err) return { ok: false, error: err };
|
|
340
|
+
applyClassMethodSyntax(p);
|
|
267
341
|
|
|
268
342
|
const fnNames = p.name.includes(',')
|
|
269
343
|
? p.name.split(',').map(n => n.trim()).filter(Boolean)
|
|
@@ -273,11 +347,23 @@ const HANDLERS = {
|
|
|
273
347
|
const notes = [];
|
|
274
348
|
|
|
275
349
|
for (const fnName of fnNames) {
|
|
276
|
-
|
|
350
|
+
// For comma-separated names, each may have Class.method syntax
|
|
351
|
+
const fnSplit = splitClassMethod(fnName);
|
|
352
|
+
const actualName = fnSplit ? fnSplit.methodName : fnName;
|
|
353
|
+
const fnClassName = fnSplit ? fnSplit.className : p.className;
|
|
354
|
+
const matches = index.find(actualName, { file: p.file, className: fnClassName, skipCounts: true })
|
|
277
355
|
.filter(m => m.type === 'function' || m.params !== undefined);
|
|
278
356
|
|
|
279
357
|
if (matches.length === 0) {
|
|
280
|
-
|
|
358
|
+
// Check if it's a class — suggest `class` command instead
|
|
359
|
+
const CLASS_TYPES = ['class', 'interface', 'type', 'enum', 'struct', 'trait'];
|
|
360
|
+
const classMatches = index.find(actualName, { file: p.file, className: fnClassName, skipCounts: true })
|
|
361
|
+
.filter(m => CLASS_TYPES.includes(m.type));
|
|
362
|
+
if (classMatches.length > 0) {
|
|
363
|
+
notes.push(`"${fnName}" is a ${classMatches[0].type}, not a function. Use \`class ${fnName}\` instead.`);
|
|
364
|
+
} else {
|
|
365
|
+
notes.push(`Function "${fnName}" not found.`);
|
|
366
|
+
}
|
|
281
367
|
continue;
|
|
282
368
|
}
|
|
283
369
|
|
|
@@ -472,13 +558,15 @@ const HANDLERS = {
|
|
|
472
558
|
verify: (index, p) => {
|
|
473
559
|
const err = requireName(p.name);
|
|
474
560
|
if (err) return { ok: false, error: err };
|
|
475
|
-
|
|
561
|
+
applyClassMethodSyntax(p);
|
|
562
|
+
const result = index.verify(p.name, { file: p.file, className: p.className });
|
|
476
563
|
return { ok: true, result };
|
|
477
564
|
},
|
|
478
565
|
|
|
479
566
|
plan: (index, p) => {
|
|
480
567
|
const err = requireName(p.name);
|
|
481
568
|
if (err) return { ok: false, error: err };
|
|
569
|
+
applyClassMethodSyntax(p);
|
|
482
570
|
if (!p.addParam && !p.removeParam && !p.renameTo) {
|
|
483
571
|
return { ok: false, error: 'Plan requires an operation: add_param, remove_param, or rename_to.' };
|
|
484
572
|
}
|
|
@@ -488,6 +576,7 @@ const HANDLERS = {
|
|
|
488
576
|
renameTo: p.renameTo,
|
|
489
577
|
defaultValue: p.defaultValue,
|
|
490
578
|
file: p.file,
|
|
579
|
+
className: p.className,
|
|
491
580
|
});
|
|
492
581
|
return { ok: true, result };
|
|
493
582
|
},
|
|
@@ -506,7 +595,8 @@ const HANDLERS = {
|
|
|
506
595
|
typedef: (index, p) => {
|
|
507
596
|
const err = requireName(p.name);
|
|
508
597
|
if (err) return { ok: false, error: err };
|
|
509
|
-
|
|
598
|
+
applyClassMethodSyntax(p);
|
|
599
|
+
const result = index.typedef(p.name, { exact: p.exact || false, className: p.className });
|
|
510
600
|
return { ok: true, result };
|
|
511
601
|
},
|
|
512
602
|
|
package/core/project.js
CHANGED
|
@@ -700,6 +700,14 @@ class ProjectIndex {
|
|
|
700
700
|
return { def: null, definitions: [], warnings: [] };
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
// Filter by class name (Class.method syntax)
|
|
704
|
+
if (options.className) {
|
|
705
|
+
const filtered = definitions.filter(d => d.className === options.className);
|
|
706
|
+
if (filtered.length > 0) {
|
|
707
|
+
definitions = filtered;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
703
711
|
// Filter by file if specified
|
|
704
712
|
if (options.file) {
|
|
705
713
|
const filtered = definitions.filter(d =>
|
|
@@ -826,6 +834,11 @@ class ProjectIndex {
|
|
|
826
834
|
_applyFindFilters(matches, options) {
|
|
827
835
|
let filtered = matches;
|
|
828
836
|
|
|
837
|
+
// Filter by class name (Class.method syntax)
|
|
838
|
+
if (options.className) {
|
|
839
|
+
filtered = filtered.filter(m => m.className === options.className);
|
|
840
|
+
}
|
|
841
|
+
|
|
829
842
|
// Filter by file pattern
|
|
830
843
|
if (options.file) {
|
|
831
844
|
filtered = filtered.filter(m =>
|
|
@@ -958,7 +971,10 @@ class ProjectIndex {
|
|
|
958
971
|
const usages = [];
|
|
959
972
|
|
|
960
973
|
// Get definitions (filtered)
|
|
961
|
-
|
|
974
|
+
let allDefinitions = this.symbols.get(name) || [];
|
|
975
|
+
if (options.className) {
|
|
976
|
+
allDefinitions = allDefinitions.filter(d => d.className === options.className);
|
|
977
|
+
}
|
|
962
978
|
const definitions = options.exclude || options.in
|
|
963
979
|
? allDefinitions.filter(d => this.matchesFilters(d.relativePath, options))
|
|
964
980
|
: allDefinitions;
|
|
@@ -1150,7 +1166,7 @@ class ProjectIndex {
|
|
|
1150
1166
|
context(name, options = {}) {
|
|
1151
1167
|
this._beginOp();
|
|
1152
1168
|
try {
|
|
1153
|
-
const resolved = this.resolveSymbol(name, { file: options.file });
|
|
1169
|
+
const resolved = this.resolveSymbol(name, { file: options.file, className: options.className });
|
|
1154
1170
|
let { def, definitions, warnings } = resolved;
|
|
1155
1171
|
if (!def) {
|
|
1156
1172
|
return null;
|
|
@@ -1310,7 +1326,7 @@ class ProjectIndex {
|
|
|
1310
1326
|
smart(name, options = {}) {
|
|
1311
1327
|
this._beginOp();
|
|
1312
1328
|
try {
|
|
1313
|
-
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
1329
|
+
const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
|
|
1314
1330
|
if (!def) {
|
|
1315
1331
|
return null;
|
|
1316
1332
|
}
|
|
@@ -2362,7 +2378,7 @@ class ProjectIndex {
|
|
|
2362
2378
|
related(name, options = {}) {
|
|
2363
2379
|
this._beginOp();
|
|
2364
2380
|
try {
|
|
2365
|
-
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2381
|
+
const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
|
|
2366
2382
|
if (!def) {
|
|
2367
2383
|
return null;
|
|
2368
2384
|
}
|
|
@@ -2404,7 +2420,7 @@ class ProjectIndex {
|
|
|
2404
2420
|
const symParts = symName.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase().split('_');
|
|
2405
2421
|
|
|
2406
2422
|
// Check for shared parts (require ≥50% of the longer name to match)
|
|
2407
|
-
const sharedParts = nameParts.filter(p => symParts.includes(p) && p.length >
|
|
2423
|
+
const sharedParts = nameParts.filter(p => symParts.includes(p) && p.length > 3);
|
|
2408
2424
|
const maxParts = Math.max(nameParts.length, symParts.length);
|
|
2409
2425
|
if (sharedParts.length > 0 && sharedParts.length / maxParts >= 0.5) {
|
|
2410
2426
|
const sym = symbols[0];
|
|
@@ -2513,7 +2529,7 @@ class ProjectIndex {
|
|
|
2513
2529
|
// trace defaults to includeMethods=true (execution flow should show method calls)
|
|
2514
2530
|
const includeMethods = options.includeMethods ?? true;
|
|
2515
2531
|
|
|
2516
|
-
const { def, definitions, warnings } = this.resolveSymbol(name, { file: options.file });
|
|
2532
|
+
const { def, definitions, warnings } = this.resolveSymbol(name, { file: options.file, className: options.className });
|
|
2517
2533
|
if (!def) {
|
|
2518
2534
|
return null;
|
|
2519
2535
|
}
|
|
@@ -2619,7 +2635,7 @@ class ProjectIndex {
|
|
|
2619
2635
|
impact(name, options = {}) {
|
|
2620
2636
|
this._beginOp();
|
|
2621
2637
|
try {
|
|
2622
|
-
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2638
|
+
const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
|
|
2623
2639
|
if (!def) {
|
|
2624
2640
|
return null;
|
|
2625
2641
|
}
|
|
@@ -2770,13 +2786,13 @@ class ProjectIndex {
|
|
|
2770
2786
|
try {
|
|
2771
2787
|
const maxCallers = options.all ? Infinity : (options.maxCallers || 10);
|
|
2772
2788
|
const maxCallees = options.all ? Infinity : (options.maxCallees || 10);
|
|
2773
|
-
const includeMethods = options.includeMethods ??
|
|
2789
|
+
const includeMethods = options.includeMethods ?? false;
|
|
2774
2790
|
|
|
2775
2791
|
// Find symbol definition(s) — skip counts since about() computes its own via usages()
|
|
2776
|
-
const definitions = this.find(name, { exact: true, file: options.file, skipCounts: true });
|
|
2792
|
+
const definitions = this.find(name, { exact: true, file: options.file, className: options.className, skipCounts: true });
|
|
2777
2793
|
if (definitions.length === 0) {
|
|
2778
2794
|
// Try fuzzy match (needs counts for suggestion ranking)
|
|
2779
|
-
const fuzzy = this.find(name, { file: options.file });
|
|
2795
|
+
const fuzzy = this.find(name, { file: options.file, className: options.className });
|
|
2780
2796
|
if (fuzzy.length === 0) {
|
|
2781
2797
|
return null;
|
|
2782
2798
|
}
|
|
@@ -2794,7 +2810,7 @@ class ProjectIndex {
|
|
|
2794
2810
|
}
|
|
2795
2811
|
|
|
2796
2812
|
// Use resolveSymbol for consistent primary selection (prefers non-test files)
|
|
2797
|
-
const { def: resolved } = this.resolveSymbol(name, { file: options.file });
|
|
2813
|
+
const { def: resolved } = this.resolveSymbol(name, { file: options.file, className: options.className });
|
|
2798
2814
|
const primary = resolved || definitions[0];
|
|
2799
2815
|
const others = definitions.filter(d =>
|
|
2800
2816
|
d.relativePath !== primary.relativePath || d.startLine !== primary.startLine
|
|
@@ -3258,11 +3274,12 @@ class ProjectIndex {
|
|
|
3258
3274
|
* @param {string} name - Symbol name
|
|
3259
3275
|
* @returns {{ best: object, totalCalls: number } | null}
|
|
3260
3276
|
*/
|
|
3261
|
-
example(name) {
|
|
3277
|
+
example(name, options = {}) {
|
|
3262
3278
|
this._beginOp();
|
|
3263
3279
|
try {
|
|
3264
3280
|
const usages = this.usages(name, {
|
|
3265
3281
|
codeOnly: true,
|
|
3282
|
+
className: options.className,
|
|
3266
3283
|
exclude: ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'],
|
|
3267
3284
|
context: 5
|
|
3268
3285
|
});
|
package/core/verify.js
CHANGED
|
@@ -177,7 +177,7 @@ function identifyCallPatterns(callSites, funcName) {
|
|
|
177
177
|
function verify(index, name, options = {}) {
|
|
178
178
|
index._beginOp();
|
|
179
179
|
try {
|
|
180
|
-
const { def } = index.resolveSymbol(name, { file: options.file });
|
|
180
|
+
const { def } = index.resolveSymbol(name, { file: options.file, className: options.className });
|
|
181
181
|
if (!def) {
|
|
182
182
|
return { found: false, function: name };
|
|
183
183
|
}
|
|
@@ -315,9 +315,9 @@ function plan(index, name, options = {}) {
|
|
|
315
315
|
return { found: false, function: name };
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
const resolved = index.resolveSymbol(name, { file: options.file });
|
|
318
|
+
const resolved = index.resolveSymbol(name, { file: options.file, className: options.className });
|
|
319
319
|
const def = resolved.def || definitions[0];
|
|
320
|
-
const impact = index.impact(name, { file: options.file });
|
|
320
|
+
const impact = index.impact(name, { file: options.file, className: options.className });
|
|
321
321
|
const currentParams = def.paramsStructured || [];
|
|
322
322
|
const currentSignature = index.formatSignature(def);
|
|
323
323
|
|
|
@@ -327,6 +327,14 @@ function plan(index, name, options = {}) {
|
|
|
327
327
|
let changes = [];
|
|
328
328
|
|
|
329
329
|
if (options.addParam) {
|
|
330
|
+
// Check if parameter already exists
|
|
331
|
+
if (currentParams.some(p => p.name === options.addParam)) {
|
|
332
|
+
return {
|
|
333
|
+
found: true,
|
|
334
|
+
error: `Parameter "${options.addParam}" already exists in ${name}`,
|
|
335
|
+
currentParams: currentParams.map(p => p.name)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
330
338
|
operation = 'add-param';
|
|
331
339
|
const newParam = {
|
|
332
340
|
name: options.addParam,
|
package/mcp/server.js
CHANGED
|
@@ -297,7 +297,7 @@ server.registerTool(
|
|
|
297
297
|
|
|
298
298
|
case 'impact': {
|
|
299
299
|
const index = getIndex(project_dir);
|
|
300
|
-
const ep = normalizeParams({ name, file, exclude });
|
|
300
|
+
const ep = normalizeParams({ name, file, exclude, top });
|
|
301
301
|
const { ok, result, error } = execute(index, 'impact', ep);
|
|
302
302
|
if (!ok) return toolResult(error); // soft error
|
|
303
303
|
return toolResult(output.formatImpact(result));
|
|
@@ -346,9 +346,11 @@ server.registerTool(
|
|
|
346
346
|
case 'find': {
|
|
347
347
|
const index = getIndex(project_dir);
|
|
348
348
|
const ep = normalizeParams({ name, file, exclude, include_tests, exact, in: inPath });
|
|
349
|
-
const { ok, result, error } = execute(index, 'find', ep);
|
|
349
|
+
const { ok, result, error, note } = execute(index, 'find', ep);
|
|
350
350
|
if (!ok) return toolResult(error); // soft error
|
|
351
|
-
|
|
351
|
+
let text = output.formatFind(result, name, top);
|
|
352
|
+
if (note) text += '\n\n' + note;
|
|
353
|
+
return toolResult(text);
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
case 'usages': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.32",
|
|
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",
|