ucn 5.5.0 → 5.5.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/.claude/skills/ucn/SKILL.md +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +41 -27
- package/cli/index.js +6 -1
- package/core/accessors.js +341 -54
- package/core/account.js +106 -31
- package/core/analysis.js +266 -127
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +144 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +5111 -1024
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- 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 +35 -15
- 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/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 +1 -0
- package/core/output/reporting.js +6 -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 +228 -114
- 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 +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +1202 -157
- package/core/rust-modules.js +21 -3
- 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 +157 -46
- 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 +548 -89
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/core/project.js
CHANGED
|
@@ -19,7 +19,8 @@ const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits, PA
|
|
|
19
19
|
const { validateFileIR } = require('./ir');
|
|
20
20
|
const { createFileEntryFromIR, populateFileEntryFromIR } = require('./index-ir');
|
|
21
21
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
22
|
-
const { escapeRegExp,
|
|
22
|
+
const { escapeRegExp, isCallableMember, codeUnitCompare, literalNameRegex, calleeNames } = require('./shared');
|
|
23
|
+
const { containsCall } = require('./callable-scope');
|
|
23
24
|
const stacktrace = require('./stacktrace');
|
|
24
25
|
const indexCache = require('./cache');
|
|
25
26
|
// Loaded on first use (fix #375): most commands never reach dead-code
|
|
@@ -35,6 +36,11 @@ const graphModule = require('./graph');
|
|
|
35
36
|
const graphBuildModule = require('./graph-build');
|
|
36
37
|
const reportingModule = require('./reporting');
|
|
37
38
|
|
|
39
|
+
// Modules read lazily (load cycles), each resolved once: a require() per
|
|
40
|
+
// call in hot paths was measurable (fix #436).
|
|
41
|
+
let lazyClassIdentityModuleCache = null;
|
|
42
|
+
const lazyClassIdentityModule = () => lazyClassIdentityModuleCache || (lazyClassIdentityModuleCache = require('./class-identity'));
|
|
43
|
+
|
|
38
44
|
// Lazy-initialized per-language keyword sets (populated on first isKeyword call)
|
|
39
45
|
let LANGUAGE_KEYWORDS = null;
|
|
40
46
|
|
|
@@ -42,10 +48,16 @@ let LANGUAGE_KEYWORDS = null;
|
|
|
42
48
|
// small cross-operation LRU so a sequence of agent queries does not reparse the
|
|
43
49
|
// same large headers/modules for every conservation-account classification.
|
|
44
50
|
// The source-byte budget is deliberately conservative because tree-sitter's
|
|
45
|
-
// native tree is larger than its source. Evictions explicitly release
|
|
46
|
-
// memory instead of waiting for N-API finalizers.
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
// native tree is larger than its source (~40x). Evictions explicitly release
|
|
52
|
+
// native memory instead of waiting for N-API finalizers. The bound is the
|
|
53
|
+
// source bytes (fix #436): a 128-tree cap evicted in scan order whenever one
|
|
54
|
+
// query read more files than that (clap `Command`: 164), so every later read
|
|
55
|
+
// of the same files re-parsed them, and freed trees left the heap larger.
|
|
56
|
+
const PARSED_TREE_CACHE_MAX_ENTRIES = 2048;
|
|
57
|
+
const PARSED_TREE_CACHE_MAX_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
58
|
+
// The most recent tree alone stays up to the former budget, so one large
|
|
59
|
+
// file (an amalgamation) is still read once across operations.
|
|
60
|
+
const PARSED_TREE_CACHE_MAX_SINGLE_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
49
61
|
// Automatic build parallelism (fix #375): source bytes to (re)parse before a
|
|
50
62
|
// build uses workers, and per worker. A worker's fixed cost (thread start
|
|
51
63
|
// and its own JIT warm-up of the parsers) is about the CPU of parsing 300KB
|
|
@@ -59,6 +71,32 @@ const symbolsByNameAndLine = new WeakMap();
|
|
|
59
71
|
/**
|
|
60
72
|
* ProjectIndex - Manages symbol table for a project
|
|
61
73
|
*/
|
|
74
|
+
// The callables of a file spanning each line, in symbol order, for a file
|
|
75
|
+
// whose enclosing-function lookups miss often within one operation (fix
|
|
76
|
+
// #435: a C# type query looked every record of a file up against every
|
|
77
|
+
// symbol of the file). Fewer lookups scan the symbols as before.
|
|
78
|
+
const ENCLOSING_LINE_INDEX = new WeakMap(); // per-operation line cache -> { misses, lines }
|
|
79
|
+
const ENCLOSING_INDEX_AFTER = 16;
|
|
80
|
+
function enclosingCandidates(byLine, fileEntry, lineNum) {
|
|
81
|
+
let state = ENCLOSING_LINE_INDEX.get(byLine);
|
|
82
|
+
if (!state) ENCLOSING_LINE_INDEX.set(byLine, state = { misses: 0, lines: null, symbols: null });
|
|
83
|
+
if (!state.lines || state.symbols !== fileEntry.symbols) {
|
|
84
|
+
if (++state.misses <= ENCLOSING_INDEX_AFTER) return fileEntry.symbols;
|
|
85
|
+
const lines = new Map();
|
|
86
|
+
for (const symbol of fileEntry.symbols) {
|
|
87
|
+
if (!isCallableMember(symbol) || !Number.isInteger(symbol.startLine) || !Number.isInteger(symbol.endLine)) continue;
|
|
88
|
+
for (let line = symbol.startLine; line <= symbol.endLine; line++) {
|
|
89
|
+
let list = lines.get(line);
|
|
90
|
+
if (!list) lines.set(line, list = []);
|
|
91
|
+
list.push(symbol);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
state.lines = lines;
|
|
95
|
+
state.symbols = fileEntry.symbols;
|
|
96
|
+
}
|
|
97
|
+
return state.lines.get(lineNum) || [];
|
|
98
|
+
}
|
|
99
|
+
|
|
62
100
|
class ProjectIndex {
|
|
63
101
|
/**
|
|
64
102
|
* Create a new ProjectIndex
|
|
@@ -80,6 +118,7 @@ class ProjectIndex {
|
|
|
80
118
|
this.lastBuildWorkerCount = 1;
|
|
81
119
|
this.lastBuildParallelEligible = false;
|
|
82
120
|
this.lastBuildRequestedWorkers = null;
|
|
121
|
+
this.lastBuildWorkerLimit = null;
|
|
83
122
|
// Parse workers the last build kept for Rust macro expansion (0: the
|
|
84
123
|
// expansion ran in place or had nothing to expand).
|
|
85
124
|
this.lastBuildExpansionWorkers = 0;
|
|
@@ -101,7 +140,8 @@ class ProjectIndex {
|
|
|
101
140
|
this._opCppTypeCategoryCache = null; // per-operation normalized C++ parameter categories
|
|
102
141
|
this._opDerefPairs = null; // per-operation Rust Deref identity pairs
|
|
103
142
|
this._opAliasPairs = null; // per-operation language type-alias identity pairs
|
|
104
|
-
this._parsedTreeCache = new Map(); // cross-operation LRU: filePath -> immutable tree entry
|
|
143
|
+
this._parsedTreeCache = new Map(); // cross-operation LRU: `${language}\0${filePath}` -> immutable tree entry
|
|
144
|
+
this._parsedTreeKeys = new Map(); // filePath -> its keys in _parsedTreeCache
|
|
105
145
|
this._parsedTreeCacheSourceBytes = 0;
|
|
106
146
|
// Cross-operation, content-hash-keyed usage classifications. Account
|
|
107
147
|
// queries often ask several projections for the same hot symbol; the
|
|
@@ -130,6 +170,8 @@ class ProjectIndex {
|
|
|
130
170
|
this._rustMacroState = null; // Rust macro_rules! expansion state (core/rust-macro-expansion.js)
|
|
131
171
|
this._cppScope = null; // C++ qualified-name memo (core/cpp-scope.js)
|
|
132
172
|
this._contractIndex = null; // contract-membership index memo (core/contract-membership.js)
|
|
173
|
+
this._implicitConversionOwners = null;
|
|
174
|
+
this._constructionAliases = null;
|
|
133
175
|
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js), reset with _classIdentity
|
|
134
176
|
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
135
177
|
this._rustScopeMemo = null; // fix #368: Rust glob/bound resolution memo, index-lifetime
|
|
@@ -137,6 +179,19 @@ class ProjectIndex {
|
|
|
137
179
|
this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
|
|
138
180
|
}
|
|
139
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The language a file is parsed in: the indexed language when the file is
|
|
184
|
+
* indexed (a `.h` header's grammar is decided at build time from the
|
|
185
|
+
* compilation database and the project's translation units), else the
|
|
186
|
+
* detection the build would make. A relative path is project-relative,
|
|
187
|
+
* never relative to the process directory (fix #436).
|
|
188
|
+
*/
|
|
189
|
+
languageOf(filePath) {
|
|
190
|
+
if (!filePath) return null;
|
|
191
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.join(this.root, filePath);
|
|
192
|
+
return this.files.get(absolute)?.language || detectLanguage(absolute, this.root);
|
|
193
|
+
}
|
|
194
|
+
|
|
140
195
|
/**
|
|
141
196
|
* Read file content with per-operation caching.
|
|
142
197
|
* When an operation cache is active (_opContentCache is set), reads are
|
|
@@ -248,37 +303,60 @@ class ProjectIndex {
|
|
|
248
303
|
}
|
|
249
304
|
}
|
|
250
305
|
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
entry.
|
|
306
|
+
/**
|
|
307
|
+
* Remove a file's persistent parsed trees (every grammar it was read in,
|
|
308
|
+
* or only `language`'s), deferring disposal if an operation still uses one.
|
|
309
|
+
*/
|
|
310
|
+
_evictParsedTree(filePath, language = null) {
|
|
311
|
+
const keys = this._parsedTreeKeys.get(filePath);
|
|
312
|
+
if (!keys) return;
|
|
313
|
+
for (const key of [...keys]) {
|
|
314
|
+
const entry = this._parsedTreeCache.get(key);
|
|
315
|
+
if (!entry || (language !== null && entry.language !== language)) continue;
|
|
316
|
+
this._parsedTreeCache.delete(key);
|
|
317
|
+
keys.delete(key);
|
|
318
|
+
this._parsedTreeCacheSourceBytes -= entry.sourceBytes;
|
|
319
|
+
if (this._opTreeCache?.get(filePath) === entry.tree) {
|
|
320
|
+
this._opTransientTrees?.add(entry.tree);
|
|
321
|
+
} else {
|
|
322
|
+
entry.tree?.delete?.();
|
|
323
|
+
}
|
|
261
324
|
}
|
|
325
|
+
if (keys.size === 0) this._parsedTreeKeys.delete(filePath);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The persistent tree of a file in one grammar, or undefined. */
|
|
329
|
+
_persistentParsedTree(filePath, language) {
|
|
330
|
+
return this._parsedTreeCache.get(`${language}\0${filePath}`);
|
|
262
331
|
}
|
|
263
332
|
|
|
264
333
|
/** Dispose every cross-operation tree. Used when an index is replaced from cache. */
|
|
265
334
|
_clearParsedTreeCache() {
|
|
266
|
-
for (const filePath of [...this.
|
|
335
|
+
for (const filePath of [...this._parsedTreeKeys.keys()]) {
|
|
267
336
|
this._evictParsedTree(filePath);
|
|
268
337
|
}
|
|
269
338
|
this._parsedTreeCacheSourceBytes = 0;
|
|
270
339
|
}
|
|
271
340
|
|
|
341
|
+
// Keyed by grammar and path: a file read in two grammars (a header a
|
|
342
|
+
// reader detects as C, the index as C++) keeps both trees instead of
|
|
343
|
+
// evicting one for the other on every alternating read (fix #435).
|
|
272
344
|
_cacheParsedTree(filePath, language, tree, sourceBytes) {
|
|
273
345
|
const fileHash = this.files.get(filePath)?.hash || null;
|
|
274
|
-
this._evictParsedTree(filePath);
|
|
275
|
-
|
|
276
|
-
|
|
346
|
+
this._evictParsedTree(filePath, language);
|
|
347
|
+
const key = `${language}\0${filePath}`;
|
|
348
|
+
this._parsedTreeCache.set(key, {
|
|
349
|
+
tree, language, fileHash, sourceBytes, filePath,
|
|
277
350
|
});
|
|
351
|
+
if (!this._parsedTreeKeys.has(filePath)) this._parsedTreeKeys.set(filePath, new Set());
|
|
352
|
+
this._parsedTreeKeys.get(filePath).add(key);
|
|
278
353
|
this._parsedTreeCacheSourceBytes += sourceBytes;
|
|
279
354
|
while (this._parsedTreeCache.size > PARSED_TREE_CACHE_MAX_ENTRIES ||
|
|
280
|
-
this._parsedTreeCacheSourceBytes > PARSED_TREE_CACHE_MAX_SOURCE_BYTES
|
|
281
|
-
|
|
355
|
+
(this._parsedTreeCacheSourceBytes > PARSED_TREE_CACHE_MAX_SOURCE_BYTES &&
|
|
356
|
+
!(this._parsedTreeCache.size === 1 &&
|
|
357
|
+
this._parsedTreeCacheSourceBytes <= PARSED_TREE_CACHE_MAX_SINGLE_SOURCE_BYTES))) {
|
|
358
|
+
const oldest = this._parsedTreeCache.values().next().value;
|
|
359
|
+
this._evictParsedTree(oldest.filePath, oldest.language);
|
|
282
360
|
}
|
|
283
361
|
}
|
|
284
362
|
|
|
@@ -291,29 +369,36 @@ class ProjectIndex {
|
|
|
291
369
|
* Returns null when parsing fails or no operation cache is active — callers
|
|
292
370
|
* fall back to parsing themselves.
|
|
293
371
|
*/
|
|
372
|
+
_peekParsedTree(filePath, language) {
|
|
373
|
+
const cached = this._persistentParsedTree(filePath, language);
|
|
374
|
+
return cached && cached.fileHash === (this.files.get(filePath)?.hash || null)
|
|
375
|
+
? cached.tree : null;
|
|
376
|
+
}
|
|
377
|
+
|
|
294
378
|
_getParsedTree(filePath, content, language) {
|
|
295
379
|
if (!this._opTreeCache) return null;
|
|
296
380
|
const cached = this._opTreeCache.get(filePath);
|
|
297
381
|
if (cached !== undefined) return cached;
|
|
298
382
|
|
|
299
|
-
const persistent = this.
|
|
383
|
+
const persistent = this._persistentParsedTree(filePath, language);
|
|
300
384
|
const currentHash = this.files.get(filePath)?.hash || null;
|
|
301
|
-
if (persistent && persistent.
|
|
302
|
-
persistent.fileHash === currentHash) {
|
|
385
|
+
if (persistent && persistent.fileHash === currentHash) {
|
|
303
386
|
// LRU touch.
|
|
304
|
-
|
|
305
|
-
this._parsedTreeCache.
|
|
387
|
+
const key = `${language}\0${filePath}`;
|
|
388
|
+
this._parsedTreeCache.delete(key);
|
|
389
|
+
this._parsedTreeCache.set(key, persistent);
|
|
306
390
|
this._opTreeCache.set(filePath, persistent.tree);
|
|
307
391
|
return persistent.tree;
|
|
308
392
|
}
|
|
309
|
-
if (persistent) this._evictParsedTree(filePath);
|
|
393
|
+
if (persistent) this._evictParsedTree(filePath, language);
|
|
310
394
|
|
|
311
395
|
let tree;
|
|
312
396
|
try {
|
|
313
397
|
const parser = getParser(language);
|
|
314
398
|
// A language may read files through its own view of the source
|
|
315
399
|
// (C# conditional compilation, fix #391); queries read the same.
|
|
316
|
-
const queryTree = parser &&
|
|
400
|
+
const queryTree = parser && !langTraits(language)?.plainQueryTree &&
|
|
401
|
+
getLanguageAdapter(language)?.queryTree;
|
|
317
402
|
tree = !parser ? null : queryTree
|
|
318
403
|
? queryTree(content, parser, this.files.get(filePath))
|
|
319
404
|
: safeParse(parser, content, undefined, PARSE_OPTIONS);
|
|
@@ -330,6 +415,35 @@ class ProjectIndex {
|
|
|
330
415
|
return tree;
|
|
331
416
|
}
|
|
332
417
|
|
|
418
|
+
/**
|
|
419
|
+
* The plain parse of a file whose query view is another tree (C#
|
|
420
|
+
* conditional compilation), kept in the persistent tree cache under its
|
|
421
|
+
* own grammar key (fix #436). Null outside an operation, as
|
|
422
|
+
* _getParsedTree.
|
|
423
|
+
*/
|
|
424
|
+
_plainParsedTree(filePath, content, language) {
|
|
425
|
+
if (!this._opTreeCache) return null;
|
|
426
|
+
const key = `${language}:plain`;
|
|
427
|
+
const persistent = this._persistentParsedTree(filePath, key);
|
|
428
|
+
const currentHash = this.files.get(filePath)?.hash || null;
|
|
429
|
+
if (persistent && persistent.fileHash === currentHash) {
|
|
430
|
+
const cacheKey = `${key}\0${filePath}`;
|
|
431
|
+
this._parsedTreeCache.delete(cacheKey);
|
|
432
|
+
this._parsedTreeCache.set(cacheKey, persistent);
|
|
433
|
+
return persistent.tree;
|
|
434
|
+
}
|
|
435
|
+
if (persistent) this._evictParsedTree(filePath, key);
|
|
436
|
+
let tree;
|
|
437
|
+
try {
|
|
438
|
+
const parser = getParser(language);
|
|
439
|
+
tree = parser ? safeParse(parser, content) : null;
|
|
440
|
+
} catch (e) {
|
|
441
|
+
tree = null;
|
|
442
|
+
}
|
|
443
|
+
if (tree) this._cacheParsedTree(filePath, key, tree, Buffer.byteLength(content));
|
|
444
|
+
return tree;
|
|
445
|
+
}
|
|
446
|
+
|
|
333
447
|
/**
|
|
334
448
|
* Split a file into lines once per operation. Callers must treat the
|
|
335
449
|
* returned array as read-only — it is shared across the whole operation.
|
|
@@ -357,6 +471,8 @@ class ProjectIndex {
|
|
|
357
471
|
* @param {object} [options]
|
|
358
472
|
* @param {boolean} [options.skipCallRecovery] - omit usage-only call
|
|
359
473
|
* recovery when the caller has already classified lines from the call index
|
|
474
|
+
* @param {boolean} [options.usageKindsOnly] - omit optional receiver facts
|
|
475
|
+
* when the consumer needs only occurrence positions and kinds
|
|
360
476
|
* @returns {Array|null} Array of usage objects or null if parsing failed
|
|
361
477
|
*/
|
|
362
478
|
_getCachedUsages(filePath, name, options = {}) {
|
|
@@ -365,9 +481,14 @@ class ProjectIndex {
|
|
|
365
481
|
// C/C++ can therefore skip its expensive macro replacement-list call
|
|
366
482
|
// recovery in that mode. Partition both cache layers so a partial
|
|
367
483
|
// account classification can never poison the full `usages` result.
|
|
484
|
+
// An adapter may also omit receiver facts for kind-only consumers;
|
|
485
|
+
// lexical-scope requests always retain the complete usage records.
|
|
368
486
|
const mode = [
|
|
369
|
-
options.skipCallRecovery ?
|
|
487
|
+
options.skipCallRecovery ? (langTraits(this.files.get(filePath)?.language)?.macroInvocationSyntax === 'call'
|
|
488
|
+
? 'skip-call-recovery-v2' : 'skip-call-recovery') : '',
|
|
370
489
|
options.lexicalScopes ? 'lexical-scopes' : '',
|
|
490
|
+
options.usageKindsOnly && !options.lexicalScopes &&
|
|
491
|
+
langTraits(this.files.get(filePath)?.language)?.usageKindsOnly ? 'usage-kinds-v1' : '',
|
|
371
492
|
].filter(Boolean).join('+');
|
|
372
493
|
const modeSuffix = mode ? `\0${mode}` : '';
|
|
373
494
|
const cacheKey = `${filePath}\0${name}${modeSuffix}`;
|
|
@@ -376,15 +497,10 @@ class ProjectIndex {
|
|
|
376
497
|
if (cached !== undefined) return cached;
|
|
377
498
|
}
|
|
378
499
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// Map insertion order is the LRU order.
|
|
384
|
-
this._usageResultCache.delete(persistentKey);
|
|
385
|
-
this._usageResultCache.set(persistentKey, cached);
|
|
386
|
-
if (this._opUsagesCache) this._opUsagesCache.set(cacheKey, cached.value);
|
|
387
|
-
return cached.value;
|
|
500
|
+
const cached = indexCache.getUsageResult(this, filePath, name, mode);
|
|
501
|
+
if (cached !== undefined) {
|
|
502
|
+
if (this._opUsagesCache) this._opUsagesCache.set(cacheKey, cached);
|
|
503
|
+
return cached;
|
|
388
504
|
}
|
|
389
505
|
|
|
390
506
|
// Header language is resolved during indexing from compilation
|
|
@@ -421,19 +537,7 @@ class ProjectIndex {
|
|
|
421
537
|
this._opUsagesCache.set(cacheKey, usages);
|
|
422
538
|
}
|
|
423
539
|
if (Array.isArray(usages) && this._usageResultCache) {
|
|
424
|
-
|
|
425
|
-
this._usageResultCache.set(persistentKey, { value: usages, weight });
|
|
426
|
-
this._usageResultCacheWeight += weight;
|
|
427
|
-
this.usageCacheDirty = true;
|
|
428
|
-
const maxEntries = 4096;
|
|
429
|
-
const maxWeight = 16 * 1024 * 1024;
|
|
430
|
-
while (this._usageResultCache.size > maxEntries ||
|
|
431
|
-
this._usageResultCacheWeight > maxWeight) {
|
|
432
|
-
const oldest = this._usageResultCache.entries().next().value;
|
|
433
|
-
if (!oldest) break;
|
|
434
|
-
this._usageResultCache.delete(oldest[0]);
|
|
435
|
-
this._usageResultCacheWeight -= oldest[1].weight;
|
|
436
|
-
}
|
|
540
|
+
indexCache.storeUsageResult(this, filePath, name, mode, usages);
|
|
437
541
|
}
|
|
438
542
|
return usages;
|
|
439
543
|
} catch (e) {
|
|
@@ -479,6 +583,8 @@ class ProjectIndex {
|
|
|
479
583
|
this._rustMacroState = null; // Rust macro_rules! expansion state (core/rust-macro-expansion.js)
|
|
480
584
|
this._cppScope = null; // C++ qualified-name memo (core/cpp-scope.js)
|
|
481
585
|
this._contractIndex = null; // contract-membership memo (core/contract-membership.js)
|
|
586
|
+
this._implicitConversionOwners = null;
|
|
587
|
+
this._constructionAliases = null;
|
|
482
588
|
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js)
|
|
483
589
|
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
484
590
|
this._rustScopeMemo = null; // fix #368: Rust glob/bound resolution memo, index-lifetime
|
|
@@ -618,6 +724,8 @@ class ProjectIndex {
|
|
|
618
724
|
}
|
|
619
725
|
|
|
620
726
|
// Always invalidate caches on rebuild
|
|
727
|
+
this._callNameCache = null;
|
|
728
|
+
this._callsGenerationKey = null;
|
|
621
729
|
this._completenessCache = null;
|
|
622
730
|
this._attrTypeCache = null;
|
|
623
731
|
this._computedDispatchBlindspots = null;
|
|
@@ -645,6 +753,9 @@ class ProjectIndex {
|
|
|
645
753
|
? workersSetting : (envWorkers > 0 ? envWorkers : null);
|
|
646
754
|
this.lastBuildWorkerCount = 1;
|
|
647
755
|
this.lastBuildRequestedWorkers = requestedWorkerCount;
|
|
756
|
+
// Threads later phases may start (fix #422): none when the build
|
|
757
|
+
// disabled workers, at most an explicit count, else their own choice.
|
|
758
|
+
this.lastBuildWorkerLimit = disableParallel ? 0 : requestedWorkerCount;
|
|
648
759
|
let usedParallel = false;
|
|
649
760
|
|
|
650
761
|
// C/C++ recovery performs materially heavier parser work per file
|
|
@@ -920,6 +1031,9 @@ class ProjectIndex {
|
|
|
920
1031
|
// These query caches depend on project symbols and include closure.
|
|
921
1032
|
// A changed file can alter either even before a full rebuild reaches
|
|
922
1033
|
// its graph phase, so no prior call-identity verdict may survive.
|
|
1034
|
+
this._implicitConversionOwners = null;
|
|
1035
|
+
this._constructionAliases = null;
|
|
1036
|
+
this._cppScope = null;
|
|
923
1037
|
this._cppVisibleFilesCache?.clear();
|
|
924
1038
|
this._textualIncludeClosureCache?.clear();
|
|
925
1039
|
this._translationUnitLanguageCache?.clear();
|
|
@@ -1019,6 +1133,9 @@ class ProjectIndex {
|
|
|
1019
1133
|
const existing = this.files.get(filePath);
|
|
1020
1134
|
if (!existing) return;
|
|
1021
1135
|
|
|
1136
|
+
this._implicitConversionOwners = null;
|
|
1137
|
+
this._constructionAliases = null;
|
|
1138
|
+
this._cppScope = null;
|
|
1022
1139
|
this._evictParsedTree(filePath);
|
|
1023
1140
|
|
|
1024
1141
|
for (const symbol of existing.symbols) {
|
|
@@ -1067,6 +1184,8 @@ class ProjectIndex {
|
|
|
1067
1184
|
this._rustMacroState = null;
|
|
1068
1185
|
this._cppScope = null;
|
|
1069
1186
|
this._contractIndex = null;
|
|
1187
|
+
this._implicitConversionOwners = null;
|
|
1188
|
+
this._constructionAliases = null;
|
|
1070
1189
|
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js)
|
|
1071
1190
|
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
1072
1191
|
this._rustScopeMemo = null;
|
|
@@ -1129,6 +1248,8 @@ class ProjectIndex {
|
|
|
1129
1248
|
this._rustMacroState = null;
|
|
1130
1249
|
this._cppScope = null;
|
|
1131
1250
|
this._contractIndex = null;
|
|
1251
|
+
this._implicitConversionOwners = null;
|
|
1252
|
+
this._constructionAliases = null;
|
|
1132
1253
|
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js)
|
|
1133
1254
|
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
1134
1255
|
this._rustScopeMemo = null; // fix #368: Rust glob/bound resolution memo, index-lifetime
|
|
@@ -1182,39 +1303,10 @@ class ProjectIndex {
|
|
|
1182
1303
|
*/
|
|
1183
1304
|
_addToCalleeIndex(filePath, calls) {
|
|
1184
1305
|
if (!this.calleeIndex || !calls) return;
|
|
1185
|
-
for (const
|
|
1186
|
-
|
|
1187
|
-
if (!this.calleeIndex.
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
this.calleeIndex.get(name).add(filePath);
|
|
1191
|
-
if (call.resolvedName && call.resolvedName !== name) {
|
|
1192
|
-
if (!this.calleeIndex.has(call.resolvedName)) {
|
|
1193
|
-
this.calleeIndex.set(call.resolvedName, new Set());
|
|
1194
|
-
}
|
|
1195
|
-
this.calleeIndex.get(call.resolvedName).add(filePath);
|
|
1196
|
-
}
|
|
1197
|
-
if (call.resolvedNames) {
|
|
1198
|
-
for (const rn of call.resolvedNames) {
|
|
1199
|
-
if (rn !== name) {
|
|
1200
|
-
if (!this.calleeIndex.has(rn)) {
|
|
1201
|
-
this.calleeIndex.set(rn, new Set());
|
|
1202
|
-
}
|
|
1203
|
-
this.calleeIndex.get(rn).add(filePath);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
// A compiler-recognized type qualifier is also a semantic use of
|
|
1208
|
-
// the receiver type: `JValue.Compare(...)` references both
|
|
1209
|
-
// JValue and Compare. Index the receiver so class/type queries do
|
|
1210
|
-
// not fall back to a full project scan or leave the occurrence as
|
|
1211
|
-
// an unexplained call-not-resolved line.
|
|
1212
|
-
if (call.receiverIsTypeQualified && call.receiver) {
|
|
1213
|
-
if (!this.calleeIndex.has(call.receiver)) {
|
|
1214
|
-
this.calleeIndex.set(call.receiver, new Set());
|
|
1215
|
-
}
|
|
1216
|
-
this.calleeIndex.get(call.receiver).add(filePath);
|
|
1217
|
-
}
|
|
1306
|
+
for (const name of calleeNames(calls)) {
|
|
1307
|
+
let files = this.calleeIndex.get(name);
|
|
1308
|
+
if (!files) this.calleeIndex.set(name, files = new Set());
|
|
1309
|
+
files.add(filePath);
|
|
1218
1310
|
}
|
|
1219
1311
|
}
|
|
1220
1312
|
|
|
@@ -1224,23 +1316,11 @@ class ProjectIndex {
|
|
|
1224
1316
|
*/
|
|
1225
1317
|
_removeFromCalleeIndex(filePath, calls) {
|
|
1226
1318
|
if (!this.calleeIndex || !calls) return;
|
|
1227
|
-
for (const
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (fileSet.size === 0) this.calleeIndex.delete(n);
|
|
1233
|
-
}
|
|
1234
|
-
};
|
|
1235
|
-
removeName(call.name);
|
|
1236
|
-
if (call.resolvedName && call.resolvedName !== call.name) removeName(call.resolvedName);
|
|
1237
|
-
if (call.resolvedNames) {
|
|
1238
|
-
for (const rn of call.resolvedNames) {
|
|
1239
|
-
if (rn !== call.name) removeName(rn);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
if (call.receiverIsTypeQualified && call.receiver) {
|
|
1243
|
-
removeName(call.receiver);
|
|
1319
|
+
for (const name of calleeNames(calls)) {
|
|
1320
|
+
const files = this.calleeIndex.get(name);
|
|
1321
|
+
if (files) {
|
|
1322
|
+
files.delete(filePath);
|
|
1323
|
+
if (files.size === 0) this.calleeIndex.delete(name);
|
|
1244
1324
|
}
|
|
1245
1325
|
}
|
|
1246
1326
|
}
|
|
@@ -1251,6 +1331,9 @@ class ProjectIndex {
|
|
|
1251
1331
|
* Enables O(relevant files) lookup instead of O(all files) scan.
|
|
1252
1332
|
*/
|
|
1253
1333
|
buildCalleeIndex() {
|
|
1334
|
+
const names = require('./call-name-cache');
|
|
1335
|
+
if (names.restore(this)) return;
|
|
1336
|
+
const collected = new Map();
|
|
1254
1337
|
const { getCachedCalls } = require('./callers');
|
|
1255
1338
|
const { ensureCallsCacheLoaded } = require('./cache');
|
|
1256
1339
|
const { withMacroExpansion } = require('./macro-expansion');
|
|
@@ -1263,7 +1346,9 @@ class ProjectIndex {
|
|
|
1263
1346
|
const calls = cached ? withMacroExpansion(this, filePath, cached.calls, { materialize: false })
|
|
1264
1347
|
: getCachedCalls(this, filePath);
|
|
1265
1348
|
this._addToCalleeIndex(filePath, calls);
|
|
1349
|
+
names.capture(this, collected, filePath, calls);
|
|
1266
1350
|
}
|
|
1351
|
+
names.remember(this, collected);
|
|
1267
1352
|
}
|
|
1268
1353
|
|
|
1269
1354
|
/**
|
|
@@ -1336,7 +1421,7 @@ class ProjectIndex {
|
|
|
1336
1421
|
// class. Only partial types gather bases from other files, and a
|
|
1337
1422
|
// C/C++ declaration may be a forward declaration of a class
|
|
1338
1423
|
// defined in another file.
|
|
1339
|
-
if (contextFile &&
|
|
1424
|
+
if (contextFile && lazyClassIdentityModule().fileDeclaresClass(this, className, contextFile)) return null;
|
|
1340
1425
|
|
|
1341
1426
|
// Try imported file
|
|
1342
1427
|
if (contextFile) {
|
|
@@ -2053,7 +2138,7 @@ class ProjectIndex {
|
|
|
2053
2138
|
// A member declared in another same-name type's body (a
|
|
2054
2139
|
// function-local class beside its module-level namesake, C#
|
|
2055
2140
|
// `Outcome` beside `Outcome<T>`) is not this type's (fix #389).
|
|
2056
|
-
const { ownerDefinitionOf, classKeyOf } =
|
|
2141
|
+
const { ownerDefinitionOf, classKeyOf } = lazyClassIdentityModule();
|
|
2057
2142
|
const typeKey = definition && definition.name === baseTypeName ? classKeyOf(this, definition) : null;
|
|
2058
2143
|
const ownedHere = symbol => {
|
|
2059
2144
|
if (!typeKey) return true;
|
|
@@ -2199,6 +2284,14 @@ class ProjectIndex {
|
|
|
2199
2284
|
*/
|
|
2200
2285
|
getLineContent(filePath, lineNum) {
|
|
2201
2286
|
try {
|
|
2287
|
+
// Within an operation the file's split lines are shared, so a
|
|
2288
|
+
// line is one lookup instead of a scan from the file's start
|
|
2289
|
+
// (fix #436: value bands read thousands of lines of one file).
|
|
2290
|
+
if (this._opLinesCache) {
|
|
2291
|
+
const lines = this._getFileLines(filePath);
|
|
2292
|
+
const at = lineNum >= 1 ? Math.ceil(lineNum) - 1 : 0;
|
|
2293
|
+
return at < lines.length ? lines[at] : '';
|
|
2294
|
+
}
|
|
2202
2295
|
const content = this._readFile(filePath);
|
|
2203
2296
|
// Find the Nth line without splitting the entire file
|
|
2204
2297
|
let start = 0;
|
|
@@ -2615,34 +2708,47 @@ class ProjectIndex {
|
|
|
2615
2708
|
const hint = call?.macroExpansion && call.enclosingFunction;
|
|
2616
2709
|
if (hint?.name) {
|
|
2617
2710
|
const exact = this._symbolsNamedAt(fileEntry, hint.name, hint.startLine)
|
|
2618
|
-
.find(symbol =>
|
|
2711
|
+
.find(symbol => isCallableMember(symbol) &&
|
|
2619
2712
|
symbol.startLine <= lineNum && symbol.endLine >= lineNum);
|
|
2620
2713
|
if (exact) return returnSymbol ? exact : exact.name;
|
|
2621
2714
|
}
|
|
2622
2715
|
|
|
2623
|
-
//
|
|
2624
|
-
|
|
2716
|
+
// Call positions distinguish declarations sharing a line. Queries
|
|
2717
|
+
// without a position retain the line-local cache entry.
|
|
2718
|
+
let byLine = null;
|
|
2719
|
+
const column = call?.column ?? call?.callSite?.column;
|
|
2720
|
+
const offset = call?.callStart ?? call?.callSite?.start;
|
|
2721
|
+
const scopeKey = Number.isInteger(column) || Number.isInteger(offset)
|
|
2722
|
+
? `${lineNum}:${column ?? ''}:${offset ?? ''}` : lineNum;
|
|
2625
2723
|
if (this._opEnclosingFnCache) {
|
|
2626
|
-
|
|
2724
|
+
byLine = this._opEnclosingFnCache.get(filePath);
|
|
2725
|
+
if (!byLine) this._opEnclosingFnCache.set(filePath, byLine = new Map());
|
|
2726
|
+
const cached = byLine.get(scopeKey);
|
|
2627
2727
|
if (cached !== undefined) {
|
|
2628
2728
|
return cached === null ? null : (returnSymbol ? cached : cached.name);
|
|
2629
2729
|
}
|
|
2630
2730
|
}
|
|
2631
2731
|
|
|
2632
2732
|
let best = null;
|
|
2633
|
-
|
|
2634
|
-
|
|
2733
|
+
let bestSourceScope = false;
|
|
2734
|
+
// An arrow-function class field is a function body (fix #423).
|
|
2735
|
+
for (const symbol of byLine ? enclosingCandidates(byLine, fileEntry, lineNum) : fileEntry.symbols) {
|
|
2736
|
+
if (isCallableMember(symbol) &&
|
|
2635
2737
|
symbol.startLine <= lineNum &&
|
|
2636
2738
|
symbol.endLine >= lineNum) {
|
|
2637
|
-
|
|
2739
|
+
const sourceScope = call ? containsCall(symbol, call) : null;
|
|
2740
|
+
if (sourceScope === false) continue;
|
|
2741
|
+
const size = candidate => sourceScope === true && candidate.sourceSpan
|
|
2742
|
+
? candidate.sourceSpan.end - candidate.sourceSpan.start : candidate.endLine - candidate.startLine;
|
|
2743
|
+
if (!best || sourceScope === true && !bestSourceScope ||
|
|
2744
|
+
(sourceScope === true) === bestSourceScope && size(symbol) < size(best)) {
|
|
2638
2745
|
best = symbol;
|
|
2746
|
+
bestSourceScope = sourceScope === true;
|
|
2639
2747
|
}
|
|
2640
2748
|
}
|
|
2641
2749
|
}
|
|
2642
2750
|
|
|
2643
|
-
if (
|
|
2644
|
-
this._opEnclosingFnCache.set(cacheKey, best);
|
|
2645
|
-
}
|
|
2751
|
+
if (byLine) byLine.set(scopeKey, best);
|
|
2646
2752
|
return best ? (returnSymbol ? best : best.name) : null;
|
|
2647
2753
|
}
|
|
2648
2754
|
|
|
@@ -2932,6 +3038,14 @@ class ProjectIndex {
|
|
|
2932
3038
|
/** Save index to cache file */
|
|
2933
3039
|
saveCache(cachePath) { return indexCache.saveCache(this, cachePath); }
|
|
2934
3040
|
|
|
3041
|
+
/**
|
|
3042
|
+
* Derive whole the Rust files whose macro-expansion calls were derived
|
|
3043
|
+
* in part (fix #405), so a saved cache holds them. Returns the count.
|
|
3044
|
+
*/
|
|
3045
|
+
completeMacroCallDerivations() {
|
|
3046
|
+
return this._rustMacroState ? require('./rust-macro-expansion').completeRustMacroCalls(this) : 0;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
2935
3049
|
/** Load index from cache file */
|
|
2936
3050
|
/**
|
|
2937
3051
|
* Build under the cross-process build lock (fix #354) and save the cache.
|