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 +2 -0
- package/core/output.js +4 -0
- package/core/project.js +55 -0
- package/core/verify.js +73 -1
- package/package.json +1 -1
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
|
-
|
|
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.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",
|