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.
- package/README.md +11 -6
- package/cli/index.js +5 -0
- package/core/account.js +32 -11
- package/core/analysis.js +9 -2
- package/core/cache.js +8 -1
- package/core/callers.js +378 -77
- package/core/project.js +15 -6
- package/core/rust-macro-expansion.js +1061 -148
- package/core/rust-modules.js +20 -2
- package/core/type-denotation.js +33 -9
- package/languages/rust.js +38 -7
- package/package.json +1 -1
package/core/rust-modules.js
CHANGED
|
@@ -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(
|
|
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;
|
package/core/type-denotation.js
CHANGED
|
@@ -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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3741
|
-
|
|
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,
|
|
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.
|
|
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",
|