ucn 5.4.2 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/project.js
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const crypto = require('crypto');
|
|
12
|
+
const { UcnError } = require('./errors');
|
|
11
13
|
const {
|
|
12
14
|
expandGlob, findProjectRoot, detectProjectPattern, isTestFile,
|
|
13
|
-
parseGitignore, gitTrackedPaths, DEFAULT_IGNORES, compareNames, classifyUnsupportedSourceFile,
|
|
15
|
+
parseGitignore, gitTrackedPaths, gitListing, DEFAULT_IGNORES, compareNames, classifyUnsupportedSourceFile,
|
|
14
16
|
} = require('./discovery');
|
|
15
17
|
const { cleanHtmlScriptTags } = require('./parser');
|
|
16
18
|
const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits, PARSE_OPTIONS } = require('../languages');
|
|
@@ -20,7 +22,10 @@ const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
|
20
22
|
const { escapeRegExp, NON_CALLABLE_TYPES, codeUnitCompare, literalNameRegex } = require('./shared');
|
|
21
23
|
const stacktrace = require('./stacktrace');
|
|
22
24
|
const indexCache = require('./cache');
|
|
23
|
-
|
|
25
|
+
// Loaded on first use (fix #375): most commands never reach dead-code
|
|
26
|
+
// analysis, and loading it (with entry-point detection) was a fixed cost of
|
|
27
|
+
// every CLI command.
|
|
28
|
+
const deadcodeModule = () => require('./deadcode');
|
|
24
29
|
const verifyModule = require('./verify');
|
|
25
30
|
const callersModule = require('./callers');
|
|
26
31
|
const tracingModule = require('./tracing');
|
|
@@ -41,6 +46,15 @@ let LANGUAGE_KEYWORDS = null;
|
|
|
41
46
|
// memory instead of waiting for N-API finalizers.
|
|
42
47
|
const PARSED_TREE_CACHE_MAX_ENTRIES = 128;
|
|
43
48
|
const PARSED_TREE_CACHE_MAX_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
49
|
+
// Automatic build parallelism (fix #375): source bytes to (re)parse before a
|
|
50
|
+
// build uses workers, and per worker. A worker's fixed cost (thread start
|
|
51
|
+
// and its own JIT warm-up of the parsers) is about the CPU of parsing 300KB
|
|
52
|
+
// on a warm thread (fix #388: at 128KB per worker from 256KB, start-up cost
|
|
53
|
+
// more CPU than the parsing it moved off the main thread).
|
|
54
|
+
const PARALLEL_MIN_WORK_BYTES = 2 * 1024 * 1024;
|
|
55
|
+
const PARALLEL_BYTES_PER_WORKER = 1024 * 1024;
|
|
56
|
+
// fileEntry.symbols array -> { length, byName } (see _symbolsNamedAt).
|
|
57
|
+
const symbolsByNameAndLine = new WeakMap();
|
|
44
58
|
|
|
45
59
|
/**
|
|
46
60
|
* ProjectIndex - Manages symbol table for a project
|
|
@@ -66,9 +80,14 @@ class ProjectIndex {
|
|
|
66
80
|
this.lastBuildWorkerCount = 1;
|
|
67
81
|
this.lastBuildParallelEligible = false;
|
|
68
82
|
this.lastBuildRequestedWorkers = null;
|
|
83
|
+
// Parse workers the last build kept for Rust macro expansion (0: the
|
|
84
|
+
// expansion ran in place or had nothing to expand).
|
|
85
|
+
this.lastBuildExpansionWorkers = 0;
|
|
69
86
|
this.callsCache = new Map(); // filePath -> { mtime, hash, calls, content }
|
|
70
87
|
this.callsCacheDirty = false; // set by getCachedCalls when entries are added or mutated
|
|
71
88
|
this.computedDispatchDirty = false; // persisted project-wide AST blind-spot inventory
|
|
89
|
+
this.macroExpansionDirty = false; // persisted C/C++ macro-expansion records (core/macro-expansion.js)
|
|
90
|
+
this._macroExpansionPersisted = null;
|
|
72
91
|
this.failedFiles = new Set(); // files that failed to index (e.g. large minified bundles)
|
|
73
92
|
this.unsupportedFiles = []; // common source files skipped by the parser registry
|
|
74
93
|
this.discoveryIssues = []; // explicit reasons the index may be partial
|
|
@@ -80,7 +99,6 @@ class ProjectIndex {
|
|
|
80
99
|
this._opInnerSymbolRangesCache = null; // per-operation sorted class-method ranges by file
|
|
81
100
|
this._opFlowTypeOriginCache = null; // per-operation annotation type identity results
|
|
82
101
|
this._opCppTypeCategoryCache = null; // per-operation normalized C++ parameter categories
|
|
83
|
-
this._opCppPathReceiverTypeCache = null; // per-operation C++ qualified receiver identity
|
|
84
102
|
this._opDerefPairs = null; // per-operation Rust Deref identity pairs
|
|
85
103
|
this._opAliasPairs = null; // per-operation language type-alias identity pairs
|
|
86
104
|
this._parsedTreeCache = new Map(); // cross-operation LRU: filePath -> immutable tree entry
|
|
@@ -106,6 +124,16 @@ class ProjectIndex {
|
|
|
106
124
|
// deliberately never persisted. It is safe across commands only
|
|
107
125
|
// until the next build, which resets it below.
|
|
108
126
|
this._returnTypeFlowCache = new Map();
|
|
127
|
+
this._classIdentity = null; // class-definition identity memo, reset at every build/load
|
|
128
|
+
this._macroExpansion = null; // C/C++ macro-expansion memo (core/macro-expansion.js), reset with _classIdentity
|
|
129
|
+
this._macroSpellable = null; // fix #385: paste-spellability memo over the same symbols
|
|
130
|
+
this._rustMacroState = null; // Rust macro_rules! expansion state (core/rust-macro-expansion.js)
|
|
131
|
+
this._cppScope = null; // C++ qualified-name memo (core/cpp-scope.js)
|
|
132
|
+
this._contractIndex = null; // contract-membership index memo (core/contract-membership.js)
|
|
133
|
+
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js), reset with _classIdentity
|
|
134
|
+
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
135
|
+
this._rustScopeMemo = null; // fix #368: Rust glob/bound resolution memo, index-lifetime
|
|
136
|
+
require('./imports').resetRustResolveMemo(); // fix #372: the tree may have changed
|
|
109
137
|
this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
|
|
110
138
|
}
|
|
111
139
|
|
|
@@ -146,10 +174,18 @@ class ProjectIndex {
|
|
|
146
174
|
this._opFlowTypeOriginCache = new Map();
|
|
147
175
|
this._opImportReachCache = new Map();
|
|
148
176
|
this._opCppTypeCategoryCache = new Map();
|
|
149
|
-
|
|
177
|
+
// Literal-name ground sets (name -> groundSet): a rename plan
|
|
178
|
+
// sweeps every slot member of one name (fix #376).
|
|
179
|
+
this._opGroundSetCache = new Map();
|
|
150
180
|
this._opDerefPairs = undefined;
|
|
151
181
|
this._opAliasPairs = undefined;
|
|
152
182
|
this._opFindCallersCaches = null; // fix #340: findCallers per-file derivations
|
|
183
|
+
// Index-derived lookups that are pure functions of the symbol
|
|
184
|
+
// table and graphs, memoized for one operation (fix #382):
|
|
185
|
+
// ancestor closures, class-file resolution, same-name definitions
|
|
186
|
+
// grouped by owner. An exact HOT ranking asks them once per
|
|
187
|
+
// candidate for every same-name definition.
|
|
188
|
+
this._opMemos = new Map();
|
|
153
189
|
this._opDepth = 0;
|
|
154
190
|
}
|
|
155
191
|
this._opDepth++;
|
|
@@ -176,10 +212,11 @@ class ProjectIndex {
|
|
|
176
212
|
this._opFlowTypeOriginCache = null;
|
|
177
213
|
this._opImportReachCache = null;
|
|
178
214
|
this._opCppTypeCategoryCache = null;
|
|
179
|
-
this.
|
|
215
|
+
this._opGroundSetCache = null;
|
|
180
216
|
this._opDerefPairs = null;
|
|
181
217
|
this._opAliasPairs = null;
|
|
182
218
|
this._opFindCallersCaches = null;
|
|
219
|
+
this._opMemos = null;
|
|
183
220
|
// Free cached file content from callsCache entries (retained during
|
|
184
221
|
// operation for _readFile caching, not needed between operations)
|
|
185
222
|
for (const entry of this.callsCache.values()) {
|
|
@@ -189,6 +226,28 @@ class ProjectIndex {
|
|
|
189
226
|
}
|
|
190
227
|
}
|
|
191
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Run `fn` in an operation of its own, with fresh per-operation caches,
|
|
231
|
+
* and give the enclosing operation (if any) its caches back afterwards
|
|
232
|
+
* (fix #394). For work that queries the index under a temporary view of
|
|
233
|
+
* a definition: no memo of the enclosing operation sees the view.
|
|
234
|
+
*/
|
|
235
|
+
_isolatedOperation(fn) {
|
|
236
|
+
const keys = Object.keys(this).filter(key => key.startsWith('_op') &&
|
|
237
|
+
!['_opMemo'].includes(key) && typeof this[key] !== 'function');
|
|
238
|
+
const saved = new Map(keys.map(key => [key, this[key]]));
|
|
239
|
+
for (const key of keys) this[key] = key === '_opDepth' ? 0 : null;
|
|
240
|
+
this._opDerefPairs = undefined;
|
|
241
|
+
this._opAliasPairs = undefined;
|
|
242
|
+
this._beginOp();
|
|
243
|
+
try {
|
|
244
|
+
return fn();
|
|
245
|
+
} finally {
|
|
246
|
+
this._endOp();
|
|
247
|
+
for (const [key, value] of saved) this[key] = value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
192
251
|
/** Remove one persistent parsed tree, deferring disposal if an operation still uses it. */
|
|
193
252
|
_evictParsedTree(filePath) {
|
|
194
253
|
const entry = this._parsedTreeCache.get(filePath);
|
|
@@ -252,7 +311,12 @@ class ProjectIndex {
|
|
|
252
311
|
let tree;
|
|
253
312
|
try {
|
|
254
313
|
const parser = getParser(language);
|
|
255
|
-
|
|
314
|
+
// A language may read files through its own view of the source
|
|
315
|
+
// (C# conditional compilation, fix #391); queries read the same.
|
|
316
|
+
const queryTree = parser && getLanguageAdapter(language)?.queryTree;
|
|
317
|
+
tree = !parser ? null : queryTree
|
|
318
|
+
? queryTree(content, parser, this.files.get(filePath))
|
|
319
|
+
: safeParse(parser, content, undefined, PARSE_OPTIONS);
|
|
256
320
|
} catch (e) {
|
|
257
321
|
tree = null;
|
|
258
322
|
}
|
|
@@ -303,6 +367,7 @@ class ProjectIndex {
|
|
|
303
367
|
// account classification can never poison the full `usages` result.
|
|
304
368
|
const mode = [
|
|
305
369
|
options.skipCallRecovery ? 'skip-call-recovery' : '',
|
|
370
|
+
options.lexicalScopes ? 'lexical-scopes' : '',
|
|
306
371
|
].filter(Boolean).join('+');
|
|
307
372
|
const modeSuffix = mode ? `\0${mode}` : '';
|
|
308
373
|
const cacheKey = `${filePath}\0${name}${modeSuffix}`;
|
|
@@ -408,6 +473,18 @@ class ProjectIndex {
|
|
|
408
473
|
this._groundSetCacheLines = 0;
|
|
409
474
|
this._nameBindingReachCache = new Map();
|
|
410
475
|
this._returnTypeFlowCache = new Map();
|
|
476
|
+
this._classIdentity = null; // class-definition identity memo (core/class-identity.js)
|
|
477
|
+
this._macroExpansion = null; // macro-expansion memo (core/macro-expansion.js)
|
|
478
|
+
this._macroSpellable = null; // fix #385: paste-spellability memo over the same symbols
|
|
479
|
+
this._rustMacroState = null; // Rust macro_rules! expansion state (core/rust-macro-expansion.js)
|
|
480
|
+
this._cppScope = null; // C++ qualified-name memo (core/cpp-scope.js)
|
|
481
|
+
this._contractIndex = null; // contract-membership memo (core/contract-membership.js)
|
|
482
|
+
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js)
|
|
483
|
+
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
484
|
+
this._rustScopeMemo = null; // fix #368: Rust glob/bound resolution memo, index-lifetime
|
|
485
|
+
// Rust module resolutions and the directory listings behind them
|
|
486
|
+
// describe the tree this build reads (fix #375).
|
|
487
|
+
require('./imports').resetRustResolveMemo();
|
|
411
488
|
|
|
412
489
|
// A (re)build invalidates any cache-loaded reachability set — the
|
|
413
490
|
// fingerprint guard in computeReachability is content-shaped and
|
|
@@ -421,6 +498,7 @@ class ProjectIndex {
|
|
|
421
498
|
|
|
422
499
|
// Accept pre-expanded file array (glob mode) or a pattern string
|
|
423
500
|
let files;
|
|
501
|
+
let discoveredCargoManifests = null;
|
|
424
502
|
const implicitProjectDiscovery = !Array.isArray(pattern) && !pattern;
|
|
425
503
|
this.unsupportedFiles = [];
|
|
426
504
|
this.discoveryIssues = [];
|
|
@@ -456,9 +534,20 @@ class ProjectIndex {
|
|
|
456
534
|
};
|
|
457
535
|
|
|
458
536
|
// Merge .gitignore and .ucn.json exclude into file discovery
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
globOpts.
|
|
537
|
+
const listing = gitListing(this.root);
|
|
538
|
+
const gitignoreRules = parseGitignore(this.root, listing);
|
|
539
|
+
globOpts.gitignoreRules = gitignoreRules;
|
|
540
|
+
globOpts.trackedPaths = gitTrackedPaths(this.root, listing);
|
|
541
|
+
this._discoveryHash = indexCache.discoveryFingerprint(gitignoreRules, listing);
|
|
542
|
+
// Git could not list the tracked files (fix #382): .gitignore
|
|
543
|
+
// rules then apply to every path, so a tracked file matching a
|
|
544
|
+
// rule is not indexed. Disclosed, never a silent file-set change.
|
|
545
|
+
if (listing?.failure) {
|
|
546
|
+
recordDiscoveryIssue({
|
|
547
|
+
path: this.root, kind: 'directory', reason: 'git-listing-failed',
|
|
548
|
+
detail: `${listing.failure.detail}; tracked files that match .gitignore rules could not be identified and are not indexed`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
462
551
|
const configExclude = Array.isArray(this.config.exclude)
|
|
463
552
|
? this.config.exclude
|
|
464
553
|
: (this.config.exclude ? [String(this.config.exclude)] : []);
|
|
@@ -469,7 +558,11 @@ class ProjectIndex {
|
|
|
469
558
|
globOpts.onDiscoveryIssue = recordDiscoveryIssue;
|
|
470
559
|
|
|
471
560
|
if (implicitProjectDiscovery) {
|
|
561
|
+
discoveredCargoManifests = [];
|
|
472
562
|
globOpts.onSkippedFile = (filePath) => {
|
|
563
|
+
if (path.basename(filePath) === 'Cargo.toml') {
|
|
564
|
+
discoveredCargoManifests.push(path.relative(this.root, filePath));
|
|
565
|
+
}
|
|
473
566
|
const kind = classifyUnsupportedSourceFile(filePath);
|
|
474
567
|
if (!kind) return;
|
|
475
568
|
this.unsupportedFiles.push({
|
|
@@ -528,7 +621,11 @@ class ProjectIndex {
|
|
|
528
621
|
this._completenessCache = null;
|
|
529
622
|
this._attrTypeCache = null;
|
|
530
623
|
this._computedDispatchBlindspots = null;
|
|
624
|
+
this._reflectionPatternIndex = null;
|
|
531
625
|
this._cppVisibleFilesCache?.clear();
|
|
626
|
+
this._textualIncludeClosureCache?.clear();
|
|
627
|
+
this._translationUnitLanguageCache?.clear();
|
|
628
|
+
if (this._compiledTranslationUnits !== undefined) this._compiledTranslationUnits = undefined;
|
|
532
629
|
this._cppTargetVisibilityCache?.clear();
|
|
533
630
|
this._cppMacroParamOutcomesCache?.clear();
|
|
534
631
|
// Endpoints cache (server routes / client requests / bridges) becomes
|
|
@@ -555,31 +652,79 @@ class ProjectIndex {
|
|
|
555
652
|
// native projects therefore benefit from workers well before the
|
|
556
653
|
// generic 150-file crossover; measured release repos cross over at
|
|
557
654
|
// roughly 25 C-family files.
|
|
558
|
-
|
|
655
|
+
let cFamilyFileCount = 0;
|
|
656
|
+
let rustFileCount = 0;
|
|
657
|
+
for (const filePath of files) {
|
|
559
658
|
const language = detectLanguage(filePath, this.root);
|
|
560
|
-
|
|
561
|
-
|
|
659
|
+
if (language === 'c' || language === 'cpp') cFamilyFileCount++;
|
|
660
|
+
else if (language === 'rust') rustFileCount++;
|
|
661
|
+
}
|
|
662
|
+
// The project's C/C++ macro definitions, so each recovery sees the
|
|
663
|
+
// decoration, statement and attribute macros other headers define
|
|
664
|
+
// (fix #396). A sequential build reads them before its first file;
|
|
665
|
+
// parse workers receive them while they parse and wait only when a
|
|
666
|
+
// recovery consults them.
|
|
667
|
+
this._macroDictionary = undefined;
|
|
668
|
+
const readMacroDictionary = () => {
|
|
669
|
+
if (this._macroDictionary === undefined && cFamilyFileCount > 0) {
|
|
670
|
+
this._macroDictionary = require('./external-macros').projectMacroDictionary(this, files);
|
|
671
|
+
}
|
|
672
|
+
return this._macroDictionary;
|
|
673
|
+
};
|
|
562
674
|
// An explicit worker count is a request for a reproducible execution
|
|
563
675
|
// shape (not merely an upper bound). This is useful to users tuning a
|
|
564
676
|
// constrained host and lets the release gate compare like with like.
|
|
677
|
+
// Without an explicit count, the worker count follows the parse work:
|
|
678
|
+
// the source bytes of the files this build must (re)parse (fix #375).
|
|
679
|
+
// A file count alone kept 100-file Rust projects (ripgrep: 1.8MB,
|
|
680
|
+
// ~1.8s of parsing) on one thread while a 151-file project of stubs
|
|
681
|
+
// paid for two workers, and an incremental rebuild of one changed
|
|
682
|
+
// file in a large project spawned the full pool.
|
|
683
|
+
const workSizes = new Map();
|
|
684
|
+
let workBytes = 0;
|
|
685
|
+
let workFiles = 0;
|
|
686
|
+
if (!disableParallel && !explicitWorkerCount && cFamilyFileCount < 25 && files.length >= 2) {
|
|
687
|
+
for (const filePath of files) {
|
|
688
|
+
let stat;
|
|
689
|
+
try { stat = fs.statSync(filePath); } catch (_) { continue; }
|
|
690
|
+
workSizes.set(filePath, stat.size);
|
|
691
|
+
const existing = this.files.get(filePath);
|
|
692
|
+
if (existing && existing.mtime === stat.mtimeMs && existing.size === stat.size) continue;
|
|
693
|
+
workBytes += stat.size;
|
|
694
|
+
workFiles++;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const availableCpus = typeof os.availableParallelism === 'function'
|
|
698
|
+
? os.availableParallelism() : os.cpus().length;
|
|
699
|
+
const byteWorkers = Math.min(workFiles >> 1, Math.max(availableCpus - 1, 1),
|
|
700
|
+
Math.ceil(workBytes / PARALLEL_BYTES_PER_WORKER));
|
|
565
701
|
const parallelWorthwhile = explicitWorkerCount
|
|
566
702
|
? files.length >= 2
|
|
567
|
-
:
|
|
703
|
+
: cFamilyFileCount >= 25 || (workBytes >= PARALLEL_MIN_WORK_BYTES && byteWorkers >= 2);
|
|
568
704
|
this.lastBuildParallelEligible = !disableParallel && parallelWorthwhile;
|
|
569
705
|
if (!disableParallel && parallelWorthwhile) {
|
|
570
706
|
try {
|
|
571
707
|
const { parallelBuild } = require('./parallel-build');
|
|
708
|
+
const autoCFamily = cFamilyFileCount >= 25 && !explicitWorkerCount;
|
|
572
709
|
const result = parallelBuild(this, files, {
|
|
573
|
-
|
|
710
|
+
macroDictionary: cFamilyFileCount > 0
|
|
711
|
+
? () => require('./external-macros').serializeDictionary(readMacroDictionary()) : null,
|
|
712
|
+
workerCount: workersSetting > 0 ? workersSetting
|
|
713
|
+
: (envWorkers > 0 ? envWorkers : (autoCFamily ? undefined : byteWorkers)),
|
|
574
714
|
// After the recovery scorer stopped repeatedly walking
|
|
575
715
|
// every candidate tree, real-repo sweeps put the native
|
|
576
716
|
// throughput knee at 5-6 workers. Default to the lower
|
|
577
717
|
// knee (five) for RSS headroom; explicit user settings
|
|
578
718
|
// retain the ordinary eight-worker safety cap.
|
|
579
|
-
maxWorkers:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
719
|
+
maxWorkers: autoCFamily ? 5 : 8,
|
|
720
|
+
minFilesPerWorker: autoCFamily ? 10 : 1,
|
|
721
|
+
sizes: workSizes,
|
|
722
|
+
// Rust macro expansion runs in parse workers kept for it
|
|
723
|
+
// (fix #388; fix #375 started fresh threads, whose parser
|
|
724
|
+
// code ran cold again).
|
|
725
|
+
retainWorkers: rustFileCount > 0
|
|
726
|
+
? require('./rust-macro-expansion').EXPANSION_WORKERS : 0,
|
|
727
|
+
onParsed: rustFileCount > 0 ? (pool) => { this._buildPool = pool || null; } : null,
|
|
583
728
|
quiet,
|
|
584
729
|
});
|
|
585
730
|
if (result !== false) {
|
|
@@ -595,30 +740,88 @@ class ProjectIndex {
|
|
|
595
740
|
}
|
|
596
741
|
}
|
|
597
742
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
743
|
+
// Parse workers kept for Rust macro expansion (fix #388); ended once
|
|
744
|
+
// the expansion no longer needs them.
|
|
745
|
+
const buildPool = this._buildPool || null;
|
|
746
|
+
this._buildPool = null;
|
|
747
|
+
try {
|
|
748
|
+
if (!usedParallel) {
|
|
749
|
+
readMacroDictionary();
|
|
750
|
+
for (const file of files) {
|
|
751
|
+
try {
|
|
752
|
+
if (this.indexFile(file)) changed++;
|
|
753
|
+
indexed++;
|
|
754
|
+
this.failedFiles.delete(file); // Succeeded now, remove from failed
|
|
755
|
+
} catch (e) {
|
|
756
|
+
this.failedFiles.add(file); // Track files that fail to index
|
|
757
|
+
if (!quiet) {
|
|
758
|
+
console.error(` Warning: Could not index ${file}: ${e.message}`);
|
|
759
|
+
}
|
|
608
760
|
}
|
|
609
761
|
}
|
|
610
762
|
}
|
|
611
|
-
}
|
|
612
763
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
764
|
+
// Project macro_rules! invocations (fix #374): what they generate
|
|
765
|
+
// (functions, impls, calls) becomes ordinary facts of the invoking
|
|
766
|
+
// file before any derived index is built. Persisted per file and
|
|
767
|
+
// revalidated against the file, the macro definitions it used and
|
|
768
|
+
// its module ancestry; a stale expanded file is re-parsed first.
|
|
769
|
+
const rustMacroExpansion = require('./rust-macro-expansion');
|
|
770
|
+
const pendingMacroFiles = rustMacroExpansion.planRustMacroExpansion(this, {
|
|
771
|
+
reindex: file => { if (this.indexFile(file, { force: true })) changed++; },
|
|
772
|
+
});
|
|
617
773
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
this.
|
|
774
|
+
// Canonical order BEFORE derived indexes, so graphs / dir index /
|
|
775
|
+
// callee index inherit it. This is what makes incremental rebuilds
|
|
776
|
+
// byte-equivalent to fresh builds (see _canonicalizeOrder).
|
|
777
|
+
this._canonicalizeOrder();
|
|
778
|
+
|
|
779
|
+
// Cargo manifests (fix #372): the workspace crate registry is scanned
|
|
780
|
+
// here, once, and persisted; a query never walks the tree for it. An
|
|
781
|
+
// incremental rebuild rescans only when a manifest changed, vanished
|
|
782
|
+
// or appeared, and then the import graph is rebuilt even when no
|
|
783
|
+
// source file changed (import resolution reads the manifests).
|
|
784
|
+
const manifestsChanged = this._refreshCargoManifests(options.forceRebuild, discoveredCargoManifests);
|
|
785
|
+
|
|
786
|
+
// Macro expansion workers run while the import graph is built (fix
|
|
787
|
+
// #375); their records are applied after it.
|
|
788
|
+
const macroOptions = { workers: options.workers, pool: buildPool };
|
|
789
|
+
const macroRun = rustMacroExpansion.startRustMacroExpansion(this, pendingMacroFiles, macroOptions);
|
|
790
|
+
this.lastBuildExpansionWorkers = macroRun?.results?.workerCount || 0;
|
|
791
|
+
if (!macroRun?.results?.usesPool) buildPool?.dispose();
|
|
792
|
+
try {
|
|
793
|
+
// Skip graph rebuild when incremental rebuild found no changes
|
|
794
|
+
if (changed > 0 || deletedInRebuild > 0 || manifestsChanged || !options.forceRebuild) {
|
|
795
|
+
this.buildImportGraph();
|
|
796
|
+
const touched = rustMacroExpansion.applyRustMacroExpansion(this, pendingMacroFiles, macroOptions, macroRun);
|
|
797
|
+
if (touched) this._canonicalizeTouched(touched);
|
|
798
|
+
// Project macro definitions a C/C++ recovery used are
|
|
799
|
+
// checked against the file's include closure, and files
|
|
800
|
+
// whose recorded definitions changed are read again
|
|
801
|
+
// (fix #396). Their #include lines do not change, so the
|
|
802
|
+
// import graph stands.
|
|
803
|
+
if (this._macroDictionary !== undefined) {
|
|
804
|
+
const dictionary = this._macroDictionary;
|
|
805
|
+
const recovered = require('./external-macros').verifyExternalMacros(this, dictionary, {
|
|
806
|
+
reindex: (file, externalMacros) => {
|
|
807
|
+
if (this.indexFile(file, { force: true, externalMacros })) changed++;
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
if (recovered) this._canonicalizeTouched(recovered);
|
|
811
|
+
}
|
|
812
|
+
this.buildInheritanceGraph();
|
|
813
|
+
} else {
|
|
814
|
+
const touched = rustMacroExpansion.applyRustMacroExpansion(this, pendingMacroFiles, macroOptions, macroRun);
|
|
815
|
+
if (touched) {
|
|
816
|
+
this._canonicalizeTouched(touched);
|
|
817
|
+
this.buildInheritanceGraph();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} finally {
|
|
821
|
+
macroRun?.results?.dispose();
|
|
822
|
+
}
|
|
823
|
+
} finally {
|
|
824
|
+
buildPool?.dispose();
|
|
622
825
|
}
|
|
623
826
|
|
|
624
827
|
// Build directory→files index for O(1) same-package lookups
|
|
@@ -641,6 +844,38 @@ class ProjectIndex {
|
|
|
641
844
|
}
|
|
642
845
|
}
|
|
643
846
|
|
|
847
|
+
/**
|
|
848
|
+
* Keep `this.cargoManifests` (persisted Cargo manifest snapshot) current.
|
|
849
|
+
* Rust projects only. Returns true when the snapshot changed on an
|
|
850
|
+
* incremental rebuild.
|
|
851
|
+
* @param {boolean} incremental
|
|
852
|
+
* @param {string[]|null} discovered - Cargo.toml paths discovery saw
|
|
853
|
+
*/
|
|
854
|
+
_refreshCargoManifests(incremental, discovered) {
|
|
855
|
+
const {
|
|
856
|
+
snapshotWorkspaceManifests, workspaceManifestsCurrent, resetCargoCaches,
|
|
857
|
+
manifestSetFingerprint,
|
|
858
|
+
} = require('./imports');
|
|
859
|
+
let hasRust = false;
|
|
860
|
+
for (const entry of this.files.values()) {
|
|
861
|
+
if (entry.language === 'rust') { hasRust = true; break; }
|
|
862
|
+
}
|
|
863
|
+
const previous = this.cargoManifests || null;
|
|
864
|
+
if (!hasRust) {
|
|
865
|
+
this.cargoManifests = null;
|
|
866
|
+
this.cargoManifestsSeen = null;
|
|
867
|
+
if (previous) resetCargoCaches(this.root);
|
|
868
|
+
return incremental && !!previous;
|
|
869
|
+
}
|
|
870
|
+
if (incremental && workspaceManifestsCurrent(this.root, previous, discovered,
|
|
871
|
+
this.cargoManifestsSeen)) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
this.cargoManifests = snapshotWorkspaceManifests(this.root);
|
|
875
|
+
this.cargoManifestsSeen = discovered ? manifestSetFingerprint(discovered) : null;
|
|
876
|
+
return incremental && JSON.stringify(previous) !== JSON.stringify(this.cargoManifests);
|
|
877
|
+
}
|
|
878
|
+
|
|
644
879
|
/**
|
|
645
880
|
* Build a minimal index for a single file (no glob, no cache, no import graph).
|
|
646
881
|
* Used by CLI file mode to route through execute().
|
|
@@ -648,21 +883,27 @@ class ProjectIndex {
|
|
|
648
883
|
buildSingleFile(filePath) {
|
|
649
884
|
const absPath = path.resolve(filePath);
|
|
650
885
|
if (!fs.existsSync(absPath)) {
|
|
651
|
-
throw new
|
|
886
|
+
throw new UcnError(`File not found: ${filePath}`);
|
|
652
887
|
}
|
|
653
888
|
this.indexFile(absPath);
|
|
889
|
+
const rustMacroExpansion = require('./rust-macro-expansion');
|
|
890
|
+
rustMacroExpansion.applyRustMacroExpansion(this,
|
|
891
|
+
rustMacroExpansion.planRustMacroExpansion(this, { files: [absPath] }));
|
|
654
892
|
this.buildTime = 0;
|
|
655
893
|
}
|
|
656
894
|
|
|
657
895
|
/**
|
|
658
896
|
* Index a single file
|
|
659
897
|
*/
|
|
660
|
-
indexFile(filePath) {
|
|
898
|
+
indexFile(filePath, options = {}) {
|
|
661
899
|
const stat = fs.statSync(filePath);
|
|
662
900
|
const existing = this.files.get(filePath);
|
|
901
|
+
// `force` re-derives an unchanged file's parser facts (fix #374: a
|
|
902
|
+
// macro-expanded file whose macro definitions changed).
|
|
903
|
+
const force = options.force === true;
|
|
663
904
|
|
|
664
905
|
// Fast path: skip read entirely when mtime+size both match
|
|
665
|
-
if (existing && existing.mtime === stat.mtimeMs && existing.size === stat.size) {
|
|
906
|
+
if (!force && existing && existing.mtime === stat.mtimeMs && existing.size === stat.size) {
|
|
666
907
|
return false;
|
|
667
908
|
}
|
|
668
909
|
|
|
@@ -670,7 +911,7 @@ class ProjectIndex {
|
|
|
670
911
|
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
671
912
|
|
|
672
913
|
// Content-based skip: mtime changed but content didn't (touch, git checkout)
|
|
673
|
-
if (existing && existing.hash === hash) {
|
|
914
|
+
if (!force && existing && existing.hash === hash) {
|
|
674
915
|
existing.mtime = stat.mtimeMs;
|
|
675
916
|
existing.size = stat.size;
|
|
676
917
|
return false;
|
|
@@ -680,6 +921,9 @@ class ProjectIndex {
|
|
|
680
921
|
// A changed file can alter either even before a full rebuild reaches
|
|
681
922
|
// its graph phase, so no prior call-identity verdict may survive.
|
|
682
923
|
this._cppVisibleFilesCache?.clear();
|
|
924
|
+
this._textualIncludeClosureCache?.clear();
|
|
925
|
+
this._translationUnitLanguageCache?.clear();
|
|
926
|
+
if (this._compiledTranslationUnits !== undefined) this._compiledTranslationUnits = undefined;
|
|
683
927
|
this._cppTargetVisibilityCache?.clear();
|
|
684
928
|
this._cppMacroParamOutcomesCache?.clear();
|
|
685
929
|
|
|
@@ -692,7 +936,12 @@ class ProjectIndex {
|
|
|
692
936
|
|
|
693
937
|
const adapter = getLanguageAdapter(language);
|
|
694
938
|
const parser = getParser(language);
|
|
695
|
-
|
|
939
|
+
// C/C++: the project macro dictionary of the current build (fix
|
|
940
|
+
// #396), unless the caller supplies the view.
|
|
941
|
+
const externalMacros = options.externalMacros !== undefined ? options.externalMacros
|
|
942
|
+
: this._macroDictionary ? require('./external-macros').contextFor(this._macroDictionary, language) : null;
|
|
943
|
+
const ir = adapter.analyze(content, parser, filePath,
|
|
944
|
+
externalMacros ? { externalMacros } : undefined);
|
|
696
945
|
const irFailures = validateFileIR(ir);
|
|
697
946
|
if (irFailures.length > 0) {
|
|
698
947
|
throw new Error(`Invalid ${language} IR: ${irFailures.join('; ')}`);
|
|
@@ -787,7 +1036,9 @@ class ProjectIndex {
|
|
|
787
1036
|
// Incrementally update callee index before deleting cached calls
|
|
788
1037
|
const oldCached = this.callsCache.get(filePath);
|
|
789
1038
|
if (oldCached) {
|
|
790
|
-
|
|
1039
|
+
const { withMacroExpansion } = require('./macro-expansion');
|
|
1040
|
+
this._removeFromCalleeIndex(filePath, withMacroExpansion(this, filePath, oldCached.calls,
|
|
1041
|
+
{ materialize: false }));
|
|
791
1042
|
}
|
|
792
1043
|
this.callsCache.delete(filePath);
|
|
793
1044
|
|
|
@@ -798,10 +1049,59 @@ class ProjectIndex {
|
|
|
798
1049
|
this._javaFileIndex = null;
|
|
799
1050
|
// Computed-dispatch diagnostics are project-wide and read source text.
|
|
800
1051
|
this._computedDispatchBlindspots = null;
|
|
1052
|
+
this._reflectionPatternIndex = null;
|
|
801
1053
|
// Endpoints cache is project-wide; safest to clear on any file removal.
|
|
802
1054
|
this._endpointsCache = null;
|
|
803
1055
|
}
|
|
804
1056
|
|
|
1057
|
+
/**
|
|
1058
|
+
* _canonicalizeOrder() after a change confined to `files` (their symbols
|
|
1059
|
+
* and bindings) and the symbol `names` involved (fix #375: Rust macro
|
|
1060
|
+
* expansion replaces a few files' declarations). Everything else is
|
|
1061
|
+
* already canonical, so the result equals a full pass.
|
|
1062
|
+
*/
|
|
1063
|
+
_canonicalizeTouched({ files, names }) {
|
|
1064
|
+
this._classIdentity = null;
|
|
1065
|
+
this._macroExpansion = null;
|
|
1066
|
+
this._macroSpellable = null; // fix #385: paste-spellability memo over the same symbols
|
|
1067
|
+
this._rustMacroState = null;
|
|
1068
|
+
this._cppScope = null;
|
|
1069
|
+
this._contractIndex = null;
|
|
1070
|
+
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js)
|
|
1071
|
+
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
1072
|
+
this._rustScopeMemo = null;
|
|
1073
|
+
const feCmp = (a, b) => (a.startLine - b.startLine)
|
|
1074
|
+
|| codeUnitCompare(String(a.type), String(b.type))
|
|
1075
|
+
|| codeUnitCompare(String(a.className || ''), String(b.className || ''))
|
|
1076
|
+
|| codeUnitCompare(String(a.name), String(b.name));
|
|
1077
|
+
for (const file of files) {
|
|
1078
|
+
const fe = this.files.get(file);
|
|
1079
|
+
if (Array.isArray(fe?.symbols)) fe.symbols.sort(feCmp);
|
|
1080
|
+
if (Array.isArray(fe?.bindings)) fe.bindings.sort(feCmp);
|
|
1081
|
+
}
|
|
1082
|
+
const defCmp = (a, b) => compareNames(a.relativePath || '', b.relativePath || '')
|
|
1083
|
+
|| (a.startLine - b.startLine)
|
|
1084
|
+
|| codeUnitCompare(String(a.type), String(b.type))
|
|
1085
|
+
|| codeUnitCompare(String(a.className || ''), String(b.className || ''))
|
|
1086
|
+
|| ((a.endLine || 0) - (b.endLine || 0));
|
|
1087
|
+
const present = [...names].filter(name => this.symbols.has(name)).sort();
|
|
1088
|
+
for (const name of present) this.symbols.get(name).sort(defCmp);
|
|
1089
|
+
// Untouched names are still in canonical order; merge the touched ones in.
|
|
1090
|
+
const merged = new Map();
|
|
1091
|
+
let t = 0;
|
|
1092
|
+
for (const [name, defs] of this.symbols) {
|
|
1093
|
+
if (names.has(name)) continue;
|
|
1094
|
+
while (t < present.length && present[t] < name) {
|
|
1095
|
+
merged.set(present[t], this.symbols.get(present[t]));
|
|
1096
|
+
t++;
|
|
1097
|
+
}
|
|
1098
|
+
merged.set(name, defs);
|
|
1099
|
+
}
|
|
1100
|
+
for (; t < present.length; t++) merged.set(present[t], this.symbols.get(present[t]));
|
|
1101
|
+
this.symbols = merged;
|
|
1102
|
+
if (this.calleeIndex) this._canonicalizeOrder();
|
|
1103
|
+
}
|
|
1104
|
+
|
|
805
1105
|
/**
|
|
806
1106
|
* Put every order-bearing index structure into ONE canonical order, so the
|
|
807
1107
|
* same repository content produces the same index state — and therefore
|
|
@@ -822,6 +1122,16 @@ class ProjectIndex {
|
|
|
822
1122
|
* by name with file sets in path order.
|
|
823
1123
|
*/
|
|
824
1124
|
_canonicalizeOrder() {
|
|
1125
|
+
// Symbol arrays are rebuilt below; identity memos keyed on them are stale.
|
|
1126
|
+
this._classIdentity = null;
|
|
1127
|
+
this._macroExpansion = null;
|
|
1128
|
+
this._macroSpellable = null; // fix #385: paste-spellability memo over the same symbols
|
|
1129
|
+
this._rustMacroState = null;
|
|
1130
|
+
this._cppScope = null;
|
|
1131
|
+
this._contractIndex = null;
|
|
1132
|
+
this._typeDenotation = null; // type-name denotation memo (core/type-denotation.js)
|
|
1133
|
+
this._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
1134
|
+
this._rustScopeMemo = null; // fix #368: Rust glob/bound resolution memo, index-lifetime
|
|
825
1135
|
// Plain code-unit comparison — canonical order must not depend on the
|
|
826
1136
|
// host ICU locale (localeCompare does).
|
|
827
1137
|
const cmpStr = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -943,13 +1253,15 @@ class ProjectIndex {
|
|
|
943
1253
|
buildCalleeIndex() {
|
|
944
1254
|
const { getCachedCalls } = require('./callers');
|
|
945
1255
|
const { ensureCallsCacheLoaded } = require('./cache');
|
|
1256
|
+
const { withMacroExpansion } = require('./macro-expansion');
|
|
946
1257
|
ensureCallsCacheLoaded(this);
|
|
947
1258
|
this.calleeIndex = new Map();
|
|
948
1259
|
|
|
949
1260
|
for (const [filePath] of this.files) {
|
|
950
1261
|
// Fast path: use pre-populated callsCache (avoids stat per file)
|
|
951
1262
|
const cached = this.callsCache.get(filePath);
|
|
952
|
-
const calls = cached ? cached.calls :
|
|
1263
|
+
const calls = cached ? withMacroExpansion(this, filePath, cached.calls, { materialize: false })
|
|
1264
|
+
: getCachedCalls(this, filePath);
|
|
953
1265
|
this._addToCalleeIndex(filePath, calls);
|
|
954
1266
|
}
|
|
955
1267
|
}
|
|
@@ -1018,6 +1330,13 @@ class ProjectIndex {
|
|
|
1018
1330
|
// Prefer same-file match
|
|
1019
1331
|
const match = entries.find(e => e.file === contextFile);
|
|
1020
1332
|
if (match) return match.parents;
|
|
1333
|
+
// A file that declares the class itself without a base clause
|
|
1334
|
+
// names no parents (fix #390): a same-name class elsewhere
|
|
1335
|
+
// (another package's `ListTest extends TestCase`) is a different
|
|
1336
|
+
// class. Only partial types gather bases from other files, and a
|
|
1337
|
+
// C/C++ declaration may be a forward declaration of a class
|
|
1338
|
+
// defined in another file.
|
|
1339
|
+
if (contextFile && require('./class-identity').fileDeclaresClass(this, className, contextFile)) return null;
|
|
1021
1340
|
|
|
1022
1341
|
// Try imported file
|
|
1023
1342
|
if (contextFile) {
|
|
@@ -1066,6 +1385,34 @@ class ProjectIndex {
|
|
|
1066
1385
|
* @returns {string|null} Resolved file path
|
|
1067
1386
|
*/
|
|
1068
1387
|
_resolveClassFile(className, contextFile) {
|
|
1388
|
+
const memo = this._opMemo('resolveClassFile');
|
|
1389
|
+
if (!memo) return this._resolveClassFileUncached(className, contextFile);
|
|
1390
|
+
const key = `${className}\0${contextFile}`;
|
|
1391
|
+
let file = memo.get(key);
|
|
1392
|
+
if (file === undefined) {
|
|
1393
|
+
file = this._resolveClassFileUncached(className, contextFile);
|
|
1394
|
+
memo.set(key, file);
|
|
1395
|
+
}
|
|
1396
|
+
return file;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* A memo for the current operation (fix #382), or null outside one. Only
|
|
1401
|
+
* for values that are pure functions of the index, which an operation
|
|
1402
|
+
* never changes.
|
|
1403
|
+
*/
|
|
1404
|
+
_opMemo(key, make) {
|
|
1405
|
+
const memos = this._opMemos;
|
|
1406
|
+
if (!memos) return null;
|
|
1407
|
+
let memo = memos.get(key);
|
|
1408
|
+
if (!memo) {
|
|
1409
|
+
memo = make ? make() : new Map();
|
|
1410
|
+
memos.set(key, memo);
|
|
1411
|
+
}
|
|
1412
|
+
return memo;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
_resolveClassFileUncached(className, contextFile) {
|
|
1069
1416
|
const symbols = this.symbols.get(className);
|
|
1070
1417
|
if (!symbols) return contextFile;
|
|
1071
1418
|
const classSymbols = symbols.filter(s =>
|
|
@@ -1703,28 +2050,38 @@ class ProjectIndex {
|
|
|
1703
2050
|
return symbol.file === definition.file;
|
|
1704
2051
|
};
|
|
1705
2052
|
|
|
2053
|
+
// A member declared in another same-name type's body (a
|
|
2054
|
+
// function-local class beside its module-level namesake, C#
|
|
2055
|
+
// `Outcome` beside `Outcome<T>`) is not this type's (fix #389).
|
|
2056
|
+
const { ownerDefinitionOf, classKeyOf } = require('./class-identity');
|
|
2057
|
+
const typeKey = definition && definition.name === baseTypeName ? classKeyOf(this, definition) : null;
|
|
2058
|
+
const ownedHere = symbol => {
|
|
2059
|
+
if (!typeKey) return true;
|
|
2060
|
+
const owner = ownerDefinitionOf(this, symbol);
|
|
2061
|
+
return !owner || classKeyOf(this, owner) === typeKey;
|
|
2062
|
+
};
|
|
2063
|
+
|
|
1706
2064
|
for (const [, symbols] of this.symbols) {
|
|
1707
2065
|
for (const symbol of symbols) {
|
|
1708
2066
|
// Skip non-method types (fields, properties, etc.)
|
|
1709
2067
|
if (symbol.type === 'field' || symbol.type === 'property') {
|
|
1710
2068
|
continue;
|
|
1711
2069
|
}
|
|
1712
|
-
|
|
1713
2070
|
// Check Go/Rust-style receiver (e.g., func (r *Router) Method())
|
|
1714
2071
|
// Also matches Rust associated functions (have receiver but isMethod=false)
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
if (receiverBase === baseTypeName && sameCompilerType(symbol)) {
|
|
1718
|
-
methods.push(symbol);
|
|
1719
|
-
continue;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
|
|
2072
|
+
const receiverMatch = !!symbol.receiver &&
|
|
2073
|
+
symbol.receiver.replace(/^\*/, '') === baseTypeName;
|
|
1723
2074
|
// Check Python/Java/JS-style className (class members)
|
|
1724
2075
|
// Must be a method type, not just any symbol with className
|
|
1725
|
-
|
|
1726
|
-
(symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor')
|
|
1727
|
-
|
|
2076
|
+
const classMatch = symbol.className === baseTypeName &&
|
|
2077
|
+
(symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor');
|
|
2078
|
+
// Ownership is resolved for members of the name only.
|
|
2079
|
+
if ((!receiverMatch && !classMatch) || !ownedHere(symbol)) continue;
|
|
2080
|
+
if (receiverMatch && sameCompilerType(symbol)) {
|
|
2081
|
+
methods.push(symbol);
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (classMatch && sameCompilerType(symbol)) {
|
|
1728
2085
|
methods.push(symbol);
|
|
1729
2086
|
continue;
|
|
1730
2087
|
}
|
|
@@ -1992,7 +2349,11 @@ class ProjectIndex {
|
|
|
1992
2349
|
return false;
|
|
1993
2350
|
}
|
|
1994
2351
|
|
|
1995
|
-
|
|
2352
|
+
// The operation's shared tree when one is active (fix #365): a
|
|
2353
|
+
// rename plan parses the same large files for its own edits.
|
|
2354
|
+
const tree = (this.files.has(filePath) &&
|
|
2355
|
+
this._getParsedTree(filePath, content, language)) ||
|
|
2356
|
+
safeParse(parser, content);
|
|
1996
2357
|
if (!tree) return false;
|
|
1997
2358
|
const tokenType = getTokenTypeAtPosition(tree.rootNode, lineNum, column);
|
|
1998
2359
|
return tokenType === 'comment' || tokenType === 'string';
|
|
@@ -2212,6 +2573,31 @@ class ProjectIndex {
|
|
|
2212
2573
|
return keywords ? keywords.has(name) : false;
|
|
2213
2574
|
}
|
|
2214
2575
|
|
|
2576
|
+
/**
|
|
2577
|
+
* A file's symbols with this name declared on this line, in file order
|
|
2578
|
+
* (fix #375: generated calls look up their parser-given enclosing
|
|
2579
|
+
* function; a file with hundreds of generated functions made that a scan
|
|
2580
|
+
* per call).
|
|
2581
|
+
*/
|
|
2582
|
+
_symbolsNamedAt(fileEntry, name, startLine) {
|
|
2583
|
+
// Keyed by the symbols array itself (replaced whenever a file's
|
|
2584
|
+
// symbols are rebuilt) and its length.
|
|
2585
|
+
const symbols = fileEntry.symbols;
|
|
2586
|
+
let memo = symbolsByNameAndLine.get(symbols);
|
|
2587
|
+
if (!memo || memo.length !== symbols.length) {
|
|
2588
|
+
const byName = new Map();
|
|
2589
|
+
for (const symbol of symbols) {
|
|
2590
|
+
const at = `${symbol.name}\0${symbol.startLine}`;
|
|
2591
|
+
const list = byName.get(at);
|
|
2592
|
+
if (list) list.push(symbol);
|
|
2593
|
+
else byName.set(at, [symbol]);
|
|
2594
|
+
}
|
|
2595
|
+
memo = { length: symbols.length, byName };
|
|
2596
|
+
symbolsByNameAndLine.set(symbols, memo);
|
|
2597
|
+
}
|
|
2598
|
+
return memo.byName.get(`${name}\0${startLine}`) || [];
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2215
2601
|
/**
|
|
2216
2602
|
* Find the enclosing function at a line
|
|
2217
2603
|
* @param {string} filePath - File path
|
|
@@ -2219,14 +2605,28 @@ class ProjectIndex {
|
|
|
2219
2605
|
* @param {boolean} returnSymbol - If true, return full symbol info instead of just name
|
|
2220
2606
|
* @returns {string|object|null} Function name, symbol object, or null
|
|
2221
2607
|
*/
|
|
2222
|
-
findEnclosingFunction(filePath, lineNum, returnSymbol = false) {
|
|
2608
|
+
findEnclosingFunction(filePath, lineNum, returnSymbol = false, call = null) {
|
|
2223
2609
|
const fileEntry = this.files.get(filePath);
|
|
2224
2610
|
if (!fileEntry) return null;
|
|
2225
2611
|
|
|
2226
|
-
//
|
|
2227
|
-
|
|
2612
|
+
// A call generated by a macro expansion (fix #374) shares its line
|
|
2613
|
+
// with every function that expansion generated there; the parser's
|
|
2614
|
+
// own enclosing function picks among them.
|
|
2615
|
+
const hint = call?.macroExpansion && call.enclosingFunction;
|
|
2616
|
+
if (hint?.name) {
|
|
2617
|
+
const exact = this._symbolsNamedAt(fileEntry, hint.name, hint.startLine)
|
|
2618
|
+
.find(symbol => !NON_CALLABLE_TYPES.has(symbol.type) &&
|
|
2619
|
+
symbol.startLine <= lineNum && symbol.endLine >= lineNum);
|
|
2620
|
+
if (exact) return returnSymbol ? exact : exact.name;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// Per-operation cache: avoid rescanning symbols for same (file, line),
|
|
2624
|
+
// keyed per file then line (no composed key string per call).
|
|
2625
|
+
let byLine = null;
|
|
2228
2626
|
if (this._opEnclosingFnCache) {
|
|
2229
|
-
|
|
2627
|
+
byLine = this._opEnclosingFnCache.get(filePath);
|
|
2628
|
+
if (!byLine) this._opEnclosingFnCache.set(filePath, byLine = new Map());
|
|
2629
|
+
const cached = byLine.get(lineNum);
|
|
2230
2630
|
if (cached !== undefined) {
|
|
2231
2631
|
return cached === null ? null : (returnSymbol ? cached : cached.name);
|
|
2232
2632
|
}
|
|
@@ -2243,9 +2643,7 @@ class ProjectIndex {
|
|
|
2243
2643
|
}
|
|
2244
2644
|
}
|
|
2245
2645
|
|
|
2246
|
-
if (
|
|
2247
|
-
this._opEnclosingFnCache.set(cacheKey, best);
|
|
2248
|
-
}
|
|
2646
|
+
if (byLine) byLine.set(lineNum, best);
|
|
2249
2647
|
return best ? (returnSymbol ? best : best.name) : null;
|
|
2250
2648
|
}
|
|
2251
2649
|
|
|
@@ -2404,10 +2802,10 @@ class ProjectIndex {
|
|
|
2404
2802
|
|
|
2405
2803
|
|
|
2406
2804
|
/** Build a usage index for all identifiers in the codebase (optimized for deadcode) */
|
|
2407
|
-
buildUsageIndex() { return deadcodeModule.buildUsageIndex(this); }
|
|
2805
|
+
buildUsageIndex() { return deadcodeModule().buildUsageIndex(this); }
|
|
2408
2806
|
|
|
2409
2807
|
/** Find dead code (unused functions/classes) */
|
|
2410
|
-
deadcode(options) { return deadcodeModule.deadcode(this, options); }
|
|
2808
|
+
deadcode(options) { return deadcodeModule().deadcode(this, options); }
|
|
2411
2809
|
|
|
2412
2810
|
/**
|
|
2413
2811
|
* Get dependency graph for a file
|
|
@@ -2535,6 +2933,14 @@ class ProjectIndex {
|
|
|
2535
2933
|
/** Save index to cache file */
|
|
2536
2934
|
saveCache(cachePath) { return indexCache.saveCache(this, cachePath); }
|
|
2537
2935
|
|
|
2936
|
+
/**
|
|
2937
|
+
* Derive whole the Rust files whose macro-expansion calls were derived
|
|
2938
|
+
* in part (fix #405), so a saved cache holds them. Returns the count.
|
|
2939
|
+
*/
|
|
2940
|
+
completeMacroCallDerivations() {
|
|
2941
|
+
return this._rustMacroState ? require('./rust-macro-expansion').completeRustMacroCalls(this) : 0;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2538
2944
|
/** Load index from cache file */
|
|
2539
2945
|
/**
|
|
2540
2946
|
* Build under the cross-process build lock (fix #354) and save the cache.
|