ucn 5.3.5 → 5.3.6
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/cache.js +2 -1
- package/core/callers.js +50 -2
- package/core/index-ir.js +7 -2
- package/languages/rust.js +12 -4
- package/package.json +1 -1
package/core/cache.js
CHANGED
|
@@ -710,7 +710,8 @@ function clearAllCaches() {
|
|
|
710
710
|
// v215: receiver-type provenance records the originating AST fact (#355).
|
|
711
711
|
// v224 (#355 recovery): Go declaration origins; Rust aliases, wrapper patterns,
|
|
712
712
|
// copied bindings and qualified macro receivers; TS indexed array evidence.
|
|
713
|
-
|
|
713
|
+
// v225 (fix #357): Rust `use path::name as local` bindings record the original name with a paired `renames` alias.
|
|
714
|
+
const CACHE_FORMAT_VERSION = 225;
|
|
714
715
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
715
716
|
|
|
716
717
|
/**
|
package/core/callers.js
CHANGED
|
@@ -2885,6 +2885,42 @@ function findCallers(index, name, options = {}) {
|
|
|
2885
2885
|
// modules must not exclude: a binding to an unresolved module
|
|
2886
2886
|
// whose first segment matches a project directory routes
|
|
2887
2887
|
// visible instead.
|
|
2888
|
+
// Rust item renames are name-level ownership (fix #357): a
|
|
2889
|
+
// bare call spelled by a `use path::name as local` alias
|
|
2890
|
+
// denotes exactly the item that binding resolves to. Two
|
|
2891
|
+
// renames of one source name from different modules
|
|
2892
|
+
// (`use alpha::widget as a; use beta::widget as b`) used to
|
|
2893
|
+
// fall through to file-level import evidence, which BOTH
|
|
2894
|
+
// modules satisfy, so each pin confirmed both sites. The
|
|
2895
|
+
// paired binding decides: resolves into a target file →
|
|
2896
|
+
// confirmable; resolves to another project file → excluded
|
|
2897
|
+
// other-definition-import; unresolvable → visible (a resolver
|
|
2898
|
+
// gap is never exclusion evidence).
|
|
2899
|
+
if (calledAs && !call.isMethod && !call.receiver &&
|
|
2900
|
+
fileEntry.language === 'rust' && call.name === calledAs) {
|
|
2901
|
+
const paired = (fileEntry.importBindings || []).filter(b =>
|
|
2902
|
+
b.alias === call.name);
|
|
2903
|
+
if (paired.length > 0) {
|
|
2904
|
+
const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
2905
|
+
let verdict = 'unknown';
|
|
2906
|
+
for (const b of paired) {
|
|
2907
|
+
const resolved = _rustBindingResolvedFiles(index, fileEntry, filePath, b);
|
|
2908
|
+
if (resolved.size === 0) { verdict = 'unknown'; break; }
|
|
2909
|
+
const hit = [...resolved].some(f => tFiles.has(f));
|
|
2910
|
+
if (hit) { verdict = 'target'; break; }
|
|
2911
|
+
verdict = 'other';
|
|
2912
|
+
}
|
|
2913
|
+
if (verdict === 'other') {
|
|
2914
|
+
recordExcluded(filePath, call.line, 'other-definition-import');
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
2917
|
+
if (verdict === 'unknown' && collectAccount) {
|
|
2918
|
+
routeUnverified(filePath, fileEntry, call, 'no-import-link', calledAs);
|
|
2919
|
+
continue;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2888
2924
|
if (!bindingId && !call.isMethod &&
|
|
2889
2925
|
langTraits(fileEntry.language)?.typeSystem === 'structural' &&
|
|
2890
2926
|
(fileEntry.importBindings || []).length > 0) {
|
|
@@ -4235,7 +4271,9 @@ function findCallers(index, name, options = {}) {
|
|
|
4235
4271
|
let aliasResolvedFile = null;
|
|
4236
4272
|
if (receiverName && !tTypes.has(receiverName)) {
|
|
4237
4273
|
for (const im of (fileEntry.importBindings || [])) {
|
|
4238
|
-
|
|
4274
|
+
// fix #357: Rust rename bindings carry the local
|
|
4275
|
+
// name as `alias` (the original is `name`).
|
|
4276
|
+
if ((im.alias || im.name) !== receiverName) continue;
|
|
4239
4277
|
// fix #353: C# `using BH = Beta.Helper` (and Java
|
|
4240
4278
|
// dotted paths) split on `.`; Rust paths on `::`.
|
|
4241
4279
|
const orig = String(im.module || '').split(/::|\./).pop();
|
|
@@ -11722,12 +11760,22 @@ function _calleeStructuralModuleRoute(index, fileEntry, call, language) {
|
|
|
11722
11760
|
}
|
|
11723
11761
|
|
|
11724
11762
|
function _calleeStructuralImportedNameRoute(index, fileEntry, call, language) {
|
|
11725
|
-
|
|
11763
|
+
let lookupName = call.resolvedName || call.name;
|
|
11726
11764
|
let bindings = (fileEntry?.importBindings || []).filter(b => b.name === lookupName);
|
|
11727
11765
|
if (call.resolvedName && bindings.some(b => b.alias)) {
|
|
11728
11766
|
const paired = bindings.filter(b => b.alias === call.name);
|
|
11729
11767
|
if (paired.length > 0) bindings = paired;
|
|
11730
11768
|
}
|
|
11769
|
+
// fix #357: a call spelled by a rename's LOCAL alias (Rust `use m::a as
|
|
11770
|
+
// b; b()`) pairs with exactly the binding carrying that alias, and the
|
|
11771
|
+
// exported item is the binding's ORIGINAL name.
|
|
11772
|
+
if (bindings.length === 0) {
|
|
11773
|
+
const aliased = (fileEntry?.importBindings || []).filter(b => b.alias === call.name);
|
|
11774
|
+
if (aliased.length > 0 && new Set(aliased.map(b => b.name)).size === 1) {
|
|
11775
|
+
bindings = aliased;
|
|
11776
|
+
lookupName = aliased[0].name;
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11731
11779
|
if (bindings.length === 0) return null;
|
|
11732
11780
|
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, lookupName, true);
|
|
11733
11781
|
}
|
package/core/index-ir.js
CHANGED
|
@@ -12,9 +12,14 @@ function createImportBindings(imports) {
|
|
|
12
12
|
return imports.flatMap(item => (item.names || [])
|
|
13
13
|
.filter(name => name && name !== '*' && name !== '_' && name !== '.')
|
|
14
14
|
.map(name => {
|
|
15
|
-
|
|
15
|
+
// A rename may be recorded under its original name (Python
|
|
16
|
+
// `from m import a as b` lists 'a') or under its local alias
|
|
17
|
+
// (Rust `use m::a as b` lists 'b', fix #357); both yield the
|
|
18
|
+
// binding {name: original, alias: local}.
|
|
19
|
+
const rename = (item.renames || []).find(candidate =>
|
|
20
|
+
candidate.original === name || candidate.local === name);
|
|
16
21
|
return {
|
|
17
|
-
name,
|
|
22
|
+
name: rename ? rename.original : name,
|
|
18
23
|
module: item.module,
|
|
19
24
|
...(item.type && { kind: item.type }),
|
|
20
25
|
...(item.line != null && { line: item.line }),
|
package/languages/rust.js
CHANGED
|
@@ -3221,7 +3221,7 @@ function findImportsInCode(code, parser) {
|
|
|
3221
3221
|
const right = String(suffix || '').replace(/^::/, '');
|
|
3222
3222
|
return left && right ? `${left}::${right}` : left || right;
|
|
3223
3223
|
};
|
|
3224
|
-
const addLeaf = (module, localName, type = 'use', dynamic = false, line) => {
|
|
3224
|
+
const addLeaf = (module, localName, type = 'use', dynamic = false, line, rename = null) => {
|
|
3225
3225
|
if (!module || !localName) return;
|
|
3226
3226
|
imports.push({
|
|
3227
3227
|
module,
|
|
@@ -3229,6 +3229,7 @@ function findImportsInCode(code, parser) {
|
|
|
3229
3229
|
type,
|
|
3230
3230
|
dynamic,
|
|
3231
3231
|
line,
|
|
3232
|
+
...(rename && { renames: [rename] }),
|
|
3232
3233
|
});
|
|
3233
3234
|
};
|
|
3234
3235
|
const collectUseTree = (node, prefix, line) => {
|
|
@@ -3250,13 +3251,20 @@ function findImportsInCode(code, parser) {
|
|
|
3250
3251
|
const pathNode = node.namedChild(0);
|
|
3251
3252
|
const aliasNode = node.childForFieldName('alias') || node.namedChild(1);
|
|
3252
3253
|
if (pathNode && aliasNode) {
|
|
3253
|
-
addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
|
|
3254
|
-
'use', false, line);
|
|
3255
3254
|
// fix #353: `use alpha::widget as renamed; renamed()` — the
|
|
3256
3255
|
// alias pairing feeds findCallers' import-rename surface
|
|
3257
3256
|
// (calledAs), exactly like Python/JS `import x as y`.
|
|
3257
|
+
// fix #357: `renames` pairs the alias with ITS record so
|
|
3258
|
+
// createImportBindings emits {name: original, alias: local}
|
|
3259
|
+
// (the #348 Python shape); two renames of one source name
|
|
3260
|
+
// from different modules each pair with their own module
|
|
3261
|
+
// instead of both bindings matching both calls. `names`
|
|
3262
|
+
// keeps the local spelling (parser contract, imports command).
|
|
3258
3263
|
const original = String(pathNode.text).split('::').pop();
|
|
3259
|
-
|
|
3264
|
+
const renamed = original && original !== aliasNode.text && original !== 'self';
|
|
3265
|
+
addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
|
|
3266
|
+
'use', false, line, renamed ? { original, local: aliasNode.text } : null);
|
|
3267
|
+
if (renamed) {
|
|
3260
3268
|
if (!imports.aliases) imports.aliases = [];
|
|
3261
3269
|
imports.aliases.push({ original, local: aliasNode.text });
|
|
3262
3270
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.6",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|