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 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
- const CACHE_FORMAT_VERSION = 224;
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
- if (im.name !== receiverName) continue;
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
- const lookupName = call.resolvedName || call.name;
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
- const rename = (item.renames || []).find(candidate => candidate.original === name);
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
- if (original && original !== aliasNode.text && original !== 'self') {
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.5",
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",