ucn 4.2.2 → 5.0.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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +445 -300
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -131
- package/core/cache.js +533 -11
- package/core/callers.js +5533 -494
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +421 -20
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +204 -42
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +216 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -177
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +371 -116
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +428 -16
- package/languages/javascript.js +452 -49
- package/languages/python.js +1041 -32
- package/languages/rust.js +1415 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +41 -24
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/project.js
CHANGED
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const {
|
|
12
|
+
expandGlob, findProjectRoot, detectProjectPattern, isTestFile,
|
|
13
|
+
parseGitignore, gitTrackedPaths, DEFAULT_IGNORES, compareNames, classifyUnsupportedSourceFile,
|
|
14
|
+
} = require('./discovery');
|
|
15
|
+
const { cleanHtmlScriptTags } = require('./parser');
|
|
16
|
+
const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits, PARSE_OPTIONS } = require('../languages');
|
|
17
|
+
const { validateFileIR } = require('./ir');
|
|
18
|
+
const { createFileEntryFromIR, populateFileEntryFromIR } = require('./index-ir');
|
|
15
19
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
16
20
|
const { escapeRegExp, NON_CALLABLE_TYPES, codeUnitCompare } = require('./shared');
|
|
17
21
|
const stacktrace = require('./stacktrace');
|
|
@@ -29,6 +33,15 @@ const reportingModule = require('./reporting');
|
|
|
29
33
|
// Lazy-initialized per-language keyword sets (populated on first isKeyword call)
|
|
30
34
|
let LANGUAGE_KEYWORDS = null;
|
|
31
35
|
|
|
36
|
+
// Query-time ASTs are immutable for the lifetime of an indexed file. Keep a
|
|
37
|
+
// small cross-operation LRU so a sequence of agent queries does not reparse the
|
|
38
|
+
// same large headers/modules for every conservation-account classification.
|
|
39
|
+
// The source-byte budget is deliberately conservative because tree-sitter's
|
|
40
|
+
// native tree is larger than its source. Evictions explicitly release native
|
|
41
|
+
// memory instead of waiting for N-API finalizers.
|
|
42
|
+
const PARSED_TREE_CACHE_MAX_ENTRIES = 128;
|
|
43
|
+
const PARSED_TREE_CACHE_MAX_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
44
|
+
|
|
32
45
|
/**
|
|
33
46
|
* ProjectIndex - Manages symbol table for a project
|
|
34
47
|
*/
|
|
@@ -47,15 +60,27 @@ class ProjectIndex {
|
|
|
47
60
|
this.extendedByGraph = new Map(); // parentName -> [childInfo]
|
|
48
61
|
this.config = this.loadConfig();
|
|
49
62
|
this.buildTime = null;
|
|
63
|
+
// Build telemetry is intentionally transient (never cached). Release
|
|
64
|
+
// performance gates use it to prove the requested worker shape was
|
|
65
|
+
// actually exercised instead of inferring parallelism from host CPUs.
|
|
66
|
+
this.lastBuildWorkerCount = 1;
|
|
67
|
+
this.lastBuildParallelEligible = false;
|
|
68
|
+
this.lastBuildRequestedWorkers = null;
|
|
50
69
|
this.callsCache = new Map(); // filePath -> { mtime, hash, calls, content }
|
|
51
70
|
this.callsCacheDirty = false; // set by getCachedCalls when entries are added or mutated
|
|
71
|
+
this.computedDispatchDirty = false; // persisted project-wide AST blind-spot inventory
|
|
52
72
|
this.failedFiles = new Set(); // files that failed to index (e.g. large minified bundles)
|
|
73
|
+
this.unsupportedFiles = []; // common source files skipped by the parser registry
|
|
74
|
+
this.discoveryIssues = []; // explicit reasons the index may be partial
|
|
53
75
|
this._opContentCache = null; // per-operation file content cache (Map<filePath, string>)
|
|
54
76
|
this._opUsagesCache = null; // per-operation findUsagesInCode cache (Map<"file:name", usages[]>)
|
|
55
77
|
this._opTreeCache = null; // per-operation parsed-tree cache (Map<filePath, tree|null>, bounded FIFO)
|
|
78
|
+
this._opTransientTrees = null; // evicted/errored trees kept alive until the active operation ends
|
|
56
79
|
this._opLinesCache = null; // per-operation split-lines cache (Map<filePath, string[]>, bounded FIFO)
|
|
57
80
|
this._opInnerSymbolRangesCache = null; // per-operation sorted class-method ranges by file
|
|
58
81
|
this._opFlowTypeOriginCache = null; // per-operation annotation type identity results
|
|
82
|
+
this._parsedTreeCache = new Map(); // cross-operation LRU: filePath -> immutable tree entry
|
|
83
|
+
this._parsedTreeCacheSourceBytes = 0;
|
|
59
84
|
this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
|
|
60
85
|
}
|
|
61
86
|
|
|
@@ -84,6 +109,7 @@ class ProjectIndex {
|
|
|
84
109
|
this._opUsageTotalsCache = new Map();
|
|
85
110
|
this._opEnclosingFnCache = new Map();
|
|
86
111
|
this._opTreeCache = new Map();
|
|
112
|
+
this._opTransientTrees = new Set();
|
|
87
113
|
this._opLinesCache = new Map();
|
|
88
114
|
// Query-time semantic derivations that depend only on one file's
|
|
89
115
|
// immutable call records. Reachability may ask findCallees() for
|
|
@@ -109,6 +135,10 @@ class ProjectIndex {
|
|
|
109
135
|
this._opUsageTotalsCache = null;
|
|
110
136
|
this._opEnclosingFnCache = null;
|
|
111
137
|
this._opTreeCache = null;
|
|
138
|
+
if (this._opTransientTrees) {
|
|
139
|
+
for (const tree of this._opTransientTrees) tree?.delete?.();
|
|
140
|
+
}
|
|
141
|
+
this._opTransientTrees = null;
|
|
112
142
|
this._opLinesCache = null;
|
|
113
143
|
this._opReturnTypeFlowCache = null;
|
|
114
144
|
this._opCallsByLineCache = null;
|
|
@@ -124,11 +154,46 @@ class ProjectIndex {
|
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
156
|
|
|
157
|
+
/** Remove one persistent parsed tree, deferring disposal if an operation still uses it. */
|
|
158
|
+
_evictParsedTree(filePath) {
|
|
159
|
+
const entry = this._parsedTreeCache.get(filePath);
|
|
160
|
+
if (!entry) return;
|
|
161
|
+
this._parsedTreeCache.delete(filePath);
|
|
162
|
+
this._parsedTreeCacheSourceBytes -= entry.sourceBytes;
|
|
163
|
+
if (this._opTreeCache?.get(filePath) === entry.tree) {
|
|
164
|
+
this._opTransientTrees?.add(entry.tree);
|
|
165
|
+
} else {
|
|
166
|
+
entry.tree?.delete?.();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Dispose every cross-operation tree. Used when an index is replaced from cache. */
|
|
171
|
+
_clearParsedTreeCache() {
|
|
172
|
+
for (const filePath of [...this._parsedTreeCache.keys()]) {
|
|
173
|
+
this._evictParsedTree(filePath);
|
|
174
|
+
}
|
|
175
|
+
this._parsedTreeCacheSourceBytes = 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_cacheParsedTree(filePath, language, tree, sourceBytes) {
|
|
179
|
+
const fileHash = this.files.get(filePath)?.hash || null;
|
|
180
|
+
this._evictParsedTree(filePath);
|
|
181
|
+
this._parsedTreeCache.set(filePath, {
|
|
182
|
+
tree, language, fileHash, sourceBytes,
|
|
183
|
+
});
|
|
184
|
+
this._parsedTreeCacheSourceBytes += sourceBytes;
|
|
185
|
+
while (this._parsedTreeCache.size > PARSED_TREE_CACHE_MAX_ENTRIES ||
|
|
186
|
+
this._parsedTreeCacheSourceBytes > PARSED_TREE_CACHE_MAX_SOURCE_BYTES) {
|
|
187
|
+
this._evictParsedTree(this._parsedTreeCache.keys().next().value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
127
191
|
/**
|
|
128
|
-
* Parse a file once
|
|
192
|
+
* Parse a file once and reuse the immutable tree across operations.
|
|
129
193
|
* Multi-symbol commands (diff-impact, check) classify ground lines for MANY
|
|
130
194
|
* names against the SAME files; without this, each (file, name) pair costs a
|
|
131
|
-
* full tree-sitter parse.
|
|
195
|
+
* full tree-sitter parse. Both operation and persistent caches are bounded
|
|
196
|
+
* because trees hold native memory.
|
|
132
197
|
* Returns null when parsing fails or no operation cache is active — callers
|
|
133
198
|
* fall back to parsing themselves.
|
|
134
199
|
*/
|
|
@@ -136,6 +201,19 @@ class ProjectIndex {
|
|
|
136
201
|
if (!this._opTreeCache) return null;
|
|
137
202
|
const cached = this._opTreeCache.get(filePath);
|
|
138
203
|
if (cached !== undefined) return cached;
|
|
204
|
+
|
|
205
|
+
const persistent = this._parsedTreeCache.get(filePath);
|
|
206
|
+
const currentHash = this.files.get(filePath)?.hash || null;
|
|
207
|
+
if (persistent && persistent.language === language &&
|
|
208
|
+
persistent.fileHash === currentHash) {
|
|
209
|
+
// LRU touch.
|
|
210
|
+
this._parsedTreeCache.delete(filePath);
|
|
211
|
+
this._parsedTreeCache.set(filePath, persistent);
|
|
212
|
+
this._opTreeCache.set(filePath, persistent.tree);
|
|
213
|
+
return persistent.tree;
|
|
214
|
+
}
|
|
215
|
+
if (persistent) this._evictParsedTree(filePath);
|
|
216
|
+
|
|
139
217
|
let tree;
|
|
140
218
|
try {
|
|
141
219
|
const parser = getParser(language);
|
|
@@ -147,6 +225,9 @@ class ProjectIndex {
|
|
|
147
225
|
this._opTreeCache.delete(this._opTreeCache.keys().next().value);
|
|
148
226
|
}
|
|
149
227
|
this._opTreeCache.set(filePath, tree);
|
|
228
|
+
if (tree) {
|
|
229
|
+
this._cacheParsedTree(filePath, language, tree, Buffer.byteLength(content));
|
|
230
|
+
}
|
|
150
231
|
return tree;
|
|
151
232
|
}
|
|
152
233
|
|
|
@@ -183,8 +264,13 @@ class ProjectIndex {
|
|
|
183
264
|
if (cached !== undefined) return cached;
|
|
184
265
|
}
|
|
185
266
|
|
|
186
|
-
|
|
187
|
-
|
|
267
|
+
// Header language is resolved during indexing from compilation
|
|
268
|
+
// databases/include context. Re-detecting `.h` here can choose C for
|
|
269
|
+
// a C++ header and silently drop member/template usages from the raw
|
|
270
|
+
// inventory. The indexed language is the authoritative parse mode.
|
|
271
|
+
const lang = this.files.get(filePath)?.language ||
|
|
272
|
+
detectLanguage(filePath, this.root);
|
|
273
|
+
const langModule = getLanguageAdapter(lang);
|
|
188
274
|
if (!langModule || typeof langModule.findUsagesInCode !== 'function') return null;
|
|
189
275
|
|
|
190
276
|
try {
|
|
@@ -202,7 +288,8 @@ class ProjectIndex {
|
|
|
202
288
|
if (!parser) return null;
|
|
203
289
|
// Language modules that accept a pre-parsed tree (4th param) reuse
|
|
204
290
|
// the per-operation tree cache; others (html) parse internally.
|
|
205
|
-
const tree = langModule.findUsagesInCode.length >= 4
|
|
291
|
+
const tree = langModule.findUsagesInCode.length >= 4 &&
|
|
292
|
+
!langModule.managesOwnParseTree
|
|
206
293
|
? this._getParsedTree(filePath, content, lang)
|
|
207
294
|
: null;
|
|
208
295
|
const usages = langModule.findUsagesInCode(content, name, parser, tree);
|
|
@@ -224,7 +311,7 @@ class ProjectIndex {
|
|
|
224
311
|
try {
|
|
225
312
|
return JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
226
313
|
} catch (e) {
|
|
227
|
-
|
|
314
|
+
this.configError = `.ucn.json could not be parsed: ${e.message}`;
|
|
228
315
|
}
|
|
229
316
|
}
|
|
230
317
|
return {};
|
|
@@ -233,10 +320,10 @@ class ProjectIndex {
|
|
|
233
320
|
/**
|
|
234
321
|
* Build index for files matching pattern
|
|
235
322
|
*
|
|
236
|
-
* @param {string} pattern - Glob pattern (e.g., "**\/*.js")
|
|
323
|
+
* @param {string|null} [pattern=null] - Glob pattern (e.g., "**\/*.js")
|
|
237
324
|
* @param {object} options - { forceRebuild, maxFiles, quiet }
|
|
238
325
|
*/
|
|
239
|
-
build(pattern, options = {}) {
|
|
326
|
+
build(pattern = null, options = {}) {
|
|
240
327
|
const startTime = Date.now();
|
|
241
328
|
const quiet = options.quiet !== false;
|
|
242
329
|
|
|
@@ -252,6 +339,23 @@ class ProjectIndex {
|
|
|
252
339
|
|
|
253
340
|
// Accept pre-expanded file array (glob mode) or a pattern string
|
|
254
341
|
let files;
|
|
342
|
+
const implicitProjectDiscovery = !Array.isArray(pattern) && !pattern;
|
|
343
|
+
this.unsupportedFiles = [];
|
|
344
|
+
this.discoveryIssues = [];
|
|
345
|
+
const discoveryIssueKeys = new Set();
|
|
346
|
+
const recordDiscoveryIssue = (issue) => {
|
|
347
|
+
const rel = path.relative(this.root, issue.path || this.root) || '.';
|
|
348
|
+
const key = `${issue.reason}\0${rel}`;
|
|
349
|
+
if (discoveryIssueKeys.has(key)) return;
|
|
350
|
+
discoveryIssueKeys.add(key);
|
|
351
|
+
this.discoveryIssues.push({ ...issue, relativePath: rel, path: undefined });
|
|
352
|
+
};
|
|
353
|
+
if (this.configError) {
|
|
354
|
+
recordDiscoveryIssue({
|
|
355
|
+
path: path.join(this.root, '.ucn.json'),
|
|
356
|
+
kind: 'file', reason: 'invalid-config', detail: this.configError,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
255
359
|
if (Array.isArray(pattern)) {
|
|
256
360
|
files = pattern;
|
|
257
361
|
} else {
|
|
@@ -262,23 +366,44 @@ class ProjectIndex {
|
|
|
262
366
|
const globOpts = {
|
|
263
367
|
root: this.root,
|
|
264
368
|
maxFiles: options.maxFiles || this.config.maxFiles || 50000,
|
|
369
|
+
maxDepth: options.maxDepth ?? this.config.maxDepth,
|
|
370
|
+
maxFileSize: options.maxFileSize ?? this.config.maxFileSize,
|
|
265
371
|
followSymlinks: options.followSymlinks
|
|
266
372
|
};
|
|
267
373
|
|
|
268
374
|
// Merge .gitignore and .ucn.json exclude into file discovery
|
|
269
375
|
const gitignorePatterns = parseGitignore(this.root);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
376
|
+
globOpts.gitignorePatterns = gitignorePatterns;
|
|
377
|
+
globOpts.trackedPaths = gitTrackedPaths(this.root);
|
|
378
|
+
const configExclude = Array.isArray(this.config.exclude)
|
|
379
|
+
? this.config.exclude
|
|
380
|
+
: (this.config.exclude ? [String(this.config.exclude)] : []);
|
|
381
|
+
if (configExclude.length > 0) {
|
|
382
|
+
globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
|
|
383
|
+
}
|
|
384
|
+
globOpts.disclosureIgnores = configExclude;
|
|
385
|
+
globOpts.onDiscoveryIssue = recordDiscoveryIssue;
|
|
386
|
+
|
|
387
|
+
if (implicitProjectDiscovery) {
|
|
388
|
+
globOpts.onSkippedFile = (filePath) => {
|
|
389
|
+
const kind = classifyUnsupportedSourceFile(filePath);
|
|
390
|
+
if (!kind) return;
|
|
391
|
+
this.unsupportedFiles.push({
|
|
392
|
+
relativePath: path.relative(this.root, filePath),
|
|
393
|
+
...kind,
|
|
394
|
+
});
|
|
395
|
+
};
|
|
273
396
|
}
|
|
274
|
-
|
|
275
397
|
files = expandGlob(pattern, globOpts);
|
|
398
|
+
this.unsupportedFiles.sort((a, b) => compareNames(a.relativePath, b.relativePath));
|
|
399
|
+
this.discoveryIssues.sort((a, b) => compareNames(a.relativePath, b.relativePath));
|
|
276
400
|
}
|
|
277
401
|
|
|
278
402
|
// Track if files were truncated by maxFiles limit
|
|
279
403
|
const maxFiles = options.maxFiles || this.config.maxFiles || 50000;
|
|
280
|
-
|
|
281
|
-
|
|
404
|
+
const maxFileIssues = this.discoveryIssues.filter(issue => issue.reason === 'max-files');
|
|
405
|
+
if (!Array.isArray(pattern) && maxFileIssues.length > 0) {
|
|
406
|
+
this.truncated = { indexed: files.length, maxFiles, skipped: maxFileIssues.length };
|
|
282
407
|
} else {
|
|
283
408
|
this.truncated = null;
|
|
284
409
|
}
|
|
@@ -318,6 +443,7 @@ class ProjectIndex {
|
|
|
318
443
|
// Always invalidate caches on rebuild
|
|
319
444
|
this._completenessCache = null;
|
|
320
445
|
this._attrTypeCache = null;
|
|
446
|
+
this._computedDispatchBlindspots = null;
|
|
321
447
|
// Endpoints cache (server routes / client requests / bridges) becomes
|
|
322
448
|
// stale when files change; clear on every rebuild.
|
|
323
449
|
this._endpointsCache = null;
|
|
@@ -330,13 +456,43 @@ class ProjectIndex {
|
|
|
330
456
|
const workersSetting = options.workers;
|
|
331
457
|
const envWorkers = parseInt(process.env.UCN_WORKERS, 10);
|
|
332
458
|
const disableParallel = workersSetting === 0 || envWorkers === 0;
|
|
459
|
+
const explicitWorkerCount = workersSetting > 0 || envWorkers > 0;
|
|
460
|
+
const requestedWorkerCount = workersSetting > 0
|
|
461
|
+
? workersSetting : (envWorkers > 0 ? envWorkers : null);
|
|
462
|
+
this.lastBuildWorkerCount = 1;
|
|
463
|
+
this.lastBuildRequestedWorkers = requestedWorkerCount;
|
|
333
464
|
let usedParallel = false;
|
|
334
465
|
|
|
335
|
-
|
|
466
|
+
// C/C++ recovery performs materially heavier parser work per file
|
|
467
|
+
// (attribute normalization plus bounded preprocessor views). Medium
|
|
468
|
+
// native projects therefore benefit from workers well before the
|
|
469
|
+
// generic 150-file crossover; measured release repos cross over at
|
|
470
|
+
// roughly 25 C-family files.
|
|
471
|
+
const cFamilyFileCount = files.reduce((count, filePath) => {
|
|
472
|
+
const language = detectLanguage(filePath, this.root);
|
|
473
|
+
return count + (language === 'c' || language === 'cpp' ? 1 : 0);
|
|
474
|
+
}, 0);
|
|
475
|
+
// An explicit worker count is a request for a reproducible execution
|
|
476
|
+
// shape (not merely an upper bound). This is useful to users tuning a
|
|
477
|
+
// constrained host and lets the release gate compare like with like.
|
|
478
|
+
const parallelWorthwhile = explicitWorkerCount
|
|
479
|
+
? files.length >= 2
|
|
480
|
+
: files.length > 150 || cFamilyFileCount >= 25;
|
|
481
|
+
this.lastBuildParallelEligible = !disableParallel && parallelWorthwhile;
|
|
482
|
+
if (!disableParallel && parallelWorthwhile) {
|
|
336
483
|
try {
|
|
337
484
|
const { parallelBuild } = require('./parallel-build');
|
|
338
485
|
const result = parallelBuild(this, files, {
|
|
339
486
|
workerCount: workersSetting > 0 ? workersSetting : (envWorkers > 0 ? envWorkers : undefined),
|
|
487
|
+
// After the recovery scorer stopped repeatedly walking
|
|
488
|
+
// every candidate tree, real-repo sweeps put the native
|
|
489
|
+
// throughput knee at 5-6 workers. Default to the lower
|
|
490
|
+
// knee (five) for RSS headroom; explicit user settings
|
|
491
|
+
// retain the ordinary eight-worker safety cap.
|
|
492
|
+
maxWorkers: cFamilyFileCount >= 25 && !explicitWorkerCount
|
|
493
|
+
? 5 : 8,
|
|
494
|
+
minFilesPerWorker: explicitWorkerCount
|
|
495
|
+
? 1 : (cFamilyFileCount >= 25 ? 10 : 100),
|
|
340
496
|
quiet,
|
|
341
497
|
});
|
|
342
498
|
if (result !== false) {
|
|
@@ -345,6 +501,7 @@ class ProjectIndex {
|
|
|
345
501
|
usedParallel = true;
|
|
346
502
|
}
|
|
347
503
|
} catch (e) {
|
|
504
|
+
this.lastBuildWorkerCount = 1;
|
|
348
505
|
if (!quiet) {
|
|
349
506
|
console.error(`Parallel build failed, falling back to sequential: ${e.message}`);
|
|
350
507
|
}
|
|
@@ -384,9 +541,10 @@ class ProjectIndex {
|
|
|
384
541
|
// avoiding the 2+ minute deferred cost when the first analysis command runs later.
|
|
385
542
|
this.buildCalleeIndex();
|
|
386
543
|
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
544
|
+
// Keep persisted iteration deterministic. Fresh and changed files
|
|
545
|
+
// already populated callsCache through the shared IR ingestion path;
|
|
546
|
+
// unchanged entries may have arrived from a sharded cache in a
|
|
547
|
+
// different order.
|
|
390
548
|
this.callsCache = new Map([...this.callsCache.entries()].sort((a, b) => compareNames(a[0], b[0])));
|
|
391
549
|
|
|
392
550
|
this.buildTime = Date.now() - startTime;
|
|
@@ -435,16 +593,16 @@ class ProjectIndex {
|
|
|
435
593
|
this.removeFileSymbols(filePath);
|
|
436
594
|
}
|
|
437
595
|
|
|
438
|
-
const language = detectLanguage(filePath);
|
|
596
|
+
const language = detectLanguage(filePath, this.root);
|
|
439
597
|
if (!language) return;
|
|
440
598
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
599
|
+
const adapter = getLanguageAdapter(language);
|
|
600
|
+
const parser = getParser(language);
|
|
601
|
+
const ir = adapter.analyze(content, parser, filePath);
|
|
602
|
+
const irFailures = validateFileIR(ir);
|
|
603
|
+
if (irFailures.length > 0) {
|
|
604
|
+
throw new Error(`Invalid ${language} IR: ${irFailures.join('; ')}`);
|
|
605
|
+
}
|
|
448
606
|
|
|
449
607
|
// Detect bundled/minified files (webpack bundles, minified code)
|
|
450
608
|
// These are build artifacts, not user-written source code
|
|
@@ -470,8 +628,12 @@ class ProjectIndex {
|
|
|
470
628
|
const isBundled = (() => {
|
|
471
629
|
// Webpack bundles contain __webpack_require__ or __webpack_modules__
|
|
472
630
|
if (content.includes('__webpack_require__') || content.includes('__webpack_modules__')) return true;
|
|
473
|
-
// Minified files:
|
|
474
|
-
|
|
631
|
+
// Minified files: large content with extreme bytes/line. Bundlers
|
|
632
|
+
// often insert 50-200 licence/chunk-separator lines, so an
|
|
633
|
+
// arbitrary <50 cap lets the same bytes flip classification when
|
|
634
|
+
// a banner is added. The size floor protects authored small files.
|
|
635
|
+
if (lineCount > 0 && content.length >= 100 * 1024 &&
|
|
636
|
+
content.length / lineCount > 500) return true;
|
|
475
637
|
// Very long single lines (> 1000 chars) in most of the file suggest minification
|
|
476
638
|
if (lineCount > 0 && longLineCount > 0 && longLineCount / lineCount > 0.3) return true;
|
|
477
639
|
return false;
|
|
@@ -484,140 +646,26 @@ class ProjectIndex {
|
|
|
484
646
|
content.slice(0, 500)
|
|
485
647
|
);
|
|
486
648
|
|
|
487
|
-
const fileEntry = {
|
|
488
|
-
|
|
649
|
+
const fileEntry = createFileEntryFromIR({
|
|
650
|
+
ir,
|
|
651
|
+
filePath,
|
|
489
652
|
relativePath: path.relative(this.root, filePath),
|
|
490
|
-
language,
|
|
491
|
-
lines: lineCount,
|
|
492
653
|
hash,
|
|
493
654
|
mtime: stat.mtimeMs,
|
|
494
655
|
size: stat.size,
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// the bare name for the whole file).
|
|
501
|
-
importBindings: imports.flatMap(i => (i.names || [])
|
|
502
|
-
.filter(n => n && n !== '*' && n !== '_' && n !== '.')
|
|
503
|
-
.map(n => {
|
|
504
|
-
// Rename pairing (fix #269): `{ validate: validateSchema }
|
|
505
|
-
// = require('./validation')` — the record's local alias
|
|
506
|
-
// pins to ITS module, not any module exporting the name.
|
|
507
|
-
const rn = (i.renames || []).find(r => r.original === n);
|
|
508
|
-
return { name: n, module: i.module, ...(rn && { alias: rn.local }) };
|
|
509
|
-
})),
|
|
510
|
-
exports: exports.map(e => e.name),
|
|
511
|
-
exportDetails: exports,
|
|
512
|
-
symbols: [],
|
|
513
|
-
bindings: [],
|
|
514
|
-
...(parsed.parseRecovery && { parseRecovery: true }),
|
|
515
|
-
...(importAliases && { importAliases }),
|
|
516
|
-
// Module-scope assignment targets (fix #217): names a module can
|
|
517
|
-
// expose WITHOUT a def/class/import binding (`render = impl`,
|
|
518
|
-
// `global name`). The import-binding name-chase treats these as
|
|
519
|
-
// undetermined — a dead-end verdict would be unsound.
|
|
520
|
-
...(parsed.moduleAssignedNames && { moduleAssignedNames: parsed.moduleAssignedNames }),
|
|
521
|
-
...(isBundled && { isBundled: true }),
|
|
522
|
-
...(isGenerated && { isGenerated: true })
|
|
523
|
-
};
|
|
524
|
-
fileEntry.dynamicImports = dynamicCount || 0;
|
|
525
|
-
|
|
526
|
-
// Add symbols
|
|
527
|
-
const addSymbol = (item, type) => {
|
|
528
|
-
const symbol = {
|
|
529
|
-
name: item.name,
|
|
530
|
-
type,
|
|
531
|
-
file: filePath,
|
|
532
|
-
relativePath: fileEntry.relativePath,
|
|
533
|
-
startLine: item.startLine,
|
|
534
|
-
endLine: item.endLine,
|
|
535
|
-
params: item.params,
|
|
536
|
-
paramsStructured: item.paramsStructured,
|
|
537
|
-
returnType: item.returnType,
|
|
538
|
-
...(item.returnedFunctionResult && { returnedFunctionResult: item.returnedFunctionResult }),
|
|
539
|
-
...(item.isFunctionVariable && { isFunctionVariable: true }),
|
|
540
|
-
...(item.paramTypes && { paramTypes: item.paramTypes }),
|
|
541
|
-
...(item.isAsync && { isAsync: true }),
|
|
542
|
-
...(item.isGenerator && { isGenerator: true }),
|
|
543
|
-
modifiers: item.modifiers,
|
|
544
|
-
docstring: item.docstring,
|
|
545
|
-
bindingId: `${fileEntry.relativePath}:${type}:${item.startLine}`,
|
|
546
|
-
...(item.generics && { generics: item.generics }),
|
|
547
|
-
...(item.extends && { extends: item.extends }),
|
|
548
|
-
...(item.implements && { implements: item.implements }),
|
|
549
|
-
...(item.indent !== undefined && { indent: item.indent }),
|
|
550
|
-
...(item.isNested && { isNested: item.isNested }),
|
|
551
|
-
...(item.isMethod && { isMethod: item.isMethod }),
|
|
552
|
-
...(item.receiver && { receiver: item.receiver }),
|
|
553
|
-
...(item.className && { className: item.className }),
|
|
554
|
-
...(item.memberType && { memberType: item.memberType }),
|
|
555
|
-
...(item.fieldType && { fieldType: item.fieldType }),
|
|
556
|
-
...(item.aliasOf && { aliasOf: item.aliasOf }),
|
|
557
|
-
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
558
|
-
// Decorator/annotation/attribute argument capture for endpoints command:
|
|
559
|
-
// these fields hold the parsed first-string-arg of each route-style annotation.
|
|
560
|
-
// Only populated when at least one entry has a string-literal arg, keeping memory
|
|
561
|
-
// overhead minimal for non-route code.
|
|
562
|
-
...(item.decoratorsWithArgs && item.decoratorsWithArgs.length > 0 && { decoratorsWithArgs: item.decoratorsWithArgs }),
|
|
563
|
-
...(item.annotationsWithArgs && item.annotationsWithArgs.length > 0 && { annotationsWithArgs: item.annotationsWithArgs }),
|
|
564
|
-
...(item.attributesWithArgs && item.attributesWithArgs.length > 0 && { attributesWithArgs: item.attributesWithArgs }),
|
|
565
|
-
...(item.nameLine && { nameLine: item.nameLine }),
|
|
566
|
-
...(item.traitImpl && { traitImpl: true }),
|
|
567
|
-
// Trait the impl block implements (rust `impl Trait for X`
|
|
568
|
-
// members) — external-contract detection needs the NAME, not
|
|
569
|
-
// just the traitImpl flag (fix #210).
|
|
570
|
-
...(item.traitName && { traitName: item.traitName }),
|
|
571
|
-
...(item.isSignature && { isSignature: true }),
|
|
572
|
-
...(item.memberAssigned && { memberAssigned: true }),
|
|
573
|
-
...(item.registryMember && { registryMember: true }),
|
|
574
|
-
...(item.registryContainer && { registryContainer: item.registryContainer })
|
|
575
|
-
};
|
|
576
|
-
fileEntry.symbols.push(symbol);
|
|
577
|
-
// Property-assignment defs (fix #269: Reply.prototype.serialize
|
|
578
|
-
// = function) declare no lexical name — a bare reference in the
|
|
579
|
-
// file can never bind them, so they never enter the bindings
|
|
580
|
-
// table (the symbol stays indexed and method-reachable).
|
|
581
|
-
if (!item.memberAssigned) {
|
|
582
|
-
fileEntry.bindings.push({
|
|
583
|
-
id: symbol.bindingId,
|
|
584
|
-
name: symbol.name,
|
|
585
|
-
type: symbol.type,
|
|
586
|
-
startLine: symbol.startLine
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (!this.symbols.has(item.name)) {
|
|
591
|
-
this.symbols.set(item.name, []);
|
|
592
|
-
}
|
|
593
|
-
this.symbols.get(item.name).push(symbol);
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
for (const fn of parsed.functions) {
|
|
597
|
-
// Go/Rust methods: set className from receiver for consistent method resolution.
|
|
598
|
-
// Go/Rust methods are standalone functions with receiver, not class members,
|
|
599
|
-
// so className is never set by the class member loop below.
|
|
600
|
-
if (fn.receiver && !fn.className) {
|
|
601
|
-
fn.className = fn.receiver.replace(/^\*/, '');
|
|
602
|
-
}
|
|
603
|
-
addSymbol(fn, fn.isConstructor ? 'constructor' : 'function');
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
for (const cls of parsed.classes) {
|
|
607
|
-
addSymbol(cls, cls.type || 'class');
|
|
608
|
-
if (cls.members) {
|
|
609
|
-
for (const m of cls.members) {
|
|
610
|
-
const memberType = m.memberType || 'method';
|
|
611
|
-
addSymbol({ ...m, className: cls.name, ...(cls.traitName && { traitImpl: true, traitName: cls.traitName }) }, memberType);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
for (const state of parsed.stateObjects) {
|
|
617
|
-
addSymbol(state, 'state');
|
|
618
|
-
}
|
|
656
|
+
lineCount,
|
|
657
|
+
isBundled,
|
|
658
|
+
isGenerated,
|
|
659
|
+
});
|
|
660
|
+
populateFileEntryFromIR(fileEntry, ir, this.symbols);
|
|
619
661
|
|
|
620
662
|
this.files.set(filePath, fileEntry);
|
|
663
|
+
this.callsCache.set(filePath, {
|
|
664
|
+
mtime: stat.mtimeMs,
|
|
665
|
+
hash,
|
|
666
|
+
calls: ir.calls,
|
|
667
|
+
});
|
|
668
|
+
this.callsCacheDirty = true;
|
|
621
669
|
return true;
|
|
622
670
|
}
|
|
623
671
|
|
|
@@ -628,6 +676,8 @@ class ProjectIndex {
|
|
|
628
676
|
const existing = this.files.get(filePath);
|
|
629
677
|
if (!existing) return;
|
|
630
678
|
|
|
679
|
+
this._evictParsedTree(filePath);
|
|
680
|
+
|
|
631
681
|
for (const symbol of existing.symbols) {
|
|
632
682
|
const entries = this.symbols.get(symbol.name);
|
|
633
683
|
if (entries) {
|
|
@@ -652,6 +702,8 @@ class ProjectIndex {
|
|
|
652
702
|
|
|
653
703
|
// Invalidate lazy Java file index (will be rebuilt on next use)
|
|
654
704
|
this._javaFileIndex = null;
|
|
705
|
+
// Computed-dispatch diagnostics are project-wide and read source text.
|
|
706
|
+
this._computedDispatchBlindspots = null;
|
|
655
707
|
// Endpoints cache is project-wide; safest to clear on any file removal.
|
|
656
708
|
this._endpointsCache = null;
|
|
657
709
|
}
|
|
@@ -748,6 +800,17 @@ class ProjectIndex {
|
|
|
748
800
|
}
|
|
749
801
|
}
|
|
750
802
|
}
|
|
803
|
+
// A compiler-recognized type qualifier is also a semantic use of
|
|
804
|
+
// the receiver type: `JValue.Compare(...)` references both
|
|
805
|
+
// JValue and Compare. Index the receiver so class/type queries do
|
|
806
|
+
// not fall back to a full project scan or leave the occurrence as
|
|
807
|
+
// an unexplained call-not-resolved line.
|
|
808
|
+
if (call.receiverIsTypeQualified && call.receiver) {
|
|
809
|
+
if (!this.calleeIndex.has(call.receiver)) {
|
|
810
|
+
this.calleeIndex.set(call.receiver, new Set());
|
|
811
|
+
}
|
|
812
|
+
this.calleeIndex.get(call.receiver).add(filePath);
|
|
813
|
+
}
|
|
751
814
|
}
|
|
752
815
|
}
|
|
753
816
|
|
|
@@ -772,6 +835,9 @@ class ProjectIndex {
|
|
|
772
835
|
if (rn !== call.name) removeName(rn);
|
|
773
836
|
}
|
|
774
837
|
}
|
|
838
|
+
if (call.receiverIsTypeQualified && call.receiver) {
|
|
839
|
+
removeName(call.receiver);
|
|
840
|
+
}
|
|
775
841
|
}
|
|
776
842
|
}
|
|
777
843
|
|
|
@@ -828,6 +894,10 @@ class ProjectIndex {
|
|
|
828
894
|
return graphBuildModule._resolveJavaPackageImport(this, importModule, javaFileIndex);
|
|
829
895
|
}
|
|
830
896
|
|
|
897
|
+
_resolveCSharpUsing(importModule) {
|
|
898
|
+
return graphBuildModule._resolveCSharpUsing(this, importModule);
|
|
899
|
+
}
|
|
900
|
+
|
|
831
901
|
/**
|
|
832
902
|
* Build import/export relationship graphs
|
|
833
903
|
*/
|
|
@@ -1038,9 +1108,11 @@ class ProjectIndex {
|
|
|
1038
1108
|
|
|
1039
1109
|
// Filter by file if specified
|
|
1040
1110
|
if (options.file) {
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1111
|
+
const resolvedFile = this.resolveFilePathForQuery(options.file);
|
|
1112
|
+
const filtered = typeof resolvedFile === 'string'
|
|
1113
|
+
? definitions.filter(d => d.file === resolvedFile)
|
|
1114
|
+
: definitions.filter(d =>
|
|
1115
|
+
d.relativePath && d.relativePath.includes(options.file));
|
|
1044
1116
|
if (filtered.length > 0) {
|
|
1045
1117
|
definitions = filtered;
|
|
1046
1118
|
}
|
|
@@ -1048,15 +1120,23 @@ class ProjectIndex {
|
|
|
1048
1120
|
|
|
1049
1121
|
// Filter by exact startLine when a handle was supplied. This pins
|
|
1050
1122
|
// the resolution to one specific definition — no ambiguity allowed.
|
|
1051
|
-
if (options.line &&
|
|
1052
|
-
const
|
|
1053
|
-
if (
|
|
1054
|
-
|
|
1123
|
+
if (options.line != null && options.line !== '') {
|
|
1124
|
+
const exactLine = Number(options.line);
|
|
1125
|
+
if (Number.isFinite(exactLine)) {
|
|
1126
|
+
// A line is an exact identity pin, never a ranking hint.
|
|
1127
|
+
// An unsatisfied pin must not fall back to another definition.
|
|
1128
|
+
definitions = definitions.filter(d =>
|
|
1129
|
+
d.startLine === exactLine || d.nameLine === exactLine);
|
|
1055
1130
|
}
|
|
1056
1131
|
}
|
|
1132
|
+
if (definitions.length === 0) {
|
|
1133
|
+
return { def: null, definitions: [], warnings: [] };
|
|
1134
|
+
}
|
|
1057
1135
|
|
|
1058
1136
|
// Score each definition for selection
|
|
1059
|
-
const typeOrder = new Set([
|
|
1137
|
+
const typeOrder = new Set([
|
|
1138
|
+
'class', 'struct', 'interface', 'type', 'impl', 'enum', 'record',
|
|
1139
|
+
]);
|
|
1060
1140
|
const { isTestPath } = require('./shared');
|
|
1061
1141
|
const scored = definitions.map(d => {
|
|
1062
1142
|
let score = 0;
|
|
@@ -1303,7 +1383,15 @@ class ProjectIndex {
|
|
|
1303
1383
|
}
|
|
1304
1384
|
|
|
1305
1385
|
const total = calls + definitions + imports;
|
|
1306
|
-
const result = {
|
|
1386
|
+
const result = {
|
|
1387
|
+
total,
|
|
1388
|
+
calls,
|
|
1389
|
+
definitions,
|
|
1390
|
+
imports,
|
|
1391
|
+
references: 0,
|
|
1392
|
+
complete: false,
|
|
1393
|
+
countKind: 'fast-disambiguation-excludes-references',
|
|
1394
|
+
};
|
|
1307
1395
|
if (memoKey) this._opUsageTotalsCache.set(memoKey, result);
|
|
1308
1396
|
return result;
|
|
1309
1397
|
}
|
|
@@ -1412,7 +1500,9 @@ class ProjectIndex {
|
|
|
1412
1500
|
calls,
|
|
1413
1501
|
definitions,
|
|
1414
1502
|
imports,
|
|
1415
|
-
references
|
|
1503
|
+
references,
|
|
1504
|
+
complete: true,
|
|
1505
|
+
countKind: 'detailed-ast-usages',
|
|
1416
1506
|
};
|
|
1417
1507
|
}
|
|
1418
1508
|
|
|
@@ -1434,11 +1524,53 @@ class ProjectIndex {
|
|
|
1434
1524
|
* @param {string} typeName - The class/struct/interface name
|
|
1435
1525
|
* @returns {Array} Methods belonging to this type
|
|
1436
1526
|
*/
|
|
1437
|
-
findMethodsForType(typeName) {
|
|
1527
|
+
findMethodsForType(typeName, definition = null) {
|
|
1438
1528
|
const methods = [];
|
|
1439
1529
|
// Match both "TypeName" and "*TypeName" receivers (for Go/Rust pointer receivers)
|
|
1440
1530
|
const baseTypeName = typeName.replace(/^\*/, '');
|
|
1441
1531
|
|
|
1532
|
+
const targetLanguage = definition?.file
|
|
1533
|
+
? this.files.get(definition.file)?.language : null;
|
|
1534
|
+
const targetNamespace = definition?.namespace || null;
|
|
1535
|
+
const targetDir = definition?.file ? path.dirname(definition.file) : null;
|
|
1536
|
+
const sameCompilerType = symbol => {
|
|
1537
|
+
if (!definition || !targetLanguage) return true;
|
|
1538
|
+
const language = this.files.get(symbol.file)?.language;
|
|
1539
|
+
if (language !== targetLanguage) return false;
|
|
1540
|
+
if (['javascript', 'typescript', 'tsx', 'python', 'html']
|
|
1541
|
+
.includes(targetLanguage)) return symbol.file === definition.file;
|
|
1542
|
+
if (targetLanguage === 'go') return path.dirname(symbol.file) === targetDir;
|
|
1543
|
+
if (targetLanguage === 'java' || targetLanguage === 'csharp') {
|
|
1544
|
+
return (symbol.namespace || null) === targetNamespace;
|
|
1545
|
+
}
|
|
1546
|
+
if (targetLanguage === 'c' || targetLanguage === 'cpp') {
|
|
1547
|
+
if (symbol.file === definition.file) return true;
|
|
1548
|
+
const reachable = (from, to) => {
|
|
1549
|
+
const queue = [from];
|
|
1550
|
+
const seen = new Set(queue);
|
|
1551
|
+
for (let depth = 0; queue.length > 0 && depth < 16; depth++) {
|
|
1552
|
+
const current = queue.shift();
|
|
1553
|
+
for (const imported of this.importGraph.get(current) || []) {
|
|
1554
|
+
if (imported === to) return true;
|
|
1555
|
+
if (!seen.has(imported)) {
|
|
1556
|
+
seen.add(imported);
|
|
1557
|
+
queue.push(imported);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return false;
|
|
1562
|
+
};
|
|
1563
|
+
return reachable(symbol.file, definition.file) ||
|
|
1564
|
+
reachable(definition.file, symbol.file);
|
|
1565
|
+
}
|
|
1566
|
+
if (targetLanguage === 'rust') {
|
|
1567
|
+
const typeDefs = (this.symbols.get(baseTypeName) || []).filter(candidate =>
|
|
1568
|
+
['struct', 'enum', 'trait', 'type'].includes(candidate.type));
|
|
1569
|
+
return typeDefs.length === 1 || symbol.file === definition.file;
|
|
1570
|
+
}
|
|
1571
|
+
return symbol.file === definition.file;
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1442
1574
|
for (const [, symbols] of this.symbols) {
|
|
1443
1575
|
for (const symbol of symbols) {
|
|
1444
1576
|
// Skip non-method types (fields, properties, etc.)
|
|
@@ -1450,7 +1582,7 @@ class ProjectIndex {
|
|
|
1450
1582
|
// Also matches Rust associated functions (have receiver but isMethod=false)
|
|
1451
1583
|
if (symbol.receiver) {
|
|
1452
1584
|
const receiverBase = symbol.receiver.replace(/^\*/, '');
|
|
1453
|
-
if (receiverBase === baseTypeName) {
|
|
1585
|
+
if (receiverBase === baseTypeName && sameCompilerType(symbol)) {
|
|
1454
1586
|
methods.push(symbol);
|
|
1455
1587
|
continue;
|
|
1456
1588
|
}
|
|
@@ -1459,7 +1591,8 @@ class ProjectIndex {
|
|
|
1459
1591
|
// Check Python/Java/JS-style className (class members)
|
|
1460
1592
|
// Must be a method type, not just any symbol with className
|
|
1461
1593
|
if (symbol.className === baseTypeName &&
|
|
1462
|
-
(symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor')
|
|
1594
|
+
(symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor') &&
|
|
1595
|
+
sameCompilerType(symbol)) {
|
|
1463
1596
|
methods.push(symbol);
|
|
1464
1597
|
continue;
|
|
1465
1598
|
}
|
|
@@ -1496,7 +1629,7 @@ class ProjectIndex {
|
|
|
1496
1629
|
* @returns {boolean} true if ALL occurrences of name are inside strings
|
|
1497
1630
|
*/
|
|
1498
1631
|
isInsideStringAST(content, lineNum, line, name, filePath) {
|
|
1499
|
-
const language = detectLanguage(filePath);
|
|
1632
|
+
const language = detectLanguage(filePath, this.root);
|
|
1500
1633
|
if (!language) {
|
|
1501
1634
|
return false; // Unsupported language - assume not inside string
|
|
1502
1635
|
}
|
|
@@ -1617,6 +1750,18 @@ class ProjectIndex {
|
|
|
1617
1750
|
parts.push(def.modifiers.join(' '));
|
|
1618
1751
|
}
|
|
1619
1752
|
parts.push(def.name);
|
|
1753
|
+
const dataMember = def.type === 'field' || def.type === 'state' ||
|
|
1754
|
+
def.memberType === 'field' || def.memberType === 'property';
|
|
1755
|
+
if (dataMember) {
|
|
1756
|
+
const declaredType = def.fieldType || def.returnType;
|
|
1757
|
+
if (declaredType) {
|
|
1758
|
+
parts.push(`: ${String(declaredType).replace(/\s+/g, ' ').trim()}`);
|
|
1759
|
+
}
|
|
1760
|
+
return parts.join(' ');
|
|
1761
|
+
}
|
|
1762
|
+
if (def.type === 'macro' && def.functionLike === false) {
|
|
1763
|
+
return parts.join(' ');
|
|
1764
|
+
}
|
|
1620
1765
|
if (def.params !== undefined) {
|
|
1621
1766
|
// Prefer typed rendering when paramTypes/paramsStructured carry annotations
|
|
1622
1767
|
const { renderTypedParams } = require('./output/shared');
|
|
@@ -1641,12 +1786,12 @@ class ProjectIndex {
|
|
|
1641
1786
|
* @returns {string} 'call', 'import', 'definition', or 'reference'
|
|
1642
1787
|
*/
|
|
1643
1788
|
classifyUsageAST(content, lineNum, name, filePath) {
|
|
1644
|
-
const language = detectLanguage(filePath);
|
|
1789
|
+
const language = detectLanguage(filePath, this.root);
|
|
1645
1790
|
if (!language) {
|
|
1646
1791
|
return null; // Signal to use fallback
|
|
1647
1792
|
}
|
|
1648
1793
|
|
|
1649
|
-
const langModule =
|
|
1794
|
+
const langModule = getLanguageAdapter(language);
|
|
1650
1795
|
if (!langModule || typeof langModule.findUsagesInCode !== 'function') {
|
|
1651
1796
|
return null;
|
|
1652
1797
|
}
|
|
@@ -1680,7 +1825,7 @@ class ProjectIndex {
|
|
|
1680
1825
|
* @returns {boolean}
|
|
1681
1826
|
*/
|
|
1682
1827
|
isCommentOrStringAtPosition(content, lineNum, column, filePath) {
|
|
1683
|
-
const language = detectLanguage(filePath);
|
|
1828
|
+
const language = detectLanguage(filePath, this.root);
|
|
1684
1829
|
if (!language) {
|
|
1685
1830
|
return false; // Can't determine, assume code
|
|
1686
1831
|
}
|
|
@@ -1844,6 +1989,62 @@ class ProjectIndex {
|
|
|
1844
1989
|
'FunctionalInterface', 'SafeVarargs',
|
|
1845
1990
|
'Iterable', 'Comparable', 'AutoCloseable', 'Cloneable',
|
|
1846
1991
|
'Enum', 'Record', 'Void'
|
|
1992
|
+
]),
|
|
1993
|
+
c: new Set([
|
|
1994
|
+
'auto', 'break', 'case', 'char', 'const', 'continue',
|
|
1995
|
+
'default', 'do', 'double', 'else', 'enum', 'extern',
|
|
1996
|
+
'float', 'for', 'goto', 'if', 'inline', 'int', 'long',
|
|
1997
|
+
'register', 'restrict', 'return', 'short', 'signed',
|
|
1998
|
+
'sizeof', 'static', 'struct', 'switch', 'typedef', 'union',
|
|
1999
|
+
'unsigned', 'void', 'volatile', 'while', '_Bool',
|
|
2000
|
+
'_Complex', '_Atomic', '_Generic', '_Noreturn',
|
|
2001
|
+
'NULL', 'true', 'false', 'size_t', 'ptrdiff_t',
|
|
2002
|
+
'printf', 'fprintf', 'sprintf', 'snprintf', 'scanf',
|
|
2003
|
+
'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove',
|
|
2004
|
+
'memset', 'strlen', 'strcmp', 'strcpy', 'strncpy',
|
|
2005
|
+
'fopen', 'fclose', 'fread', 'fwrite', 'exit', 'abort',
|
|
2006
|
+
]),
|
|
2007
|
+
cpp: new Set([
|
|
2008
|
+
'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto',
|
|
2009
|
+
'bitand', 'bitor', 'bool', 'break', 'case', 'catch',
|
|
2010
|
+
'char', 'char8_t', 'char16_t', 'char32_t', 'class',
|
|
2011
|
+
'compl', 'concept', 'const', 'consteval', 'constexpr',
|
|
2012
|
+
'constinit', 'const_cast', 'continue', 'co_await',
|
|
2013
|
+
'co_return', 'co_yield', 'decltype', 'default', 'delete',
|
|
2014
|
+
'do', 'double', 'dynamic_cast', 'else', 'enum', 'explicit',
|
|
2015
|
+
'export', 'extern', 'false', 'float', 'for', 'friend',
|
|
2016
|
+
'goto', 'if', 'inline', 'int', 'long', 'mutable',
|
|
2017
|
+
'namespace', 'new', 'noexcept', 'not', 'nullptr',
|
|
2018
|
+
'operator', 'or', 'private', 'protected', 'public',
|
|
2019
|
+
'register', 'reinterpret_cast', 'requires', 'return',
|
|
2020
|
+
'short', 'signed', 'sizeof', 'static', 'static_assert',
|
|
2021
|
+
'static_cast', 'struct', 'switch', 'template', 'this',
|
|
2022
|
+
'thread_local', 'throw', 'true', 'try', 'typedef',
|
|
2023
|
+
'typeid', 'typename', 'union', 'unsigned', 'using',
|
|
2024
|
+
'virtual', 'void', 'volatile', 'wchar_t', 'while',
|
|
2025
|
+
'std', 'string', 'vector', 'map', 'unordered_map', 'set',
|
|
2026
|
+
'unique_ptr', 'shared_ptr', 'weak_ptr', 'optional',
|
|
2027
|
+
'variant', 'tuple', 'move', 'forward', 'make_shared',
|
|
2028
|
+
'make_unique', 'cout', 'cerr', 'cin', 'endl',
|
|
2029
|
+
]),
|
|
2030
|
+
csharp: new Set([
|
|
2031
|
+
'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case',
|
|
2032
|
+
'catch', 'char', 'checked', 'class', 'const', 'continue',
|
|
2033
|
+
'decimal', 'default', 'delegate', 'do', 'double', 'else',
|
|
2034
|
+
'enum', 'event', 'explicit', 'extern', 'false', 'finally',
|
|
2035
|
+
'fixed', 'float', 'for', 'foreach', 'goto', 'if',
|
|
2036
|
+
'implicit', 'in', 'int', 'interface', 'internal', 'is',
|
|
2037
|
+
'lock', 'long', 'namespace', 'new', 'null', 'object',
|
|
2038
|
+
'operator', 'out', 'override', 'params', 'private',
|
|
2039
|
+
'protected', 'public', 'readonly', 'record', 'ref',
|
|
2040
|
+
'return', 'sbyte', 'sealed', 'short', 'sizeof',
|
|
2041
|
+
'stackalloc', 'static', 'string', 'struct', 'switch',
|
|
2042
|
+
'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong',
|
|
2043
|
+
'unchecked', 'unsafe', 'ushort', 'using', 'virtual',
|
|
2044
|
+
'void', 'volatile', 'while', 'async', 'await', 'var',
|
|
2045
|
+
'dynamic', 'yield', 'Console', 'Math', 'String', 'Object',
|
|
2046
|
+
'Task', 'ValueTask', 'List', 'Dictionary', 'HashSet',
|
|
2047
|
+
'IEnumerable', 'IDisposable', 'Exception',
|
|
1847
2048
|
])
|
|
1848
2049
|
};
|
|
1849
2050
|
// TypeScript/TSX share JavaScript keywords
|
|
@@ -2181,6 +2382,12 @@ class ProjectIndex {
|
|
|
2181
2382
|
/** Load index from cache file */
|
|
2182
2383
|
loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
|
|
2183
2384
|
|
|
2385
|
+
/** Return this project's default per-user cache file path. */
|
|
2386
|
+
getCachePath() { return indexCache.getProjectCachePath(this.root); }
|
|
2387
|
+
|
|
2388
|
+
/** Remove this project's per-user cache and any legacy project-local cache. */
|
|
2389
|
+
clearCache() { return indexCache.clearProjectCache(this.root); }
|
|
2390
|
+
|
|
2184
2391
|
/** Load callsCache from separate file on demand (called by findCallers/findCallees) */
|
|
2185
2392
|
loadCallsCache() { return indexCache.loadCallsCache(this); }
|
|
2186
2393
|
|