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.
Files changed (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +41 -27
  6. package/cli/index.js +6 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +106 -31
  9. package/core/analysis.js +266 -127
  10. package/core/ast-analysis.js +12 -3
  11. package/core/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +144 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +5111 -1024
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +228 -114
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +1202 -157
  63. package/core/rust-modules.js +21 -3
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +157 -46
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +548 -89
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. 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, NON_CALLABLE_TYPES, codeUnitCompare, literalNameRegex } = require('./shared');
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 native
46
- // memory instead of waiting for N-API finalizers.
47
- const PARSED_TREE_CACHE_MAX_ENTRIES = 128;
48
- const PARSED_TREE_CACHE_MAX_SOURCE_BYTES = 32 * 1024 * 1024;
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
- /** Remove one persistent parsed tree, deferring disposal if an operation still uses it. */
252
- _evictParsedTree(filePath) {
253
- const entry = this._parsedTreeCache.get(filePath);
254
- if (!entry) return;
255
- this._parsedTreeCache.delete(filePath);
256
- this._parsedTreeCacheSourceBytes -= entry.sourceBytes;
257
- if (this._opTreeCache?.get(filePath) === entry.tree) {
258
- this._opTransientTrees?.add(entry.tree);
259
- } else {
260
- entry.tree?.delete?.();
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._parsedTreeCache.keys()]) {
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
- this._parsedTreeCache.set(filePath, {
276
- tree, language, fileHash, sourceBytes,
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
- this._evictParsedTree(this._parsedTreeCache.keys().next().value);
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._parsedTreeCache.get(filePath);
383
+ const persistent = this._persistentParsedTree(filePath, language);
300
384
  const currentHash = this.files.get(filePath)?.hash || null;
301
- if (persistent && persistent.language === language &&
302
- persistent.fileHash === currentHash) {
385
+ if (persistent && persistent.fileHash === currentHash) {
303
386
  // LRU touch.
304
- this._parsedTreeCache.delete(filePath);
305
- this._parsedTreeCache.set(filePath, persistent);
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 && getLanguageAdapter(language)?.queryTree;
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 ? 'skip-call-recovery' : '',
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 fileHash = this.files.get(filePath)?.hash || '';
380
- const persistentKey = `${filePath}\0${fileHash}\0${name}${modeSuffix}`;
381
- if (this._usageResultCache?.has(persistentKey)) {
382
- const cached = this._usageResultCache.get(persistentKey);
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
- const weight = 64 + usages.length * 40;
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 call of calls) {
1186
- const name = call.name;
1187
- if (!this.calleeIndex.has(name)) {
1188
- this.calleeIndex.set(name, new Set());
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 call of calls) {
1228
- const removeName = (n) => {
1229
- const fileSet = this.calleeIndex.get(n);
1230
- if (fileSet) {
1231
- fileSet.delete(filePath);
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 && require('./class-identity').fileDeclaresClass(this, className, contextFile)) return null;
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 } = require('./class-identity');
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 => !NON_CALLABLE_TYPES.has(symbol.type) &&
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
- // Per-operation cache: avoid rescanning symbols for same (file, line)
2624
- const cacheKey = filePath + '\0' + lineNum;
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
- const cached = this._opEnclosingFnCache.get(cacheKey);
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
- for (const symbol of fileEntry.symbols) {
2634
- if (!NON_CALLABLE_TYPES.has(symbol.type) &&
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
- if (!best || (symbol.endLine - symbol.startLine) < (best.endLine - best.startLine)) {
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 (this._opEnclosingFnCache) {
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.