ucn 5.5.1 → 5.5.3
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/.claude/skills/ucn/SKILL.md +35 -9
- package/.claude/skills/ucn/references/commands.md +40 -18
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +32 -21
- package/cli/index.js +2 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/bridge.js +15 -65
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/client-receivers.js +189 -0
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +61 -16
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +2 -0
- package/core/output/reporting.js +12 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +270 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/request-helpers.js +63 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +3 -3
package/core/bridge.js
CHANGED
|
@@ -2279,53 +2279,6 @@ function extractNextjsRoutes(index) {
|
|
|
2279
2279
|
* Detect HTTP client requests across the project.
|
|
2280
2280
|
* Cached on `index._endpointsCache.clientRequests`.
|
|
2281
2281
|
*/
|
|
2282
|
-
// Python HTTP client types whose instances are receivers of request calls.
|
|
2283
|
-
// A receiver typed to one of these (with-binding, constructor assignment) or
|
|
2284
|
-
// bound to a pytest fixture that constructs one is a client by evidence, not
|
|
2285
|
-
// by name (fix #349: 145 of 625 real `tc.get("/api/...")` sites on one repo
|
|
2286
|
-
// were invisible because the receiver was not literally named `client`).
|
|
2287
|
-
const PY_CLIENT_TYPES = new Set([
|
|
2288
|
-
'TestClient', 'Client', 'AsyncClient', 'Session', 'FlaskClient',
|
|
2289
|
-
'ClientSession', 'AsyncSession', 'HTTPConnection', 'HTTPSConnection',
|
|
2290
|
-
]);
|
|
2291
|
-
const PY_CLIENT_FACTORIES = new Set(['test_client', 'Session', 'Client', 'AsyncClient']);
|
|
2292
|
-
const PY_REQUEST_METHODS = /^(get|post|put|delete|patch|options|head|request)$/;
|
|
2293
|
-
|
|
2294
|
-
function _pythonClientFixtures(index) {
|
|
2295
|
-
if (index._endpointsCache?.pyClientFixtures) return index._endpointsCache.pyClientFixtures;
|
|
2296
|
-
const fixtures = new Set();
|
|
2297
|
-
for (const [filePath, fileEntry] of index.files) {
|
|
2298
|
-
if (fileEntry.language !== 'python') continue;
|
|
2299
|
-
const fixtureDefs = (fileEntry.symbols || []).filter(s =>
|
|
2300
|
-
(s.decorators || []).some(d => /(^|\.)fixture$/.test(String(d))));
|
|
2301
|
-
if (fixtureDefs.length === 0) continue;
|
|
2302
|
-
const calls = getCachedCalls(index, filePath) || [];
|
|
2303
|
-
for (const def of fixtureDefs) {
|
|
2304
|
-
const constructsClient = calls.some(c =>
|
|
2305
|
-
c.enclosingFunction?.name === def.name &&
|
|
2306
|
-
c.enclosingFunction?.startLine === def.startLine &&
|
|
2307
|
-
(PY_CLIENT_TYPES.has(c.name) || (c.isMethod && PY_CLIENT_FACTORIES.has(c.name))));
|
|
2308
|
-
if (constructsClient) fixtures.add(def.name);
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
if (!index._endpointsCache) index._endpointsCache = {};
|
|
2312
|
-
index._endpointsCache.pyClientFixtures = fixtures;
|
|
2313
|
-
return fixtures;
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
function _pythonClientReceiver(index, fileEntry, call, fixtures) {
|
|
2317
|
-
if (!call.isMethod || !call.receiver || !PY_REQUEST_METHODS.test(call.name)) return null;
|
|
2318
|
-
if (call.receiverType && PY_CLIENT_TYPES.has(String(call.receiverType).split('.').pop())) {
|
|
2319
|
-
return 'python-client';
|
|
2320
|
-
}
|
|
2321
|
-
if (fixtures.size === 0 || !fixtures.has(call.receiver)) return null;
|
|
2322
|
-
const fn = call.enclosingFunction;
|
|
2323
|
-
if (!fn) return null;
|
|
2324
|
-
const sym = (fileEntry.symbols || []).find(s => s.name === fn.name && s.startLine === fn.startLine);
|
|
2325
|
-
const params = String(sym?.params || '').split(',').map(p => p.trim().split(/[:=]/)[0].trim());
|
|
2326
|
-
return params.includes(call.receiver) ? 'pytest-client-fixture' : null;
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
2282
|
// `fastify().route({...})` - a route declared on a freshly built server.
|
|
2330
2283
|
const JS_SERVER_FACTORY_CALL = /^(fastify|Fastify|express|Hono|Koa)$/;
|
|
2331
2284
|
|
|
@@ -2347,7 +2300,7 @@ function extractClientRequests(index) {
|
|
|
2347
2300
|
}
|
|
2348
2301
|
const requests = [];
|
|
2349
2302
|
const uncertain = [];
|
|
2350
|
-
const
|
|
2303
|
+
const clientProof = new (require('./client-receivers').ClientReceiverProof)(index, endpointsSession(index));
|
|
2351
2304
|
let helperProof = null;
|
|
2352
2305
|
|
|
2353
2306
|
for (const [filePath, fileEntry] of index.files) {
|
|
@@ -2431,7 +2384,9 @@ function extractClientRequests(index) {
|
|
|
2431
2384
|
continue;
|
|
2432
2385
|
}
|
|
2433
2386
|
if (!helperProof) helperProof = new RequestHelperProof(index, endpointsSession(index));
|
|
2434
|
-
const proof =
|
|
2387
|
+
const proof = call.isMethod
|
|
2388
|
+
? { proven: !!clientProof.classify(filePath, call), reason: 'receiver-unproven' }
|
|
2389
|
+
: helperProof.classify(filePath, call);
|
|
2435
2390
|
const verb = call.isMethod && /^(get|post|put|delete|patch|options|head)$/i.test(call.name)
|
|
2436
2391
|
? call.name.toUpperCase() : null;
|
|
2437
2392
|
const method = cfg.method || verb || 'ALL';
|
|
@@ -2462,7 +2417,7 @@ function extractClientRequests(index) {
|
|
|
2462
2417
|
// fix #383: Go request constructors read their method and URL
|
|
2463
2418
|
// from their own argument positions.
|
|
2464
2419
|
if (call.requestTarget && lang === 'go' && call.receiver && !call.isPathCall &&
|
|
2465
|
-
matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang,
|
|
2420
|
+
matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang, clientProof.classify(filePath, call))) {
|
|
2466
2421
|
const target = call.requestTarget;
|
|
2467
2422
|
if (isPathShaped(target.url)) {
|
|
2468
2423
|
// A request with a path but no scheme and host cannot be
|
|
@@ -2496,26 +2451,22 @@ function extractClientRequests(index) {
|
|
|
2496
2451
|
continue;
|
|
2497
2452
|
}
|
|
2498
2453
|
if (!call.firstStringArg) continue;
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
r = { method: call.name.toUpperCase() === 'REQUEST' ? 'ALL' : call.name.toUpperCase(),
|
|
2504
|
-
framework, methodInferred: call.name === 'request' };
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2454
|
+
const conf = CLIENT_PATTERNS[lang];
|
|
2455
|
+
const candidate = conf && (conf.bareCalls.has(call.name) ||
|
|
2456
|
+
conf.receivers.some(p => p.methodPattern.test(call.name)));
|
|
2457
|
+
const r = matchClientRequest(call, lang, candidate ? clientProof.classify(filePath, call) : null);
|
|
2507
2458
|
if (!r) {
|
|
2508
2459
|
// Visible uncertainty: request-shaped call on an unrecognized
|
|
2509
2460
|
// receiver with a path-shaped literal. Listed, never counted.
|
|
2510
|
-
const conf = CLIENT_PATTERNS[lang];
|
|
2511
2461
|
const pathShaped = call.firstStringArg.startsWith('/') || call.firstStringArg.includes('://');
|
|
2512
|
-
if (conf && call.
|
|
2462
|
+
if (conf && call.receiver && pathShaped &&
|
|
2513
2463
|
conf.receivers.some(p => p.methodPattern.test(call.name))) {
|
|
2514
2464
|
uncertain.push({
|
|
2515
2465
|
receiver: call.receiver, method: call.name, path: call.firstStringArg,
|
|
2516
2466
|
file: fileEntry.relativePath || filePath, absoluteFile: filePath, line: call.line,
|
|
2517
2467
|
callerName: call.enclosingFunction?.name || '<top-level>',
|
|
2518
|
-
reason:
|
|
2468
|
+
reason: conf.receivers.some(p => p.receiverPattern.test(call.receiver))
|
|
2469
|
+
? 'receiver-name-only' : 'receiver-unrecognized',
|
|
2519
2470
|
});
|
|
2520
2471
|
}
|
|
2521
2472
|
continue;
|
|
@@ -2640,9 +2591,9 @@ function supertestProducerOf(call, calls, producers, locals) {
|
|
|
2640
2591
|
* Match a call against client request patterns.
|
|
2641
2592
|
* Returns { method, framework, methodInferred } or null.
|
|
2642
2593
|
*/
|
|
2643
|
-
function matchClientRequest(call, lang,
|
|
2594
|
+
function matchClientRequest(call, lang, origin) {
|
|
2644
2595
|
const conf = CLIENT_PATTERNS[lang];
|
|
2645
|
-
if (!conf) return null;
|
|
2596
|
+
if (!conf || !origin) return null;
|
|
2646
2597
|
|
|
2647
2598
|
// 1) Bare-call patterns: fetch('/path') or fetch('/path', { method: 'POST' })
|
|
2648
2599
|
if (!call.isMethod && conf.bareCalls.has(call.name)) {
|
|
@@ -2662,7 +2613,6 @@ function matchClientRequest(call, lang, allCallsInFile) {
|
|
|
2662
2613
|
const isMethodLike = call.isMethod || (lang === 'go' && !!call.receiver && !call.isPathCall);
|
|
2663
2614
|
if (isMethodLike && call.receiver) {
|
|
2664
2615
|
for (const p of conf.receivers) {
|
|
2665
|
-
if (!p.receiverPattern.test(call.receiver)) continue;
|
|
2666
2616
|
if (!p.methodPattern.test(call.name)) continue;
|
|
2667
2617
|
|
|
2668
2618
|
// Determine method
|
|
@@ -2697,7 +2647,7 @@ function matchClientRequest(call, lang, allCallsInFile) {
|
|
|
2697
2647
|
} else {
|
|
2698
2648
|
method = methodName.toUpperCase();
|
|
2699
2649
|
}
|
|
2700
|
-
return { method, framework:
|
|
2650
|
+
return { method, framework: origin, methodInferred: inferred };
|
|
2701
2651
|
}
|
|
2702
2652
|
}
|
|
2703
2653
|
|
package/core/brief.js
CHANGED
|
@@ -102,7 +102,7 @@ function brief(index, name, options = {}) {
|
|
|
102
102
|
const { def } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
103
103
|
if (!def) return null;
|
|
104
104
|
|
|
105
|
-
const language =
|
|
105
|
+
const language = index.languageOf(def.file || def.relativePath);
|
|
106
106
|
const symbol = {
|
|
107
107
|
name: def.name,
|
|
108
108
|
type: def.type,
|
|
@@ -229,7 +229,10 @@ function firstSentence(text) {
|
|
|
229
229
|
function countMembers(index, def) {
|
|
230
230
|
if (!def || !def.file) return 0;
|
|
231
231
|
let count = 0;
|
|
232
|
-
|
|
232
|
+
// Build and cache loading keep each file's declaration list alongside
|
|
233
|
+
// the name index. This count only reads declarations of the pinned file.
|
|
234
|
+
const fileSymbols = index.files?.get(def.file)?.symbols;
|
|
235
|
+
for (const arr of fileSymbols ? [fileSymbols] : index.symbols.values()) {
|
|
233
236
|
for (const s of arr) {
|
|
234
237
|
if (s.file === def.file && s.className === def.name && s.isMethod) count++;
|
|
235
238
|
}
|
|
@@ -362,8 +365,9 @@ function sideEffectsFor(index, symbol) {
|
|
|
362
365
|
index._sideEffectCache.set(key, null);
|
|
363
366
|
return null;
|
|
364
367
|
}
|
|
365
|
-
const language =
|
|
366
|
-
|
|
368
|
+
const language = typeof index.languageOf === 'function'
|
|
369
|
+
? index.languageOf(filePath) : detectLanguage(filePath, index.root);
|
|
370
|
+
const fileEntry = index.files.get(filePath);
|
|
367
371
|
const fileImports = collectImportNames(fileEntry);
|
|
368
372
|
const tags = classifySideEffects(bodyText, language, fileImports);
|
|
369
373
|
index._sideEffectCache.set(key, tags);
|
package/core/build-worker.js
CHANGED
|
@@ -55,6 +55,7 @@ function externalMacrosFor(language) {
|
|
|
55
55
|
consulted: new Set(),
|
|
56
56
|
get objectBodies() { return resolve()?.objectBodies || EMPTY_MACROS; },
|
|
57
57
|
get functionMacros() { return resolve()?.functionMacros || EMPTY_MACROS; },
|
|
58
|
+
get functionHeads() { return resolve()?.functionHeads || EMPTY_MACROS; },
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
const signalArray = new Int32Array(signal);
|
|
@@ -180,7 +181,7 @@ if (control) {
|
|
|
180
181
|
Atomics.wait(controlArray, workerIndex, 0);
|
|
181
182
|
if (Atomics.load(controlArray, workerIndex) === 2) {
|
|
182
183
|
const job = receiveMessageOnPort(port)?.message;
|
|
183
|
-
if (job) require('./rust-macro-expansion').runExpansionQueue(job, port);
|
|
184
|
+
if (job) await require('./rust-macro-expansion').runExpansionQueue(job, port);
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
try { port.close(); } catch (_) { /* closed */ }
|
package/core/cache.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
classifyUnsupportedSourceFile,
|
|
15
15
|
} = require('./discovery');
|
|
16
16
|
const { codeUnitCompare } = require('./shared');
|
|
17
|
+
const { syntaxWeightKind } = require('./source-memo');
|
|
17
18
|
|
|
18
19
|
// Read UCN version for cache invalidation
|
|
19
20
|
const UCN_VERSION = require('../package.json').version;
|
|
@@ -925,9 +926,123 @@ function clearAllCaches() {
|
|
|
925
926
|
// v276 (fix #405b): a pending Rust file whose expansions the calls pass
|
|
926
927
|
// reads by groups records each group's lines, expanded length and the words
|
|
927
928
|
// its expansions output and its headers hold (`rustMacroExpansion.groups`).
|
|
928
|
-
|
|
929
|
+
// v277: refactoring usages distinguish declarator-name tokens from value
|
|
930
|
+
// references; C# local-function name tokens are definitions.
|
|
931
|
+
// v278: generic invocation usages preserve the terminal name and receiver.
|
|
932
|
+
// v279: C++ using facts retain their enclosing preprocessor branch groups.
|
|
933
|
+
// v291: Value declarations retain lexical scope and forward-declaration facts;
|
|
934
|
+
// value queries retain source-local shadow/text proofs and lexical block
|
|
935
|
+
// ranges with each parse view's error positions.
|
|
936
|
+
// v292: Python class syntax facts retain members, writes and dynamic stores.
|
|
937
|
+
// v300: Java constructor declarations, construction arguments and delegation
|
|
938
|
+
// records; qualified construction types, declaration source extents and
|
|
939
|
+
// JS/TS assigned value names.
|
|
940
|
+
// v303: Java producer chains retain argument counts for collection contracts.
|
|
941
|
+
// v304: Java receiver producer paths retain their written type and source scope.
|
|
942
|
+
// v305: Java variadic parameters retain their declared array type.
|
|
943
|
+
// v306: Overload witnesses omit absent optional object fields consistently.
|
|
944
|
+
// v307: Overload witnesses capture observed file fields separately.
|
|
945
|
+
// v308 (fix #419): Java records without a declared canonical constructor
|
|
946
|
+
// carry its parameters (`primaryParams`); record constructors delegate.
|
|
947
|
+
// v309 (fix #421): C++ out-of-class constructor mem-initializers and
|
|
948
|
+
// default initializations are call records.
|
|
949
|
+
// v310 (fix #422): Rust bare and path call records carry the binding the
|
|
950
|
+
// enclosing blocks give their name (`scopeBinding`, `receiverScopeBinding`),
|
|
951
|
+
// qualified-path receivers and `macroScopeUnknown`; block `use` imports carry
|
|
952
|
+
// their block lines and `use` items their visibility; files list their
|
|
953
|
+
// item-position macro invocations (`itemMacros`).
|
|
954
|
+
// v311 (fix #423): JS/TS module resolution follows a directory's package.json
|
|
955
|
+
// `main` and maps workspace subpath exports naming absent build output to
|
|
956
|
+
// their `src/` sources (persisted `moduleResolved` and import edges).
|
|
957
|
+
// v312 (fix #424): Python usage records mark module-path segments
|
|
958
|
+
// (`modulePath`) and the source-side name of an aliased from-import
|
|
959
|
+
// (`importSource`).
|
|
960
|
+
// v313 (fix #425): Go call records read method expressions `(*T).M` like
|
|
961
|
+
// `T.M`, mark receivers no local binds (`receiverPackageLevel`), record
|
|
962
|
+
// conversion operands, instantiated values and positional literal-element
|
|
963
|
+
// selectors as values, and type type-switch bindings per clause.
|
|
964
|
+
// v314 (fix #426): C# `out var` / implicitly typed pattern locals record no
|
|
965
|
+
// receiver type (was the written `var`).
|
|
966
|
+
// v315 (fix #427): C/C++ call records mark `field->m()` receivers
|
|
967
|
+
// (`receiverFieldArrow`), the implicit object's constness
|
|
968
|
+
// (`receiverConst`) and `(N)(x)` cast-or-call shapes
|
|
969
|
+
// (`parenthesizedCallee`); member functions record `memberConst` /
|
|
970
|
+
// `refQualifier`; a definition after a `static` declaration is `static`;
|
|
971
|
+
// out-of-class conversion operators, pointer-to-member parameters,
|
|
972
|
+
// `decltype(x)` locals and macro-wrapped C bodies are read; deduction
|
|
973
|
+
// guides are no functions.
|
|
974
|
+
// v316 (fix #428): C# call records mark implicitly typed lambda arguments
|
|
975
|
+
// (`lambda:N:i`); Java files whose only parse errors are type annotations
|
|
976
|
+
// before a variable arity `...` are read from the recovered parse (the
|
|
977
|
+
// parameter is kept); reflection sites keyed by a parameter of the enclosing
|
|
978
|
+
// callable record it (`keyParameter`).
|
|
979
|
+
// v317 (fix #430): Python instance attributes (`self.x = v`), every name of
|
|
980
|
+
// a Go multi-name field, Java enum fields, C/C++ members of unnamed nested
|
|
981
|
+
// structs and unions (`memberPaths`) and C++ variable templates are indexed;
|
|
982
|
+
// C# receivers drop the null-forgiving `!`; Java resources take their
|
|
983
|
+
// declared type; C/C++ macro bodies read `#param` as a string literal.
|
|
984
|
+
// v318 (fix #431): JS/TS properties a class's methods assign on `this` are
|
|
985
|
+
// indexed as fields (`instanceAttribute`); value-reference records read
|
|
986
|
+
// destructuring assignment targets as writes, parameter defaults as reads
|
|
987
|
+
// and Python dict keys as values.
|
|
988
|
+
// v319 (fix #433): a saved symbol keeps `file` (and a default `bindingId`)
|
|
989
|
+
// as a 0 placeholder at its built position, so a loaded symbol has the key
|
|
990
|
+
// order of a fresh build.
|
|
991
|
+
// v320 (fix #434): call records of constructions through a method's own
|
|
992
|
+
// receiver (`cls(..)`, `type(self)(..)`, `self.__class__(..)`, `new
|
|
993
|
+
// this(..)`, `new this.constructor(..)`) carry `ownClass`.
|
|
994
|
+
// v321 (fix #436): file entries carry `computedDispatchSites`, extracted
|
|
995
|
+
// from the literal parse at index time (the doctor read them by parsing every
|
|
996
|
+
// file on the first `repo` of a process).
|
|
997
|
+
// v322: C# constructions retain terminal names and arity; nested generic receivers retain their qualifier.
|
|
998
|
+
// v326: C++ files retain raw base-clause positions and their syntactic scopes.
|
|
999
|
+
// v327: Python explicit-argument super calls retain a producer, not a lexical parent receiver.
|
|
1000
|
+
// v328: ambiguous C++ direct initializers retain empty template-call records.
|
|
1001
|
+
const CACHE_FORMAT_VERSION = 330;
|
|
929
1002
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
930
1003
|
|
|
1004
|
+
function usageResultKey(index, filePath, name, mode) {
|
|
1005
|
+
const hash = index.files.get(filePath)?.hash || '';
|
|
1006
|
+
return `${filePath}\0${hash}\0${name}${mode ? `\0${mode}` : ''}`;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// A syntax family's stored value weighs what its producer declares
|
|
1010
|
+
// (core/source-memo.js SYNTAX_FAMILIES); a usage list weighs its sites.
|
|
1011
|
+
function usageResultWeight(value, mode) {
|
|
1012
|
+
const kind = syntaxWeightKind(mode);
|
|
1013
|
+
if (kind === 'rows') return 128 + value.length * 128;
|
|
1014
|
+
return kind === 'json' ? 128 + 2 * Buffer.byteLength(JSON.stringify(value)) : 64 + value.length * 40;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Read an immutable, source-local analysis result from the shared LRU. */
|
|
1018
|
+
function getUsageResult(index, filePath, name, mode = '') {
|
|
1019
|
+
const key = usageResultKey(index, filePath, name, mode);
|
|
1020
|
+
const cached = index._usageResultCache?.get(key);
|
|
1021
|
+
if (!cached) return undefined;
|
|
1022
|
+
index._usageResultCache.delete(key);
|
|
1023
|
+
index._usageResultCache.set(key, cached);
|
|
1024
|
+
return cached.value;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** Store file-local facts only; project-dependent resolutions do not belong here. */
|
|
1028
|
+
function storeUsageResult(index, filePath, name, mode, value) {
|
|
1029
|
+
if (!index._usageResultCache || !Array.isArray(value)) return;
|
|
1030
|
+
const key = usageResultKey(index, filePath, name, mode);
|
|
1031
|
+
const prior = index._usageResultCache.get(key);
|
|
1032
|
+
if (prior) index._usageResultCacheWeight -= prior.weight;
|
|
1033
|
+
const weight = usageResultWeight(value, mode);
|
|
1034
|
+
index._usageResultCache.delete(key);
|
|
1035
|
+
index._usageResultCache.set(key, { value, weight });
|
|
1036
|
+
index._usageResultCacheWeight += weight;
|
|
1037
|
+
index.usageCacheDirty = true;
|
|
1038
|
+
while (index._usageResultCache.size > 4096 || index._usageResultCacheWeight > 16 * 1024 * 1024) {
|
|
1039
|
+
const oldest = index._usageResultCache.entries().next().value;
|
|
1040
|
+
if (!oldest) break;
|
|
1041
|
+
index._usageResultCache.delete(oldest[0]);
|
|
1042
|
+
index._usageResultCacheWeight -= oldest[1].weight;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
931
1046
|
/**
|
|
932
1047
|
* Persist the small hot-name usage cache independently of index/call shards.
|
|
933
1048
|
* This keeps one-shot CLI repeats fast without rewriting the whole project
|
|
@@ -987,7 +1102,7 @@ function loadUsageCache(index, cacheFile) {
|
|
|
987
1102
|
if (!fileEntry || fileEntry.hash !== fileHash) continue;
|
|
988
1103
|
const suffix = mode ? `\0${mode}` : '';
|
|
989
1104
|
const key = `${filePath}\0${fileHash}\0${name}${suffix}`;
|
|
990
|
-
const weight =
|
|
1105
|
+
const weight = usageResultWeight(value, mode);
|
|
991
1106
|
restored.set(key, { value, weight });
|
|
992
1107
|
weightTotal += weight;
|
|
993
1108
|
while (restored.size > 4096 || weightTotal > 16 * 1024 * 1024) {
|
|
@@ -1058,14 +1173,18 @@ function saveCache(index, cachePath) {
|
|
|
1058
1173
|
const strippedSymbols = [];
|
|
1059
1174
|
for (const [name, defs] of index.symbols) {
|
|
1060
1175
|
const stripped = defs.map(s => {
|
|
1061
|
-
const { file, bindingId, ...rest } = s;
|
|
1062
1176
|
const defaultBindingId = s.relativePath && s.type && s.startLine
|
|
1063
1177
|
? `${s.relativePath}:${s.type}:${s.startLine}`
|
|
1064
1178
|
: null;
|
|
1065
|
-
|
|
1066
|
-
|
|
1179
|
+
// `file` and a default `bindingId` are rebuilt on load; their 0
|
|
1180
|
+
// placeholders keep the key order of the built symbol (fix #433).
|
|
1181
|
+
const out = {};
|
|
1182
|
+
for (const key in s) {
|
|
1183
|
+
if (key === 'file') out.file = 0;
|
|
1184
|
+
else if (key === 'bindingId') out.bindingId = s.bindingId && s.bindingId !== defaultBindingId ? s.bindingId : 0;
|
|
1185
|
+
else out[key] = s[key];
|
|
1067
1186
|
}
|
|
1068
|
-
return
|
|
1187
|
+
return out;
|
|
1069
1188
|
});
|
|
1070
1189
|
strippedSymbols.push([name, stripped]);
|
|
1071
1190
|
}
|
|
@@ -1128,6 +1247,7 @@ function saveCache(index, cachePath) {
|
|
|
1128
1247
|
// argKinds for overload discipline (stale shapes would silently
|
|
1129
1248
|
// disable arity pruning)
|
|
1130
1249
|
version: CACHE_FORMAT_VERSION,
|
|
1250
|
+
callsGeneration: crypto.randomBytes(16).toString('hex'),
|
|
1131
1251
|
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
1132
1252
|
configHash,
|
|
1133
1253
|
discoveryHash,
|
|
@@ -1190,6 +1310,7 @@ function saveCache(index, cachePath) {
|
|
|
1190
1310
|
|
|
1191
1311
|
// PERF-3: atomic write — tmp file + rename so concurrent readers/writers
|
|
1192
1312
|
// never see a torn JSON. The calls/ shard write below already does this.
|
|
1313
|
+
index._callsGenerationKey = cacheData.callsGeneration;
|
|
1193
1314
|
const tmpFile = cacheFile + '.tmp';
|
|
1194
1315
|
fs.writeFileSync(tmpFile, JSON.stringify(cacheData));
|
|
1195
1316
|
fs.renameSync(tmpFile, cacheFile);
|
|
@@ -1254,6 +1375,7 @@ function saveCache(index, cachePath) {
|
|
|
1254
1375
|
fs.rmSync(callsDir, { recursive: true, force: true });
|
|
1255
1376
|
}
|
|
1256
1377
|
fs.renameSync(callsTmpDir, callsDir);
|
|
1378
|
+
require('./call-name-cache').save(index, callsDir);
|
|
1257
1379
|
|
|
1258
1380
|
// Clean up legacy monolithic file
|
|
1259
1381
|
const legacyFile = path.join(cacheDir, 'calls-cache.json');
|
|
@@ -1339,6 +1461,8 @@ function loadCache(index, cachePath) {
|
|
|
1339
1461
|
index._rustMacroState = null;
|
|
1340
1462
|
index._cppScope = null;
|
|
1341
1463
|
index._contractIndex = null;
|
|
1464
|
+
index._implicitConversionOwners = null;
|
|
1465
|
+
index._constructionAliases = null;
|
|
1342
1466
|
index._typeDenotation = null;
|
|
1343
1467
|
index._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
1344
1468
|
|
|
@@ -1405,6 +1529,11 @@ function loadCache(index, cachePath) {
|
|
|
1405
1529
|
// extendsGraph/extendedByGraph are derived below from the rehydrated
|
|
1406
1530
|
// symbols (fix #342) — never read from the payload.
|
|
1407
1531
|
|
|
1532
|
+
index._callsGenerationKey = typeof cacheData.callsGeneration === 'string'
|
|
1533
|
+
? cacheData.callsGeneration : null;
|
|
1534
|
+
index._callNameCache = null;
|
|
1535
|
+
index._callNameCacheReadGeneration = null;
|
|
1536
|
+
|
|
1408
1537
|
// Prepare lazy calls cache loading — load manifest but defer shard parsing.
|
|
1409
1538
|
// Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
|
|
1410
1539
|
if (index.callsCache.size === 0) {
|
|
@@ -1693,6 +1822,7 @@ function _prepareCallsCache(index, cacheFile) {
|
|
|
1693
1822
|
});
|
|
1694
1823
|
}
|
|
1695
1824
|
index._callsCachePrepared = true;
|
|
1825
|
+
index._callNameCacheDir = path.join(cacheDir, 'calls');
|
|
1696
1826
|
return;
|
|
1697
1827
|
} catch (e) {
|
|
1698
1828
|
// Corrupted manifest — fall through
|
|
@@ -2114,6 +2244,7 @@ function buildWithLock(index, buildOpts = {}, options = {}) {
|
|
|
2114
2244
|
module.exports = {
|
|
2115
2245
|
buildWithLock,
|
|
2116
2246
|
saveCache, saveUsageCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
2247
|
+
getUsageResult, storeUsageResult,
|
|
2117
2248
|
ensureCallsShardForFile,
|
|
2118
2249
|
getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
|
|
2119
2250
|
getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { calleeNames, codeUnitCompare } = require('./shared');
|
|
6
|
+
const { fileMtime } = require('./source-memo');
|
|
7
|
+
const { version } = require('../package.json');
|
|
8
|
+
|
|
9
|
+
// Call-record names only. Binding, dispatch, aliases and type queries still
|
|
10
|
+
// run through their ordinary record consumers.
|
|
11
|
+
function snapshot(index, files, captured) {
|
|
12
|
+
return { files, captured, filesMap: index.files, graph: index.importGraph,
|
|
13
|
+
config: JSON.stringify(index.config), generation: index._callsGenerationKey };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function current(index, cache) {
|
|
17
|
+
return !!cache && cache.filesMap === index.files && cache.graph === index.importGraph &&
|
|
18
|
+
cache.config === JSON.stringify(index.config) && cache.files.size === index.files.size &&
|
|
19
|
+
[...cache.files].every(([file, item]) => index.files.get(file) === item.entry &&
|
|
20
|
+
item.entry.hash === item.hash);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ensure(index) {
|
|
24
|
+
if (index.isProvenanceFactIndex || !index._callsGenerationKey ||
|
|
25
|
+
index._callNameCacheReadGeneration === index._callsGenerationKey) return;
|
|
26
|
+
index._callNameCacheReadGeneration = index._callsGenerationKey;
|
|
27
|
+
if (index._callNameCacheDir) load(index, index._callNameCacheDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function capture(index, collected, file, calls) {
|
|
31
|
+
if (index.isProvenanceFactIndex) return;
|
|
32
|
+
const entry = index.files.get(file), raw = index.callsCache.get(file);
|
|
33
|
+
if (!entry?.hash || raw?.hash !== entry.hash || !Array.isArray(calls) ||
|
|
34
|
+
!Number.isFinite(raw.mtime)) return;
|
|
35
|
+
const names = calleeNames(calls);
|
|
36
|
+
if ([...names].some(name => typeof name !== 'string')) return;
|
|
37
|
+
collected.set(file, { entry, hash: entry.hash, raw, rawCalls: raw.calls, names, mtime: raw.mtime });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function remember(index, files) {
|
|
41
|
+
if (index.isProvenanceFactIndex || files.size !== index.files.size) return;
|
|
42
|
+
index._callNameCache = snapshot(index, files, true);
|
|
43
|
+
if (index._callsGenerationKey && !index.callsCacheDirty && !index.macroExpansionDirty && index._callsCacheDir) save(index, path.join(index._callsCacheDir, 'calls'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function save(index, directory) {
|
|
47
|
+
if (index.isProvenanceFactIndex) return;
|
|
48
|
+
const cache = index._callNameCache;
|
|
49
|
+
if (!cache?.captured || !index._callsGenerationKey ||
|
|
50
|
+
!current(index, cache) || !fs.existsSync(directory)) return;
|
|
51
|
+
for (const [file, item] of cache.files) {
|
|
52
|
+
if (index.files.get(file) !== item.entry ||
|
|
53
|
+
index.callsCache.get(file) !== item.raw || item.raw.calls !== item.rawCalls ||
|
|
54
|
+
item.raw.mtime !== item.mtime) return;
|
|
55
|
+
}
|
|
56
|
+
const dictionary = [...new Set([...cache.files.values()].flatMap(item =>
|
|
57
|
+
[...item.names]))].sort(codeUnitCompare);
|
|
58
|
+
const ids = new Map(dictionary.map((name, id) => [name, id]));
|
|
59
|
+
const files = [...cache.files].map(([file, item]) => [
|
|
60
|
+
path.relative(index.root, file), item.mtime,
|
|
61
|
+
[...item.names].map(name => ids.get(name)),
|
|
62
|
+
]).sort((a, b) => codeUnitCompare(a[0], b[0]));
|
|
63
|
+
const target = path.join(directory, 'names.json');
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(target + '.tmp', JSON.stringify({
|
|
66
|
+
version, format: 2, generation: index._callsGenerationKey, dictionary, files,
|
|
67
|
+
}));
|
|
68
|
+
fs.renameSync(target + '.tmp', target);
|
|
69
|
+
} catch (_) { /* A missing optimization falls back to the record shards. */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function load(index, directory) {
|
|
73
|
+
if (index.isProvenanceFactIndex) return;
|
|
74
|
+
index._callNameCache = null;
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(fs.readFileSync(path.join(directory, 'names.json'), 'utf8'));
|
|
77
|
+
if (data.version !== version || data.format !== 2 ||
|
|
78
|
+
data.generation !== index._callsGenerationKey ||
|
|
79
|
+
!Array.isArray(data.dictionary) || !Array.isArray(data.files) ||
|
|
80
|
+
data.files.length !== index.files.size ||
|
|
81
|
+
data.dictionary.some(name => typeof name !== 'string') ||
|
|
82
|
+
new Set(data.dictionary).size !== data.dictionary.length) return;
|
|
83
|
+
const files = new Map();
|
|
84
|
+
for (const row of data.files) {
|
|
85
|
+
if (!Array.isArray(row) || row.length !== 3 || typeof row[0] !== 'string' ||
|
|
86
|
+
!Number.isFinite(row[1]) || !Array.isArray(row[2])) return;
|
|
87
|
+
const file = path.resolve(index.root, row[0]), entry = index.files.get(file);
|
|
88
|
+
if (!entry || files.has(file) || row[2].some(id =>
|
|
89
|
+
!Number.isInteger(id) || id < 0 || id >= data.dictionary.length) ||
|
|
90
|
+
new Set(row[2]).size !== row[2].length) return;
|
|
91
|
+
files.set(file, {
|
|
92
|
+
entry, hash: entry.hash, mtime: row[1], names: new Set(row[2].map(id => data.dictionary[id])),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
index._callNameCache = snapshot(index, files, false);
|
|
96
|
+
} catch (_) { /* Missing or invalid names: use the record shards. */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function restore(index) {
|
|
100
|
+
if (index.isProvenanceFactIndex) return false;
|
|
101
|
+
ensure(index);
|
|
102
|
+
const cache = index._callNameCache;
|
|
103
|
+
// A fresh in-memory parse wins over persisted names, as it does over
|
|
104
|
+
// persisted call records. A rebuild discards this cache explicitly.
|
|
105
|
+
if (index.callsCache.size || !current(index, cache) ||
|
|
106
|
+
cache.generation !== index._callsGenerationKey) return false;
|
|
107
|
+
for (const [file, item] of cache.files) {
|
|
108
|
+
if (index.files.get(file) !== item.entry) return false;
|
|
109
|
+
}
|
|
110
|
+
const byName = new Map();
|
|
111
|
+
for (const [file] of index.files) {
|
|
112
|
+
for (const name of cache.files.get(file).names) {
|
|
113
|
+
let files = byName.get(name);
|
|
114
|
+
if (!files) byName.set(name, files = new Set());
|
|
115
|
+
files.add(file);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
index.calleeIndex = byName;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function absent(index, file, name) {
|
|
123
|
+
if (index.isProvenanceFactIndex || index.callsCache.has(file)) return false;
|
|
124
|
+
ensure(index);
|
|
125
|
+
const item = index._callNameCache?.files.get(file);
|
|
126
|
+
if (!item || index.files.get(file) !== item.entry || item.entry.hash !== item.hash ||
|
|
127
|
+
item.entry.rustMacroExpansion || item.names.has(name)) return false;
|
|
128
|
+
// Match the raw record cache's mtime rule. Changed source must reach
|
|
129
|
+
// its normal read/hash/reparse path even if its previous names lacked
|
|
130
|
+
// this query. No negative binding or ownership verdict is cached.
|
|
131
|
+
try { return fileMtime(index, file) === item.mtime; } catch (_) { return false; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { capture, remember, save, load, restore, absent };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Whether the written call lies inside the declaration's AST extent. */
|
|
4
|
+
function containsCall(definition, call) {
|
|
5
|
+
const span = definition?.sourceSpan;
|
|
6
|
+
if (!span || call?.macroExpansion || definition.macroExpansion || definition.generatedByMacro) return null;
|
|
7
|
+
const line = call.line, column = call.column ?? call.callSite?.column;
|
|
8
|
+
if (!Number.isInteger(line) || !Number.isInteger(span.startLine) || !Number.isInteger(span.endLine)) return null;
|
|
9
|
+
if (line < span.startLine || line > span.endLine) return false;
|
|
10
|
+
if (Number.isInteger(column)) {
|
|
11
|
+
return (line > span.startLine || column >= span.startColumn) &&
|
|
12
|
+
(line < span.endLine || column < span.endColumn);
|
|
13
|
+
}
|
|
14
|
+
const start = call.callStart ?? call.callSite?.start;
|
|
15
|
+
if (Number.isInteger(start) && Number.isInteger(span.start) && Number.isInteger(span.end)) {
|
|
16
|
+
return start >= span.start && start < span.end;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { containsCall };
|