ucn 4.2.3 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- 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 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- 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 +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- 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 +212 -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 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- 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 +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- 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,150 +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 {
|
|
509
|
-
name: n,
|
|
510
|
-
module: i.module,
|
|
511
|
-
...(rn && { alias: rn.local }),
|
|
512
|
-
...(i.defaultLike && { defaultLike: true }),
|
|
513
|
-
};
|
|
514
|
-
})),
|
|
515
|
-
exports: exports.map(e => e.name),
|
|
516
|
-
exportDetails: exports,
|
|
517
|
-
symbols: [],
|
|
518
|
-
bindings: [],
|
|
519
|
-
...(parsed.parseRecovery && { parseRecovery: true }),
|
|
520
|
-
...(importAliases && { importAliases }),
|
|
521
|
-
// Module-scope assignment targets (fix #217): names a module can
|
|
522
|
-
// expose WITHOUT a def/class/import binding (`render = impl`,
|
|
523
|
-
// `global name`). The import-binding name-chase treats these as
|
|
524
|
-
// undetermined — a dead-end verdict would be unsound.
|
|
525
|
-
...(parsed.moduleAssignedNames && { moduleAssignedNames: parsed.moduleAssignedNames }),
|
|
526
|
-
...(isBundled && { isBundled: true }),
|
|
527
|
-
...(isGenerated && { isGenerated: true })
|
|
528
|
-
};
|
|
529
|
-
fileEntry.dynamicImports = dynamicCount || 0;
|
|
530
|
-
|
|
531
|
-
// Add symbols
|
|
532
|
-
const addSymbol = (item, type) => {
|
|
533
|
-
const symbol = {
|
|
534
|
-
name: item.name,
|
|
535
|
-
type,
|
|
536
|
-
file: filePath,
|
|
537
|
-
relativePath: fileEntry.relativePath,
|
|
538
|
-
startLine: item.startLine,
|
|
539
|
-
endLine: item.endLine,
|
|
540
|
-
params: item.params,
|
|
541
|
-
paramsStructured: item.paramsStructured,
|
|
542
|
-
returnType: item.returnType,
|
|
543
|
-
...(item.returnedFunctionResult && { returnedFunctionResult: item.returnedFunctionResult }),
|
|
544
|
-
...(item.isFunctionVariable && { isFunctionVariable: true }),
|
|
545
|
-
...(item.paramTypes && { paramTypes: item.paramTypes }),
|
|
546
|
-
...(item.isAsync && { isAsync: true }),
|
|
547
|
-
...(item.isGenerator && { isGenerator: true }),
|
|
548
|
-
modifiers: item.modifiers,
|
|
549
|
-
docstring: item.docstring,
|
|
550
|
-
bindingId: `${fileEntry.relativePath}:${type}:${item.startLine}`,
|
|
551
|
-
...(item.generics && { generics: item.generics }),
|
|
552
|
-
...(item.extends && { extends: item.extends }),
|
|
553
|
-
...(item.implements && { implements: item.implements }),
|
|
554
|
-
...(item.indent !== undefined && { indent: item.indent }),
|
|
555
|
-
...(item.isNested && { isNested: item.isNested }),
|
|
556
|
-
...(item.enclosingType && { enclosingType: item.enclosingType }),
|
|
557
|
-
...(item.isMethod && { isMethod: item.isMethod }),
|
|
558
|
-
...(item.receiver && { receiver: item.receiver }),
|
|
559
|
-
...(item.className && { className: item.className }),
|
|
560
|
-
...(item.memberType && { memberType: item.memberType }),
|
|
561
|
-
...(item.fieldType && { fieldType: item.fieldType }),
|
|
562
|
-
...(item.aliasOf && { aliasOf: item.aliasOf }),
|
|
563
|
-
...(item.derefTarget && { derefTarget: item.derefTarget }),
|
|
564
|
-
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
565
|
-
// Decorator/annotation/attribute argument capture for endpoints command:
|
|
566
|
-
// these fields hold the parsed first-string-arg of each route-style annotation.
|
|
567
|
-
// Only populated when at least one entry has a string-literal arg, keeping memory
|
|
568
|
-
// overhead minimal for non-route code.
|
|
569
|
-
...(item.decoratorsWithArgs && item.decoratorsWithArgs.length > 0 && { decoratorsWithArgs: item.decoratorsWithArgs }),
|
|
570
|
-
...(item.annotationsWithArgs && item.annotationsWithArgs.length > 0 && { annotationsWithArgs: item.annotationsWithArgs }),
|
|
571
|
-
...(item.attributesWithArgs && item.attributesWithArgs.length > 0 && { attributesWithArgs: item.attributesWithArgs }),
|
|
572
|
-
...(item.nameLine && { nameLine: item.nameLine }),
|
|
573
|
-
...(item.traitImpl && { traitImpl: true }),
|
|
574
|
-
// Trait the impl block implements (rust `impl Trait for X`
|
|
575
|
-
// members) — external-contract detection needs the NAME, not
|
|
576
|
-
// just the traitImpl flag (fix #210).
|
|
577
|
-
...(item.traitName && { traitName: item.traitName }),
|
|
578
|
-
...(item.isSignature && { isSignature: true }),
|
|
579
|
-
...(item.memberAssigned && { memberAssigned: true }),
|
|
580
|
-
...(item.bodyScopedName && { bodyScopedName: true }),
|
|
581
|
-
...(item.registryMember && { registryMember: true }),
|
|
582
|
-
...(item.registryContainer && { registryContainer: item.registryContainer })
|
|
583
|
-
};
|
|
584
|
-
fileEntry.symbols.push(symbol);
|
|
585
|
-
// Property-assignment defs (fix #269: Reply.prototype.serialize
|
|
586
|
-
// = function) declare no lexical name — a bare reference in the
|
|
587
|
-
// file can never bind them, so they never enter the bindings
|
|
588
|
-
// table (the symbol stays indexed and method-reachable). Named
|
|
589
|
-
// function expressions (bodyScopedName) bind only inside their
|
|
590
|
-
// own body (ECMA-262) — same consequence.
|
|
591
|
-
if (!item.memberAssigned && !item.bodyScopedName) {
|
|
592
|
-
fileEntry.bindings.push({
|
|
593
|
-
id: symbol.bindingId,
|
|
594
|
-
name: symbol.name,
|
|
595
|
-
type: symbol.type,
|
|
596
|
-
startLine: symbol.startLine
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (!this.symbols.has(item.name)) {
|
|
601
|
-
this.symbols.set(item.name, []);
|
|
602
|
-
}
|
|
603
|
-
this.symbols.get(item.name).push(symbol);
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
for (const fn of parsed.functions) {
|
|
607
|
-
// Go/Rust methods: set className from receiver for consistent method resolution.
|
|
608
|
-
// Go/Rust methods are standalone functions with receiver, not class members,
|
|
609
|
-
// so className is never set by the class member loop below.
|
|
610
|
-
if (fn.receiver && !fn.className) {
|
|
611
|
-
fn.className = fn.receiver.replace(/^\*/, '');
|
|
612
|
-
}
|
|
613
|
-
addSymbol(fn, fn.isConstructor ? 'constructor' : 'function');
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
for (const cls of parsed.classes) {
|
|
617
|
-
addSymbol(cls, cls.type || 'class');
|
|
618
|
-
if (cls.members) {
|
|
619
|
-
for (const m of cls.members) {
|
|
620
|
-
const memberType = m.memberType || 'method';
|
|
621
|
-
addSymbol({ ...m, className: cls.name, ...(cls.traitName && { traitImpl: true, traitName: cls.traitName }) }, memberType);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
for (const state of parsed.stateObjects) {
|
|
627
|
-
addSymbol(state, 'state');
|
|
628
|
-
}
|
|
656
|
+
lineCount,
|
|
657
|
+
isBundled,
|
|
658
|
+
isGenerated,
|
|
659
|
+
});
|
|
660
|
+
populateFileEntryFromIR(fileEntry, ir, this.symbols);
|
|
629
661
|
|
|
630
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;
|
|
631
669
|
return true;
|
|
632
670
|
}
|
|
633
671
|
|
|
@@ -638,6 +676,8 @@ class ProjectIndex {
|
|
|
638
676
|
const existing = this.files.get(filePath);
|
|
639
677
|
if (!existing) return;
|
|
640
678
|
|
|
679
|
+
this._evictParsedTree(filePath);
|
|
680
|
+
|
|
641
681
|
for (const symbol of existing.symbols) {
|
|
642
682
|
const entries = this.symbols.get(symbol.name);
|
|
643
683
|
if (entries) {
|
|
@@ -662,6 +702,8 @@ class ProjectIndex {
|
|
|
662
702
|
|
|
663
703
|
// Invalidate lazy Java file index (will be rebuilt on next use)
|
|
664
704
|
this._javaFileIndex = null;
|
|
705
|
+
// Computed-dispatch diagnostics are project-wide and read source text.
|
|
706
|
+
this._computedDispatchBlindspots = null;
|
|
665
707
|
// Endpoints cache is project-wide; safest to clear on any file removal.
|
|
666
708
|
this._endpointsCache = null;
|
|
667
709
|
}
|
|
@@ -758,6 +800,17 @@ class ProjectIndex {
|
|
|
758
800
|
}
|
|
759
801
|
}
|
|
760
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
|
+
}
|
|
761
814
|
}
|
|
762
815
|
}
|
|
763
816
|
|
|
@@ -782,6 +835,9 @@ class ProjectIndex {
|
|
|
782
835
|
if (rn !== call.name) removeName(rn);
|
|
783
836
|
}
|
|
784
837
|
}
|
|
838
|
+
if (call.receiverIsTypeQualified && call.receiver) {
|
|
839
|
+
removeName(call.receiver);
|
|
840
|
+
}
|
|
785
841
|
}
|
|
786
842
|
}
|
|
787
843
|
|
|
@@ -838,6 +894,10 @@ class ProjectIndex {
|
|
|
838
894
|
return graphBuildModule._resolveJavaPackageImport(this, importModule, javaFileIndex);
|
|
839
895
|
}
|
|
840
896
|
|
|
897
|
+
_resolveCSharpUsing(importModule) {
|
|
898
|
+
return graphBuildModule._resolveCSharpUsing(this, importModule);
|
|
899
|
+
}
|
|
900
|
+
|
|
841
901
|
/**
|
|
842
902
|
* Build import/export relationship graphs
|
|
843
903
|
*/
|
|
@@ -1048,9 +1108,11 @@ class ProjectIndex {
|
|
|
1048
1108
|
|
|
1049
1109
|
// Filter by file if specified
|
|
1050
1110
|
if (options.file) {
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
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));
|
|
1054
1116
|
if (filtered.length > 0) {
|
|
1055
1117
|
definitions = filtered;
|
|
1056
1118
|
}
|
|
@@ -1058,15 +1120,23 @@ class ProjectIndex {
|
|
|
1058
1120
|
|
|
1059
1121
|
// Filter by exact startLine when a handle was supplied. This pins
|
|
1060
1122
|
// the resolution to one specific definition — no ambiguity allowed.
|
|
1061
|
-
if (options.line &&
|
|
1062
|
-
const
|
|
1063
|
-
if (
|
|
1064
|
-
|
|
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);
|
|
1065
1130
|
}
|
|
1066
1131
|
}
|
|
1132
|
+
if (definitions.length === 0) {
|
|
1133
|
+
return { def: null, definitions: [], warnings: [] };
|
|
1134
|
+
}
|
|
1067
1135
|
|
|
1068
1136
|
// Score each definition for selection
|
|
1069
|
-
const typeOrder = new Set([
|
|
1137
|
+
const typeOrder = new Set([
|
|
1138
|
+
'class', 'struct', 'interface', 'type', 'impl', 'enum', 'record',
|
|
1139
|
+
]);
|
|
1070
1140
|
const { isTestPath } = require('./shared');
|
|
1071
1141
|
const scored = definitions.map(d => {
|
|
1072
1142
|
let score = 0;
|
|
@@ -1313,7 +1383,15 @@ class ProjectIndex {
|
|
|
1313
1383
|
}
|
|
1314
1384
|
|
|
1315
1385
|
const total = calls + definitions + imports;
|
|
1316
|
-
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
|
+
};
|
|
1317
1395
|
if (memoKey) this._opUsageTotalsCache.set(memoKey, result);
|
|
1318
1396
|
return result;
|
|
1319
1397
|
}
|
|
@@ -1422,7 +1500,9 @@ class ProjectIndex {
|
|
|
1422
1500
|
calls,
|
|
1423
1501
|
definitions,
|
|
1424
1502
|
imports,
|
|
1425
|
-
references
|
|
1503
|
+
references,
|
|
1504
|
+
complete: true,
|
|
1505
|
+
countKind: 'detailed-ast-usages',
|
|
1426
1506
|
};
|
|
1427
1507
|
}
|
|
1428
1508
|
|
|
@@ -1444,11 +1524,53 @@ class ProjectIndex {
|
|
|
1444
1524
|
* @param {string} typeName - The class/struct/interface name
|
|
1445
1525
|
* @returns {Array} Methods belonging to this type
|
|
1446
1526
|
*/
|
|
1447
|
-
findMethodsForType(typeName) {
|
|
1527
|
+
findMethodsForType(typeName, definition = null) {
|
|
1448
1528
|
const methods = [];
|
|
1449
1529
|
// Match both "TypeName" and "*TypeName" receivers (for Go/Rust pointer receivers)
|
|
1450
1530
|
const baseTypeName = typeName.replace(/^\*/, '');
|
|
1451
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
|
+
|
|
1452
1574
|
for (const [, symbols] of this.symbols) {
|
|
1453
1575
|
for (const symbol of symbols) {
|
|
1454
1576
|
// Skip non-method types (fields, properties, etc.)
|
|
@@ -1460,7 +1582,7 @@ class ProjectIndex {
|
|
|
1460
1582
|
// Also matches Rust associated functions (have receiver but isMethod=false)
|
|
1461
1583
|
if (symbol.receiver) {
|
|
1462
1584
|
const receiverBase = symbol.receiver.replace(/^\*/, '');
|
|
1463
|
-
if (receiverBase === baseTypeName) {
|
|
1585
|
+
if (receiverBase === baseTypeName && sameCompilerType(symbol)) {
|
|
1464
1586
|
methods.push(symbol);
|
|
1465
1587
|
continue;
|
|
1466
1588
|
}
|
|
@@ -1469,7 +1591,8 @@ class ProjectIndex {
|
|
|
1469
1591
|
// Check Python/Java/JS-style className (class members)
|
|
1470
1592
|
// Must be a method type, not just any symbol with className
|
|
1471
1593
|
if (symbol.className === baseTypeName &&
|
|
1472
|
-
(symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor')
|
|
1594
|
+
(symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor') &&
|
|
1595
|
+
sameCompilerType(symbol)) {
|
|
1473
1596
|
methods.push(symbol);
|
|
1474
1597
|
continue;
|
|
1475
1598
|
}
|
|
@@ -1506,7 +1629,7 @@ class ProjectIndex {
|
|
|
1506
1629
|
* @returns {boolean} true if ALL occurrences of name are inside strings
|
|
1507
1630
|
*/
|
|
1508
1631
|
isInsideStringAST(content, lineNum, line, name, filePath) {
|
|
1509
|
-
const language = detectLanguage(filePath);
|
|
1632
|
+
const language = detectLanguage(filePath, this.root);
|
|
1510
1633
|
if (!language) {
|
|
1511
1634
|
return false; // Unsupported language - assume not inside string
|
|
1512
1635
|
}
|
|
@@ -1627,6 +1750,18 @@ class ProjectIndex {
|
|
|
1627
1750
|
parts.push(def.modifiers.join(' '));
|
|
1628
1751
|
}
|
|
1629
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
|
+
}
|
|
1630
1765
|
if (def.params !== undefined) {
|
|
1631
1766
|
// Prefer typed rendering when paramTypes/paramsStructured carry annotations
|
|
1632
1767
|
const { renderTypedParams } = require('./output/shared');
|
|
@@ -1651,12 +1786,12 @@ class ProjectIndex {
|
|
|
1651
1786
|
* @returns {string} 'call', 'import', 'definition', or 'reference'
|
|
1652
1787
|
*/
|
|
1653
1788
|
classifyUsageAST(content, lineNum, name, filePath) {
|
|
1654
|
-
const language = detectLanguage(filePath);
|
|
1789
|
+
const language = detectLanguage(filePath, this.root);
|
|
1655
1790
|
if (!language) {
|
|
1656
1791
|
return null; // Signal to use fallback
|
|
1657
1792
|
}
|
|
1658
1793
|
|
|
1659
|
-
const langModule =
|
|
1794
|
+
const langModule = getLanguageAdapter(language);
|
|
1660
1795
|
if (!langModule || typeof langModule.findUsagesInCode !== 'function') {
|
|
1661
1796
|
return null;
|
|
1662
1797
|
}
|
|
@@ -1690,7 +1825,7 @@ class ProjectIndex {
|
|
|
1690
1825
|
* @returns {boolean}
|
|
1691
1826
|
*/
|
|
1692
1827
|
isCommentOrStringAtPosition(content, lineNum, column, filePath) {
|
|
1693
|
-
const language = detectLanguage(filePath);
|
|
1828
|
+
const language = detectLanguage(filePath, this.root);
|
|
1694
1829
|
if (!language) {
|
|
1695
1830
|
return false; // Can't determine, assume code
|
|
1696
1831
|
}
|
|
@@ -1854,6 +1989,62 @@ class ProjectIndex {
|
|
|
1854
1989
|
'FunctionalInterface', 'SafeVarargs',
|
|
1855
1990
|
'Iterable', 'Comparable', 'AutoCloseable', 'Cloneable',
|
|
1856
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',
|
|
1857
2048
|
])
|
|
1858
2049
|
};
|
|
1859
2050
|
// TypeScript/TSX share JavaScript keywords
|
|
@@ -2191,6 +2382,12 @@ class ProjectIndex {
|
|
|
2191
2382
|
/** Load index from cache file */
|
|
2192
2383
|
loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
|
|
2193
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
|
+
|
|
2194
2391
|
/** Load callsCache from separate file on demand (called by findCallers/findCallees) */
|
|
2195
2392
|
loadCallsCache() { return indexCache.loadCallsCache(this); }
|
|
2196
2393
|
|