ucn 5.5.0 → 5.5.1

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.
@@ -89,11 +89,29 @@ function moduleTree(index) {
89
89
  */
90
90
  function rustPathModule(index, filePath, line, qualifier) {
91
91
  if (!isRustEntry(index.files.get(filePath)) || !qualifier) return null;
92
+ const tree = moduleTree(index);
93
+ const inline = tree.inlineByFile.get(filePath) || [];
94
+ // The answer depends on the line only through the inline modules around
95
+ // it (fix #405: path calls asked once per site; fix #405b: files with
96
+ // inline modules too).
97
+ const memo = tree.pathMemo || (tree.pathMemo = new Map());
98
+ let byFile = memo.get(filePath);
99
+ if (!byFile) memo.set(filePath, byFile = new Map());
100
+ const chain = inline.length > 0 ? rustInlineChainAt(inline, line) : null;
101
+ const key = chain?.length > 0 ? `${chain.join('::')}\0${qualifier}` : qualifier;
102
+ let resolved = byFile.get(key);
103
+ if (resolved === undefined) {
104
+ resolved = resolvePathModule(tree, filePath, line, qualifier, inline);
105
+ byFile.set(key, resolved);
106
+ }
107
+ return resolved;
108
+ }
109
+
110
+ function resolvePathModule(tree, filePath, line, qualifier, inline) {
92
111
  const segments = String(qualifier).split('::');
93
112
  if (segments.some(segment => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment))) return null;
94
- const tree = moduleTree(index);
95
113
  const own = tree.moduleOf(filePath);
96
- const current = [...own.segs, ...rustInlineChainAt(tree.inlineByFile.get(filePath) || [], line)];
114
+ const current = [...own.segs, ...rustInlineChainAt(inline, line)];
97
115
  const has = segs => tree.entries.has(`${own.root}\0${segs.join('::')}`);
98
116
  let segs;
99
117
  let i = 0;
@@ -774,11 +774,14 @@ function rustScopedLine(index, filePath, line, name) {
774
774
  if (line == null) return null;
775
775
  const fileEntry = index.files.get(filePath);
776
776
  if (fileEntry?.language !== 'rust') return null;
777
- const declares = (index.symbols.get(name) || []).some(definition => definition.file === filePath &&
778
- (TYPE_KINDS.has(definition.type) || definition.type === 'type'));
779
- const imports = (fileEntry.importBindings || []).some(binding =>
780
- binding.name !== '*' && (binding.alias || binding.name) === name && binding.module);
781
- return declares || imports ? line : null;
777
+ // Per file and name (fix #405: every `T::new()` site asked again).
778
+ const scoped = memoized(index, `scoped\x00${filePath}\x00${name}`, () => {
779
+ const declares = (index.symbols.get(name) || []).some(definition => definition.file === filePath &&
780
+ (TYPE_KINDS.has(definition.type) || definition.type === 'type'));
781
+ return declares || (fileEntry.importBindings || []).some(binding =>
782
+ binding.name !== '*' && (binding.alias || binding.name) === name && binding.module);
783
+ });
784
+ return scoped ? line : null;
782
785
  }
783
786
 
784
787
  /** Bindings visible at `line`: those of the innermost enclosing scope that
@@ -946,6 +949,30 @@ function projectTypeNamed(index, name) {
946
949
  return (index.symbols.get(name) || []).some(definition => TYPE_KINDS.has(definition.type));
947
950
  }
948
951
 
952
+ /**
953
+ * The type a call record writes and what it denotes in its file. A record
954
+ * never changes once its file's calls are read, so the answer is kept per
955
+ * record object while the index lives (fix #405b: orientation refines every
956
+ * definition of a name over the same records, and each refinement asked
957
+ * again per record).
958
+ */
959
+ function siteDenotation(index, filePath, fileEntry, call) {
960
+ const memos = memoOf(index);
961
+ let memo = memos.get('\x01site');
962
+ if (!memo) memos.set('\x01site', memo = new WeakMap());
963
+ let site = memo.get(call);
964
+ if (site === undefined) {
965
+ const written = siteWrittenType(index, fileEntry, call);
966
+ site = {
967
+ written,
968
+ denotation: written ? externalTypeDenotation(index, filePath, written.name, written.qualifier,
969
+ call.line, { unscoped: written.unscoped === true }) : null,
970
+ };
971
+ memo.set(call, site);
972
+ }
973
+ return site;
974
+ }
975
+
949
976
  /**
950
977
  * Verdict for a call site against candidate target definitions (the pinned
951
978
  * targets on the caller side, every same-name callable on the callee side).
@@ -961,10 +988,7 @@ function externalTypeGate(index, filePath, fileEntry, call, targetsOrThunk) {
961
988
  // every project type with the method set satisfies; the existing
962
989
  // package-qualified physics routes it (possible dispatch).
963
990
  if (fileEntry.language === 'go') return null;
964
- const written = siteWrittenType(index, fileEntry, call);
965
- if (!written) return null;
966
- const denotation = externalTypeDenotation(index, filePath, written.name, written.qualifier, call.line,
967
- { unscoped: written.unscoped === true });
991
+ const { written, denotation } = siteDenotation(index, filePath, fileEntry, call);
968
992
  if (!denotation) return null;
969
993
  const { original, via } = denotation;
970
994
  // Only a real collision needs the gate: without a project type of that
package/languages/rust.js CHANGED
@@ -126,6 +126,16 @@ function buildMacroItemRecoveryTree(code, parser, tree) {
126
126
  return { tree: recovered, itemCount, declarationNameStarts, callRecovery };
127
127
  }
128
128
 
129
+ /**
130
+ * The item-body call recovery of an expanded file parsed by the caller
131
+ * (fix #405): what rustMacroItemCallRecovery computes for it once its
132
+ * declaration trees are primed with `tree`. Its walk builds the tree's flat
133
+ * node list, which the call extraction then reads.
134
+ */
135
+ function rustExpandedItemRecovery(code, parser, tree) {
136
+ return buildMacroItemRecoveryTree(code, parser, tree)?.callRecovery || null;
137
+ }
138
+
129
139
  /**
130
140
  * Rust macro invocation bodies are token trees, even when they contain item
131
141
  * declarations verbatim. Reparse only item-position bodies in a byte- and
@@ -2193,13 +2203,19 @@ const DECLARATION_NODE_TYPES = [
2193
2203
  * extracts them (fix #374: the declarations a macro expansion generates,
2194
2204
  * without walking the rest of the expanded file).
2195
2205
  */
2196
- function parseDeclarationsIn(code, roots) {
2206
+ function parseDeclarationsIn(code, roots, { descendants = null } = {}) {
2197
2207
  const lines = code.split('\n');
2198
2208
  const functions = [], classes = [], stateObjects = [];
2199
2209
  const processedFn = new Set(), processedCls = new Set();
2200
- for (const root of roots) {
2210
+ // `descendants(root, i)`: the root's descendant nodes the caller already
2211
+ // listed (a superset of DECLARATION_NODE_TYPES, in document order).
2212
+ for (let i = 0; i < roots.length; i++) {
2213
+ const root = roots[i];
2201
2214
  const nodes = DECLARATION_NODE_TYPES.includes(root.type) ? [root] : [];
2202
- for (const node of nodes.concat(root.descendantsOfType(DECLARATION_NODE_TYPES))) {
2215
+ const below = descendants
2216
+ ? descendants(root, i).filter(node => DECLARATION_NODE_TYPES.includes(node.type))
2217
+ : root.descendantsOfType(DECLARATION_NODE_TYPES);
2218
+ for (const node of nodes.concat(below)) {
2203
2219
  _processFunction(node, functions, processedFn, lines, code);
2204
2220
  _processClass(node, classes, processedCls, lines, code);
2205
2221
  _processState(node, stateObjects, lines);
@@ -3737,8 +3753,17 @@ function rustFieldAliasOf(identNode) {
3737
3753
  return null;
3738
3754
  }
3739
3755
 
3740
- function findCallsInCode(code, parser) {
3741
- const tree = parseTree(parser, code);
3756
+ /**
3757
+ * Call records of Rust code. `options` (fix #405, macro-expansion calls):
3758
+ * `tree` - the already parsed tree of `code` (an expanded file parsed with
3759
+ * included ranges), whose item-recovery facts are `itemRecovery` (null when
3760
+ * none); `skip(node)` - nodes whose subtrees are not read (other units of an
3761
+ * expanded file: every scope the extractor keeps is a function's own, so a
3762
+ * unit's records do not depend on its sibling units).
3763
+ */
3764
+ function findCallsInCode(code, parser, options = {}) {
3765
+ const tree = options.tree || parseTree(parser, code);
3766
+ const skip = typeof options.skip === 'function' ? options.skip : null;
3742
3767
  const calls = [];
3743
3768
  // Words of every value-binding pattern seen so far in document order
3744
3769
  // (fix #369). A binding shadows a bare call only if it precedes the call,
@@ -4378,7 +4403,7 @@ function findCallsInCode(code, parser) {
4378
4403
  // Item bodies of item-position macros (`quickcheck! { fn p(..) { .. } }`,
4379
4404
  // fix #401) are parsed as items: their calls are read from that parse,
4380
4405
  // with their functions' scopes and local types, instead of as tokens.
4381
- const itemRecovery = rustMacroItemCallRecovery(code, parser);
4406
+ const itemRecovery = options.tree ? (options.itemRecovery || null) : rustMacroItemCallRecovery(code, parser);
4382
4407
  const recoveredTokenTrees = new Set((itemRecovery?.ranges || []).map(([start]) => start - 1));
4383
4408
 
4384
4409
  const visitNode = (node) => {
@@ -5227,17 +5252,21 @@ function findCallsInCode(code, parser) {
5227
5252
  }
5228
5253
  }
5229
5254
  };
5230
- traverseTree(tree.rootNode, visitNode, { onLeave: leaveNode });
5255
+ traverseTree(tree.rootNode, skip ? node => (skip(node) ? false : visitNode(node)) : visitNode,
5256
+ { onLeave: leaveNode });
5231
5257
  if (itemRecovery) {
5232
5258
  const ranges = itemRecovery.ranges;
5233
5259
  const inside = node => ranges.some(([start, end]) => node.startIndex >= start && node.endIndex <= end);
5234
5260
  const overlaps = node => ranges.some(([start, end]) => node.startIndex < end && node.endIndex > start);
5235
5261
  traverseTree(itemRecovery.tree.rootNode, node => {
5262
+ if (skip && skip(node)) return false;
5236
5263
  if (inside(node)) return visitNode(node);
5237
5264
  return overlaps(node);
5238
5265
  }, { onLeave: node => { if (inside(node)) leaveNode(node); } });
5239
5266
  }
5240
5267
 
5268
+ // A tree handed in is an expanded file: no unexpanded item bodies.
5269
+ if (options.tree) return calls;
5241
5270
  const declaration = declarationTrees(code, parser);
5242
5271
  if (!declaration.macroItemRecovery) return calls;
5243
5272
  const functions = findFunctions(code, parser);
@@ -5974,6 +6003,8 @@ function isEntryPoint(symbol) {
5974
6003
 
5975
6004
  module.exports = {
5976
6005
  primeDeclarationTrees,
6006
+ DECLARATION_NODE_TYPES,
6007
+ rustExpandedItemRecovery,
5977
6008
  parseDeclarationsIn,
5978
6009
  rustMacroInvocationContext,
5979
6010
  findFunctions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "5.5.0",
3
+ "version": "5.5.1",
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",