ucn 3.7.42 → 3.7.44
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 +2 -0
- package/core/output.js +4 -0
- package/core/project.js +29 -0
- package/core/verify.js +73 -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
|
@@ -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
|
@@ -2772,6 +2772,35 @@ class ProjectIndex {
|
|
|
2772
2772
|
}
|
|
2773
2773
|
}
|
|
2774
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
|
+
}
|
|
2775
2804
|
// className explicitly set but receiver type unknown — filter it out.
|
|
2776
2805
|
// User asked for a specific class; unknown receivers are likely unrelated.
|
|
2777
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
|
-
|
|
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.
|
|
3
|
+
"version": "3.7.44",
|
|
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",
|