ucn 5.2.1 → 5.2.2
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/cli/index.js +3 -0
- package/core/account.js +36 -8
- package/core/cache.js +146 -9
- package/core/callers.js +1183 -115
- package/core/index-ir.js +5 -3
- package/core/ir.js +56 -8
- package/core/project.js +88 -3
- package/languages/c-family.js +19 -17
- package/languages/go.js +170 -42
- package/languages/javascript.js +257 -9
- package/languages/python.js +563 -26
- package/languages/rust.js +1 -0
- package/package.json +1 -1
package/core/index-ir.js
CHANGED
|
@@ -76,9 +76,9 @@ function createFileEntryFromIR({
|
|
|
76
76
|
|
|
77
77
|
const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
78
78
|
'returnedFunctionResult', 'isFunctionVariable', 'paramTypes', 'isAsync',
|
|
79
|
-
'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
|
|
79
|
+
'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
|
|
80
80
|
'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
|
|
81
|
-
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
81
|
+
'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
82
82
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
83
83
|
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
84
84
|
'registryMember', 'registryContainer', 'namespace',
|
|
@@ -86,9 +86,11 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
|
86
86
|
'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
87
87
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
88
88
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
89
|
+
'returnedCallStart', 'returnedCallEnd',
|
|
90
|
+
'returnedReceiverPath',
|
|
89
91
|
'isSpecialization',
|
|
90
92
|
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
91
|
-
'aliasOwner', 'aliasMember', 'macroParamEffects',
|
|
93
|
+
'aliasOwner', 'aliasMember', 'callableTarget', 'macroParamEffects',
|
|
92
94
|
]);
|
|
93
95
|
|
|
94
96
|
function materializeSymbol(fileEntry, item) {
|
package/core/ir.js
CHANGED
|
@@ -11,6 +11,24 @@
|
|
|
11
11
|
const IR_SCHEMA_VERSION = 1;
|
|
12
12
|
const EVIDENCE_TIERS = Object.freeze(['confirmed', 'unverified', 'excluded']);
|
|
13
13
|
|
|
14
|
+
// Runtime receiver identity for overload-heavy JS/TS member aliases. Generic
|
|
15
|
+
// arguments may differ while the produced value still has one concrete owner
|
|
16
|
+
// (`Factory.create(): Schema<A>` / `Schema<B>`). Transparent/async wrappers
|
|
17
|
+
// are deliberately rejected here: downstream assignment flow unwraps them,
|
|
18
|
+
// so agreement on Promise/Optional alone would not prove the inner receiver.
|
|
19
|
+
function callableAliasReturnHead(returnType) {
|
|
20
|
+
if (!returnType || typeof returnType !== 'string') return null;
|
|
21
|
+
const text = returnType.trim();
|
|
22
|
+
const match = text.match(
|
|
23
|
+
/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*(?:<[\s\S]*>|\[[\s\S]*\])?$/);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
const head = match[1].split('.').pop();
|
|
26
|
+
if (['Promise', 'Awaitable', 'Optional', 'Annotated', 'Final'].includes(head)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return head;
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
15
33
|
let normalizedOwner = owner || symbol.className || null;
|
|
16
34
|
if (!normalizedOwner && symbol.receiver && family === 'callable') {
|
|
@@ -43,9 +61,9 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
43
61
|
};
|
|
44
62
|
const passthrough = [
|
|
45
63
|
'docstring', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
|
|
46
|
-
'isAsync', 'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent',
|
|
64
|
+
'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent',
|
|
47
65
|
'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
|
|
48
|
-
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
66
|
+
'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
49
67
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
50
68
|
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
51
69
|
'registryMember', 'registryContainer', 'isConstructor',
|
|
@@ -53,9 +71,11 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
53
71
|
'namespace', 'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
54
72
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
55
73
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
74
|
+
'returnedCallStart', 'returnedCallEnd',
|
|
75
|
+
'returnedReceiverPath',
|
|
56
76
|
'isSpecialization',
|
|
57
77
|
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
58
|
-
'aliasOwner', 'aliasMember', 'macroParamEffects',
|
|
78
|
+
'aliasOwner', 'aliasMember', 'callableTarget', 'macroParamEffects',
|
|
59
79
|
];
|
|
60
80
|
for (const field of passthrough) {
|
|
61
81
|
if (symbol[field] !== undefined && symbol[field] !== null) {
|
|
@@ -106,6 +126,9 @@ function createFileIR({
|
|
|
106
126
|
traitImpl: true,
|
|
107
127
|
traitName: type.traitName,
|
|
108
128
|
}),
|
|
129
|
+
...(type.generics && {
|
|
130
|
+
ownerGenerics: type.generics,
|
|
131
|
+
}),
|
|
109
132
|
};
|
|
110
133
|
append(inherited,
|
|
111
134
|
['field', 'property'].includes(inherited.memberType)
|
|
@@ -120,19 +143,44 @@ function createFileIR({
|
|
|
120
143
|
// An immutable module-scope member alias has the callable signature of
|
|
121
144
|
// the class member it captures: `const make = Widget.create`. Materialize
|
|
122
145
|
// the local value and each explicit export alias as real function symbols
|
|
123
|
-
// only when its member is static and every declared return type
|
|
124
|
-
//
|
|
125
|
-
//
|
|
146
|
+
// only when its member is static and every declared return type has the
|
|
147
|
+
// same concrete runtime head. Generic arguments may differ across legal
|
|
148
|
+
// overloads; a different head remains ambiguous. This is compiler-visible
|
|
149
|
+
// identity; mutable aliases and ambiguous overload returns stay rejected.
|
|
126
150
|
for (const alias of (parsed.callableAliases || [])) {
|
|
127
|
-
|
|
151
|
+
let sources = normalizedSymbols.filter(symbol =>
|
|
128
152
|
symbol.name === alias.member && symbol.owner === alias.owner &&
|
|
129
153
|
(symbol.params !== undefined || symbol.paramsStructured) &&
|
|
130
154
|
(symbol.modifiers?.includes('static') ||
|
|
131
155
|
String(symbol.memberType || symbol.kind).startsWith('static')) &&
|
|
132
156
|
symbol.returnType);
|
|
157
|
+
if (sources.length === 0) {
|
|
158
|
+
// Static callable forwarding (zod-measured):
|
|
159
|
+
// `static create = createSchema; const schema = Type.create`.
|
|
160
|
+
// The parser records only a direct identifier initializer. Pin it
|
|
161
|
+
// to top-level callables in this file, and require every matching
|
|
162
|
+
// field declaration to agree before borrowing its signatures.
|
|
163
|
+
const forwarded = normalizedSymbols.filter(symbol =>
|
|
164
|
+
symbol.name === alias.member && symbol.owner === alias.owner &&
|
|
165
|
+
symbol.family === 'state' && symbol.callableTarget &&
|
|
166
|
+
symbol.modifiers?.includes('static'));
|
|
167
|
+
const targets = new Set(forwarded.map(symbol => symbol.callableTarget));
|
|
168
|
+
if (forwarded.length > 0 && targets.size === 1) {
|
|
169
|
+
const [target] = targets;
|
|
170
|
+
sources = normalizedSymbols.filter(symbol =>
|
|
171
|
+
symbol.name === target && symbol.family === 'callable' &&
|
|
172
|
+
!symbol.owner && !symbol.isNested &&
|
|
173
|
+
(symbol.params !== undefined || symbol.paramsStructured) &&
|
|
174
|
+
symbol.returnType);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
133
177
|
if (sources.length === 0) continue;
|
|
134
178
|
const sourceReturns = new Set(sources.map(source => source.returnType));
|
|
135
|
-
if (sourceReturns.size !== 1)
|
|
179
|
+
if (sourceReturns.size !== 1) {
|
|
180
|
+
const heads = new Set(sources.map(source =>
|
|
181
|
+
callableAliasReturnHead(source.returnType)));
|
|
182
|
+
if (heads.has(null) || heads.size !== 1) continue;
|
|
183
|
+
}
|
|
136
184
|
const source = sources[0];
|
|
137
185
|
const exported = (parsed.exports || []).filter(item =>
|
|
138
186
|
!item.source && item.name === alias.name);
|
package/core/project.js
CHANGED
|
@@ -79,8 +79,33 @@ class ProjectIndex {
|
|
|
79
79
|
this._opLinesCache = null; // per-operation split-lines cache (Map<filePath, string[]>, bounded FIFO)
|
|
80
80
|
this._opInnerSymbolRangesCache = null; // per-operation sorted class-method ranges by file
|
|
81
81
|
this._opFlowTypeOriginCache = null; // per-operation annotation type identity results
|
|
82
|
+
this._opCppTypeCategoryCache = null; // per-operation normalized C++ parameter categories
|
|
83
|
+
this._opCppPathReceiverTypeCache = null; // per-operation C++ qualified receiver identity
|
|
84
|
+
this._opDerefPairs = null; // per-operation Rust Deref identity pairs
|
|
85
|
+
this._opAliasPairs = null; // per-operation language type-alias identity pairs
|
|
82
86
|
this._parsedTreeCache = new Map(); // cross-operation LRU: filePath -> immutable tree entry
|
|
83
87
|
this._parsedTreeCacheSourceBytes = 0;
|
|
88
|
+
// Cross-operation, content-hash-keyed usage classifications. Account
|
|
89
|
+
// queries often ask several projections for the same hot symbol; the
|
|
90
|
+
// AST answer is immutable until the file hash changes. Bounded by
|
|
91
|
+
// both entries and approximate payload size to avoid turning a warm
|
|
92
|
+
// MCP process into an unbounded repository mirror.
|
|
93
|
+
this._usageResultCache = new Map();
|
|
94
|
+
this._usageResultCacheWeight = 0;
|
|
95
|
+
this.usageCacheDirty = false;
|
|
96
|
+
// Exact text-ground sets are likewise immutable for one built index.
|
|
97
|
+
// The cache is cleared at every build and bounded in account.js.
|
|
98
|
+
this._groundSetCache = new Map();
|
|
99
|
+
this._groundSetCacheLines = 0;
|
|
100
|
+
// Bounded cross-operation memo for immutable name-level export
|
|
101
|
+
// ownership. Agent workflows ask show/impact/tests about related
|
|
102
|
+
// symbols in sequence; retaining these tri-state barrel verdicts
|
|
103
|
+
// avoids repeating the same bounded graph walks after every command.
|
|
104
|
+
this._nameBindingReachCache = new Map();
|
|
105
|
+
// Query-derived return flow depends on cross-file annotations and is
|
|
106
|
+
// deliberately never persisted. It is safe across commands only
|
|
107
|
+
// until the next build, which resets it below.
|
|
108
|
+
this._returnTypeFlowCache = new Map();
|
|
84
109
|
this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
|
|
85
110
|
}
|
|
86
111
|
|
|
@@ -120,6 +145,10 @@ class ProjectIndex {
|
|
|
120
145
|
this._opInnerSymbolRangesCache = new Map();
|
|
121
146
|
this._opFlowTypeOriginCache = new Map();
|
|
122
147
|
this._opImportReachCache = new Map();
|
|
148
|
+
this._opCppTypeCategoryCache = new Map();
|
|
149
|
+
this._opCppPathReceiverTypeCache = new Map();
|
|
150
|
+
this._opDerefPairs = undefined;
|
|
151
|
+
this._opAliasPairs = undefined;
|
|
123
152
|
this._opDepth = 0;
|
|
124
153
|
}
|
|
125
154
|
this._opDepth++;
|
|
@@ -145,6 +174,10 @@ class ProjectIndex {
|
|
|
145
174
|
this._opInnerSymbolRangesCache = null;
|
|
146
175
|
this._opFlowTypeOriginCache = null;
|
|
147
176
|
this._opImportReachCache = null;
|
|
177
|
+
this._opCppTypeCategoryCache = null;
|
|
178
|
+
this._opCppPathReceiverTypeCache = null;
|
|
179
|
+
this._opDerefPairs = null;
|
|
180
|
+
this._opAliasPairs = null;
|
|
148
181
|
// Free cached file content from callsCache entries (retained during
|
|
149
182
|
// operation for _readFile caching, not needed between operations)
|
|
150
183
|
for (const entry of this.callsCache.values()) {
|
|
@@ -255,15 +288,38 @@ class ProjectIndex {
|
|
|
255
288
|
* multiple times within one operation (e.g., about() calls both countSymbolUsages and usages).
|
|
256
289
|
* @param {string} filePath - File to scan
|
|
257
290
|
* @param {string} name - Symbol name to find
|
|
291
|
+
* @param {object} [options]
|
|
292
|
+
* @param {boolean} [options.skipCallRecovery] - omit usage-only call
|
|
293
|
+
* recovery when the caller has already classified lines from the call index
|
|
258
294
|
* @returns {Array|null} Array of usage objects or null if parsing failed
|
|
259
295
|
*/
|
|
260
|
-
_getCachedUsages(filePath, name) {
|
|
261
|
-
|
|
296
|
+
_getCachedUsages(filePath, name, options = {}) {
|
|
297
|
+
// Account construction checks the complete calls cache before it asks
|
|
298
|
+
// the language adapter to classify the remaining name occurrences.
|
|
299
|
+
// C/C++ can therefore skip its expensive macro replacement-list call
|
|
300
|
+
// recovery in that mode. Partition both cache layers so a partial
|
|
301
|
+
// account classification can never poison the full `usages` result.
|
|
302
|
+
const mode = [
|
|
303
|
+
options.skipCallRecovery ? 'skip-call-recovery' : '',
|
|
304
|
+
].filter(Boolean).join('+');
|
|
305
|
+
const modeSuffix = mode ? `\0${mode}` : '';
|
|
306
|
+
const cacheKey = `${filePath}\0${name}${modeSuffix}`;
|
|
262
307
|
if (this._opUsagesCache) {
|
|
263
308
|
const cached = this._opUsagesCache.get(cacheKey);
|
|
264
309
|
if (cached !== undefined) return cached;
|
|
265
310
|
}
|
|
266
311
|
|
|
312
|
+
const fileHash = this.files.get(filePath)?.hash || '';
|
|
313
|
+
const persistentKey = `${filePath}\0${fileHash}\0${name}${modeSuffix}`;
|
|
314
|
+
if (this._usageResultCache?.has(persistentKey)) {
|
|
315
|
+
const cached = this._usageResultCache.get(persistentKey);
|
|
316
|
+
// Map insertion order is the LRU order.
|
|
317
|
+
this._usageResultCache.delete(persistentKey);
|
|
318
|
+
this._usageResultCache.set(persistentKey, cached);
|
|
319
|
+
if (this._opUsagesCache) this._opUsagesCache.set(cacheKey, cached.value);
|
|
320
|
+
return cached.value;
|
|
321
|
+
}
|
|
322
|
+
|
|
267
323
|
// Header language is resolved during indexing from compilation
|
|
268
324
|
// databases/include context. Re-detecting `.h` here can choose C for
|
|
269
325
|
// a C++ header and silently drop member/template usages from the raw
|
|
@@ -292,10 +348,26 @@ class ProjectIndex {
|
|
|
292
348
|
!langModule.managesOwnParseTree
|
|
293
349
|
? this._getParsedTree(filePath, content, lang)
|
|
294
350
|
: null;
|
|
295
|
-
const usages = langModule.findUsagesInCode(
|
|
351
|
+
const usages = langModule.findUsagesInCode(
|
|
352
|
+
content, name, parser, tree, options);
|
|
296
353
|
if (this._opUsagesCache) {
|
|
297
354
|
this._opUsagesCache.set(cacheKey, usages);
|
|
298
355
|
}
|
|
356
|
+
if (Array.isArray(usages) && this._usageResultCache) {
|
|
357
|
+
const weight = 64 + usages.length * 40;
|
|
358
|
+
this._usageResultCache.set(persistentKey, { value: usages, weight });
|
|
359
|
+
this._usageResultCacheWeight += weight;
|
|
360
|
+
this.usageCacheDirty = true;
|
|
361
|
+
const maxEntries = 4096;
|
|
362
|
+
const maxWeight = 16 * 1024 * 1024;
|
|
363
|
+
while (this._usageResultCache.size > maxEntries ||
|
|
364
|
+
this._usageResultCacheWeight > maxWeight) {
|
|
365
|
+
const oldest = this._usageResultCache.entries().next().value;
|
|
366
|
+
if (!oldest) break;
|
|
367
|
+
this._usageResultCache.delete(oldest[0]);
|
|
368
|
+
this._usageResultCacheWeight -= oldest[1].weight;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
299
371
|
return usages;
|
|
300
372
|
} catch (e) {
|
|
301
373
|
return null;
|
|
@@ -327,6 +399,14 @@ class ProjectIndex {
|
|
|
327
399
|
const startTime = Date.now();
|
|
328
400
|
const quiet = options.quiet !== false;
|
|
329
401
|
|
|
402
|
+
// Build/discovery can add, remove, or reclassify files. Do not retain
|
|
403
|
+
// a text-universe answer across that boundary. Hash-keyed usage
|
|
404
|
+
// results remain safe and useful for unchanged files.
|
|
405
|
+
this._groundSetCache = new Map();
|
|
406
|
+
this._groundSetCacheLines = 0;
|
|
407
|
+
this._nameBindingReachCache = new Map();
|
|
408
|
+
this._returnTypeFlowCache = new Map();
|
|
409
|
+
|
|
330
410
|
// A (re)build invalidates any cache-loaded reachability set — the
|
|
331
411
|
// fingerprint guard in computeReachability is content-shaped and
|
|
332
412
|
// cannot see every rebuild (fix #249: a stale loaded set survived
|
|
@@ -2414,6 +2494,11 @@ class ProjectIndex {
|
|
|
2414
2494
|
/** Load index from cache file */
|
|
2415
2495
|
loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
|
|
2416
2496
|
|
|
2497
|
+
/** Persist the bounded, content-hash-keyed usage-query cache. */
|
|
2498
|
+
saveUsageCache(cachePath = undefined) {
|
|
2499
|
+
return indexCache.saveUsageCache(this, cachePath);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2417
2502
|
/** Return this project's default per-user cache file path. */
|
|
2418
2503
|
getCachePath() { return indexCache.getProjectCachePath(this.root); }
|
|
2419
2504
|
|
package/languages/c-family.js
CHANGED
|
@@ -2776,7 +2776,7 @@ function findImportsInCode(code, parser) {
|
|
|
2776
2776
|
} finally { /* cached with the selected tree */ }
|
|
2777
2777
|
}
|
|
2778
2778
|
|
|
2779
|
-
function findUsagesInCode(code, name, parser, existingTree) {
|
|
2779
|
+
function findUsagesInCode(code, name, parser, existingTree, options = {}) {
|
|
2780
2780
|
// Usage is the raw literal-name inventory. The literal C/C++ tree retains
|
|
2781
2781
|
// identifiers from every preprocessor branch and is sufficient for
|
|
2782
2782
|
// occurrence kind/line classification; symbol ownership still comes from
|
|
@@ -2860,22 +2860,24 @@ function findUsagesInCode(code, name, parser, existingTree) {
|
|
|
2860
2860
|
// call extractor reparses those AST-proven regions; surface the resulting
|
|
2861
2861
|
// call usages here as well so callers/callees, usages, and tests share one
|
|
2862
2862
|
// semantic fact set.
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2863
|
+
if (!options.skipCallRecovery) {
|
|
2864
|
+
const seenCalls = new Set(usages
|
|
2865
|
+
.filter(usage => usage.usageType === 'call')
|
|
2866
|
+
.map(usage => `${usage.line}:${usage.column ?? ''}`));
|
|
2867
|
+
const macroCalls = findMacroBodyCalls(tree, code, parser, name);
|
|
2868
|
+
for (const call of macroCalls) {
|
|
2869
|
+
if (call.name !== name) continue;
|
|
2870
|
+
const key = `${call.line}:${call.column ?? ''}`;
|
|
2871
|
+
if (seenCalls.has(key)) continue;
|
|
2872
|
+
seenCalls.add(key);
|
|
2873
|
+
addUsage({
|
|
2874
|
+
line: call.line,
|
|
2875
|
+
column: call.column,
|
|
2876
|
+
usageType: 'call',
|
|
2877
|
+
...(call.receiver && { receiver: call.receiver }),
|
|
2878
|
+
...(call.macroParameter && { macroParameter: true }),
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2879
2881
|
}
|
|
2880
2882
|
return usages;
|
|
2881
2883
|
}
|