ucn 3.1.8 → 3.3.0
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.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/.claude/skills/ucn/SKILL.md +113 -48
- package/README.md +159 -29
- package/cli/index.js +147 -137
- package/core/discovery.js +1 -2
- package/core/imports.js +157 -331
- package/core/output.js +129 -147
- package/core/project.js +484 -220
- package/languages/go.js +21 -10
- package/languages/java.js +25 -9
- package/languages/javascript.js +56 -37
- package/languages/python.js +39 -10
- package/languages/rust.js +36 -8
- package/package.json +1 -1
- package/test/parser.test.js +967 -7
- package/test/reliability-test-prompt.md +58 -0
package/core/project.js
CHANGED
|
@@ -11,12 +11,15 @@ const crypto = require('crypto');
|
|
|
11
11
|
const { expandGlob, findProjectRoot, detectProjectPattern, isTestFile } = require('./discovery');
|
|
12
12
|
const { extractImports, extractExports, resolveImport } = require('./imports');
|
|
13
13
|
const { parseFile } = require('./parser');
|
|
14
|
-
const { detectLanguage, getParser, getLanguageModule, PARSE_OPTIONS } = require('../languages');
|
|
14
|
+
const { detectLanguage, getParser, getLanguageModule, PARSE_OPTIONS, safeParse } = require('../languages');
|
|
15
15
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
16
16
|
|
|
17
17
|
// Read UCN version for cache invalidation
|
|
18
18
|
const UCN_VERSION = require('../package.json').version;
|
|
19
19
|
|
|
20
|
+
// Lazy-initialized per-language keyword sets (populated on first isKeyword call)
|
|
21
|
+
let LANGUAGE_KEYWORDS = null;
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* Escape special regex characters
|
|
22
25
|
*/
|
|
@@ -136,7 +139,7 @@ class ProjectIndex {
|
|
|
136
139
|
if (!language) return;
|
|
137
140
|
|
|
138
141
|
const parsed = parseFile(filePath);
|
|
139
|
-
const { imports } = extractImports(content, language);
|
|
142
|
+
const { imports, dynamicCount } = extractImports(content, language);
|
|
140
143
|
const { exports } = extractExports(content, language);
|
|
141
144
|
|
|
142
145
|
const fileEntry = {
|
|
@@ -149,8 +152,10 @@ class ProjectIndex {
|
|
|
149
152
|
size: stat.size,
|
|
150
153
|
imports: imports.map(i => i.module),
|
|
151
154
|
exports: exports.map(e => e.name),
|
|
152
|
-
symbols: []
|
|
155
|
+
symbols: [],
|
|
156
|
+
bindings: []
|
|
153
157
|
};
|
|
158
|
+
fileEntry.dynamicImports = dynamicCount || 0;
|
|
154
159
|
|
|
155
160
|
// Add symbols
|
|
156
161
|
const addSymbol = (item, type) => {
|
|
@@ -166,6 +171,7 @@ class ProjectIndex {
|
|
|
166
171
|
returnType: item.returnType,
|
|
167
172
|
modifiers: item.modifiers,
|
|
168
173
|
docstring: item.docstring,
|
|
174
|
+
bindingId: `${fileEntry.relativePath}:${type}:${item.startLine}`,
|
|
169
175
|
...(item.extends && { extends: item.extends }),
|
|
170
176
|
...(item.implements && { implements: item.implements }),
|
|
171
177
|
...(item.indent !== undefined && { indent: item.indent }),
|
|
@@ -176,6 +182,12 @@ class ProjectIndex {
|
|
|
176
182
|
...(item.memberType && { memberType: item.memberType })
|
|
177
183
|
};
|
|
178
184
|
fileEntry.symbols.push(symbol);
|
|
185
|
+
fileEntry.bindings.push({
|
|
186
|
+
id: symbol.bindingId,
|
|
187
|
+
name: symbol.name,
|
|
188
|
+
type: symbol.type,
|
|
189
|
+
startLine: symbol.startLine
|
|
190
|
+
});
|
|
179
191
|
|
|
180
192
|
if (!this.symbols.has(item.name)) {
|
|
181
193
|
this.symbols.set(item.name, []);
|
|
@@ -231,16 +243,41 @@ class ProjectIndex {
|
|
|
231
243
|
this.importGraph.clear();
|
|
232
244
|
this.exportGraph.clear();
|
|
233
245
|
|
|
246
|
+
// Build Java suffix lookup for package import resolution
|
|
247
|
+
// Maps "com/google/gson/Gson.java" -> absolute path
|
|
248
|
+
let javaSuffixMap = null;
|
|
249
|
+
|
|
234
250
|
for (const [filePath, fileEntry] of this.files) {
|
|
235
251
|
const importedFiles = [];
|
|
236
252
|
|
|
237
253
|
for (const importModule of fileEntry.imports) {
|
|
238
|
-
|
|
254
|
+
let resolved = resolveImport(importModule, filePath, {
|
|
239
255
|
aliases: this.config.aliases,
|
|
240
256
|
language: fileEntry.language,
|
|
241
257
|
root: this.root
|
|
242
258
|
});
|
|
243
259
|
|
|
260
|
+
// Java package imports: resolve by matching file path suffix
|
|
261
|
+
// e.g., "com.google.gson.Gson" -> find file ending in "com/google/gson/Gson.java"
|
|
262
|
+
if (!resolved && fileEntry.language === 'java' &&
|
|
263
|
+
!importModule.startsWith('.') && !importModule.endsWith('.*')) {
|
|
264
|
+
if (!javaSuffixMap) {
|
|
265
|
+
javaSuffixMap = new Map();
|
|
266
|
+
for (const [absPath, entry] of this.files) {
|
|
267
|
+
if (entry.language === 'java') {
|
|
268
|
+
javaSuffixMap.set(absPath, absPath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const fileSuffix = '/' + importModule.split('.').join('/') + '.java';
|
|
273
|
+
for (const absPath of javaSuffixMap.keys()) {
|
|
274
|
+
if (absPath.endsWith(fileSuffix)) {
|
|
275
|
+
resolved = absPath;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
244
281
|
if (resolved && this.files.has(resolved)) {
|
|
245
282
|
importedFiles.push(resolved);
|
|
246
283
|
|
|
@@ -284,6 +321,17 @@ class ProjectIndex {
|
|
|
284
321
|
}
|
|
285
322
|
}
|
|
286
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Count dynamic imports across indexed files
|
|
326
|
+
*/
|
|
327
|
+
getDynamicImportCount() {
|
|
328
|
+
let total = 0;
|
|
329
|
+
for (const fileEntry of this.files.values()) {
|
|
330
|
+
total += fileEntry.dynamicImports || 0;
|
|
331
|
+
}
|
|
332
|
+
return total;
|
|
333
|
+
}
|
|
334
|
+
|
|
287
335
|
// ========================================================================
|
|
288
336
|
// QUERY METHODS
|
|
289
337
|
// ========================================================================
|
|
@@ -364,6 +412,80 @@ class ProjectIndex {
|
|
|
364
412
|
* @param {object} options - { file, prefer, exact, exclude, in }
|
|
365
413
|
* @returns {Array} Matching symbols with usage counts
|
|
366
414
|
*/
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Resolve a symbol name to the best matching definition.
|
|
418
|
+
* Centralized selection logic used by all commands for consistency.
|
|
419
|
+
*
|
|
420
|
+
* Priority order:
|
|
421
|
+
* 1. Filter by --file if specified
|
|
422
|
+
* 2. Prefer class/struct/interface/type over functions/constructors
|
|
423
|
+
* 3. Prefer non-test file definitions over test files
|
|
424
|
+
* 4. Prefer higher usage count
|
|
425
|
+
*
|
|
426
|
+
* @param {string} name - Symbol name
|
|
427
|
+
* @param {object} [options] - { file }
|
|
428
|
+
* @returns {{ def: object|null, definitions: Array, warnings: Array }}
|
|
429
|
+
*/
|
|
430
|
+
resolveSymbol(name, options = {}) {
|
|
431
|
+
let definitions = this.symbols.get(name) || [];
|
|
432
|
+
if (definitions.length === 0) {
|
|
433
|
+
return { def: null, definitions: [], warnings: [] };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Filter by file if specified
|
|
437
|
+
if (options.file) {
|
|
438
|
+
const filtered = definitions.filter(d =>
|
|
439
|
+
d.relativePath && d.relativePath.includes(options.file)
|
|
440
|
+
);
|
|
441
|
+
if (filtered.length > 0) {
|
|
442
|
+
definitions = filtered;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Score each definition for selection
|
|
447
|
+
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
448
|
+
const scored = definitions.map(d => {
|
|
449
|
+
let score = 0;
|
|
450
|
+
const rp = d.relativePath || '';
|
|
451
|
+
// Prefer class/struct/interface types (+1000)
|
|
452
|
+
if (typeOrder.has(d.type)) score += 1000;
|
|
453
|
+
// Deprioritize test files (-500)
|
|
454
|
+
if (isTestFile(rp, detectLanguage(d.file))) {
|
|
455
|
+
score -= 500;
|
|
456
|
+
}
|
|
457
|
+
// Deprioritize examples/docs/vendor directories (-300)
|
|
458
|
+
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) {
|
|
459
|
+
score -= 300;
|
|
460
|
+
}
|
|
461
|
+
// Boost lib/src/core/internal directories (+200)
|
|
462
|
+
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) {
|
|
463
|
+
score += 200;
|
|
464
|
+
}
|
|
465
|
+
return { def: d, score };
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Sort by score descending, then by index order for stability
|
|
469
|
+
scored.sort((a, b) => b.score - a.score);
|
|
470
|
+
|
|
471
|
+
const def = scored[0].def;
|
|
472
|
+
|
|
473
|
+
// Build warnings
|
|
474
|
+
const warnings = [];
|
|
475
|
+
if (definitions.length > 1) {
|
|
476
|
+
warnings.push({
|
|
477
|
+
type: 'ambiguous',
|
|
478
|
+
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
|
|
479
|
+
alternatives: definitions.filter(d => d !== def).map(d => ({
|
|
480
|
+
file: d.relativePath,
|
|
481
|
+
line: d.startLine
|
|
482
|
+
}))
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { def, definitions, warnings };
|
|
487
|
+
}
|
|
488
|
+
|
|
367
489
|
find(name, options = {}) {
|
|
368
490
|
const matches = this.symbols.get(name) || [];
|
|
369
491
|
|
|
@@ -416,25 +538,6 @@ class ProjectIndex {
|
|
|
416
538
|
return withCounts;
|
|
417
539
|
}
|
|
418
540
|
|
|
419
|
-
/**
|
|
420
|
-
* Count usages of a symbol across the codebase
|
|
421
|
-
*/
|
|
422
|
-
countUsages(name) {
|
|
423
|
-
let count = 0;
|
|
424
|
-
const regex = new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g');
|
|
425
|
-
|
|
426
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
427
|
-
try {
|
|
428
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
429
|
-
const matches = content.match(regex);
|
|
430
|
-
if (matches) count += matches.length;
|
|
431
|
-
} catch (e) {
|
|
432
|
-
// Skip unreadable files
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return count;
|
|
437
|
-
}
|
|
438
541
|
|
|
439
542
|
/**
|
|
440
543
|
* Count usages of a specific symbol (not just by name)
|
|
@@ -709,30 +812,10 @@ class ProjectIndex {
|
|
|
709
812
|
* Get context for a symbol (callers + callees)
|
|
710
813
|
*/
|
|
711
814
|
context(name, options = {}) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// Filter by file if specified
|
|
718
|
-
if (options.file) {
|
|
719
|
-
const filtered = definitions.filter(d =>
|
|
720
|
-
d.relativePath && d.relativePath.includes(options.file)
|
|
721
|
-
);
|
|
722
|
-
if (filtered.length > 0) {
|
|
723
|
-
definitions = filtered;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Prefer class/struct/interface definitions over functions/methods/constructors
|
|
728
|
-
// This ensures context('ClassName') finds the class, not a constructor with same name
|
|
729
|
-
const typeOrder = ['class', 'struct', 'interface', 'type', 'impl'];
|
|
730
|
-
let def = definitions[0];
|
|
731
|
-
for (const d of definitions) {
|
|
732
|
-
if (typeOrder.includes(d.type)) {
|
|
733
|
-
def = d;
|
|
734
|
-
break;
|
|
735
|
-
}
|
|
815
|
+
const resolved = this.resolveSymbol(name, { file: options.file });
|
|
816
|
+
let { def, definitions, warnings } = resolved;
|
|
817
|
+
if (!def) {
|
|
818
|
+
return null;
|
|
736
819
|
}
|
|
737
820
|
|
|
738
821
|
// Special handling for class/struct/interface types
|
|
@@ -754,25 +837,28 @@ class ProjectIndex {
|
|
|
754
837
|
receiver: m.receiver
|
|
755
838
|
})),
|
|
756
839
|
// Also include places where the type is used in function parameters/returns
|
|
757
|
-
callers: this.findCallers(name, { includeMethods: options.includeMethods })
|
|
840
|
+
callers: this.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain })
|
|
758
841
|
};
|
|
759
842
|
|
|
760
|
-
if (
|
|
761
|
-
result.warnings =
|
|
762
|
-
type: 'ambiguous',
|
|
763
|
-
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
|
|
764
|
-
alternatives: definitions.slice(1).map(d => ({
|
|
765
|
-
file: d.relativePath,
|
|
766
|
-
line: d.startLine
|
|
767
|
-
}))
|
|
768
|
-
}];
|
|
843
|
+
if (warnings.length > 0) {
|
|
844
|
+
result.warnings = warnings;
|
|
769
845
|
}
|
|
770
846
|
|
|
771
847
|
return result;
|
|
772
848
|
}
|
|
773
849
|
|
|
774
|
-
const
|
|
775
|
-
const
|
|
850
|
+
const stats = { uncertain: 0 };
|
|
851
|
+
const callers = this.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
852
|
+
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
853
|
+
|
|
854
|
+
const filesInScope = new Set([def.file]);
|
|
855
|
+
callers.forEach(c => filesInScope.add(c.file));
|
|
856
|
+
callees.forEach(c => filesInScope.add(c.file));
|
|
857
|
+
let dynamicImports = 0;
|
|
858
|
+
for (const f of filesInScope) {
|
|
859
|
+
const fe = this.files.get(f);
|
|
860
|
+
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
861
|
+
}
|
|
776
862
|
|
|
777
863
|
const result = {
|
|
778
864
|
function: name,
|
|
@@ -782,19 +868,17 @@ class ProjectIndex {
|
|
|
782
868
|
params: def.params,
|
|
783
869
|
returnType: def.returnType,
|
|
784
870
|
callers,
|
|
785
|
-
callees
|
|
871
|
+
callees,
|
|
872
|
+
meta: {
|
|
873
|
+
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
874
|
+
skipped: 0,
|
|
875
|
+
dynamicImports,
|
|
876
|
+
uncertain: stats.uncertain
|
|
877
|
+
}
|
|
786
878
|
};
|
|
787
879
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
result.warnings = [{
|
|
791
|
-
type: 'ambiguous',
|
|
792
|
-
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
|
|
793
|
-
alternatives: definitions.slice(1).map(d => ({
|
|
794
|
-
file: d.relativePath,
|
|
795
|
-
line: d.startLine
|
|
796
|
-
}))
|
|
797
|
-
}];
|
|
880
|
+
if (warnings.length > 0) {
|
|
881
|
+
result.warnings = warnings;
|
|
798
882
|
}
|
|
799
883
|
|
|
800
884
|
return result;
|
|
@@ -875,6 +959,7 @@ class ProjectIndex {
|
|
|
875
959
|
*/
|
|
876
960
|
findCallers(name, options = {}) {
|
|
877
961
|
const callers = [];
|
|
962
|
+
const stats = options.stats;
|
|
878
963
|
|
|
879
964
|
// Get definition lines to exclude them
|
|
880
965
|
const definitions = this.symbols.get(name) || [];
|
|
@@ -895,6 +980,23 @@ class ProjectIndex {
|
|
|
895
980
|
// Skip if not matching our target name
|
|
896
981
|
if (call.name !== name) continue;
|
|
897
982
|
|
|
983
|
+
// Resolve binding within this file (without mutating cached call objects)
|
|
984
|
+
let bindingId = call.bindingId;
|
|
985
|
+
let isUncertain = call.uncertain;
|
|
986
|
+
if (!bindingId) {
|
|
987
|
+
const bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
|
|
988
|
+
if (bindings.length === 1) {
|
|
989
|
+
bindingId = bindings[0].id;
|
|
990
|
+
} else if (bindings.length !== 0) {
|
|
991
|
+
isUncertain = true;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (isUncertain && !options.includeUncertain) {
|
|
996
|
+
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
|
|
898
1000
|
// Smart method call handling
|
|
899
1001
|
if (call.isMethod) {
|
|
900
1002
|
// Always skip this/self/cls calls (internal state access, not function calls)
|
|
@@ -907,6 +1009,12 @@ class ProjectIndex {
|
|
|
907
1009
|
// Skip definition lines
|
|
908
1010
|
if (definitionLines.has(`${filePath}:${call.line}`)) continue;
|
|
909
1011
|
|
|
1012
|
+
// If we have a binding id on definition, require match when available
|
|
1013
|
+
const targetBindingIds = new Set(definitions.map(d => d.bindingId).filter(Boolean));
|
|
1014
|
+
if (targetBindingIds.size > 0 && bindingId && !targetBindingIds.has(bindingId)) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
910
1018
|
// Find the enclosing function (get full symbol info)
|
|
911
1019
|
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
912
1020
|
|
|
@@ -991,7 +1099,7 @@ class ProjectIndex {
|
|
|
991
1099
|
const fileEntry = this.files.get(def.file);
|
|
992
1100
|
const language = fileEntry?.language;
|
|
993
1101
|
|
|
994
|
-
const callees = new Map(); //
|
|
1102
|
+
const callees = new Map(); // key -> { name, bindingId, count }
|
|
995
1103
|
|
|
996
1104
|
for (const call of calls) {
|
|
997
1105
|
// Filter to calls within this function's scope using enclosingFunction
|
|
@@ -1007,9 +1115,58 @@ class ProjectIndex {
|
|
|
1007
1115
|
}
|
|
1008
1116
|
|
|
1009
1117
|
// Skip keywords and built-ins
|
|
1010
|
-
if (this.isKeyword(call.name)) continue;
|
|
1118
|
+
if (this.isKeyword(call.name, language)) continue;
|
|
1119
|
+
|
|
1120
|
+
// Resolve binding within this file (without mutating cached call objects)
|
|
1121
|
+
let calleeKey = call.bindingId || call.name;
|
|
1122
|
+
let bindingResolved = call.bindingId;
|
|
1123
|
+
let isUncertain = call.uncertain;
|
|
1124
|
+
if (!call.bindingId && fileEntry?.bindings) {
|
|
1125
|
+
const bindings = fileEntry.bindings.filter(b => b.name === call.name);
|
|
1126
|
+
if (bindings.length === 1) {
|
|
1127
|
+
bindingResolved = bindings[0].id;
|
|
1128
|
+
calleeKey = bindingResolved;
|
|
1129
|
+
} else if (bindings.length > 1) {
|
|
1130
|
+
if (call.name === def.name) {
|
|
1131
|
+
// Calling same-name function (e.g., Java overloads)
|
|
1132
|
+
// Add ALL other overloads as potential callees
|
|
1133
|
+
const otherBindings = bindings.filter(b =>
|
|
1134
|
+
b.startLine !== def.startLine
|
|
1135
|
+
);
|
|
1136
|
+
for (const ob of otherBindings) {
|
|
1137
|
+
const existing = callees.get(ob.id);
|
|
1138
|
+
if (existing) {
|
|
1139
|
+
existing.count += 1;
|
|
1140
|
+
} else {
|
|
1141
|
+
callees.set(ob.id, {
|
|
1142
|
+
name: call.name,
|
|
1143
|
+
bindingId: ob.id,
|
|
1144
|
+
count: 1
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
continue; // Already added all overloads, skip normal add
|
|
1149
|
+
} else {
|
|
1150
|
+
isUncertain = true;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (isUncertain && !options.includeUncertain) {
|
|
1156
|
+
if (options.stats) options.stats.uncertain = (options.stats.uncertain || 0) + 1;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1011
1159
|
|
|
1012
|
-
|
|
1160
|
+
const existing = callees.get(calleeKey);
|
|
1161
|
+
if (existing) {
|
|
1162
|
+
existing.count += 1;
|
|
1163
|
+
} else {
|
|
1164
|
+
callees.set(calleeKey, {
|
|
1165
|
+
name: call.name,
|
|
1166
|
+
bindingId: bindingResolved,
|
|
1167
|
+
count: 1
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1013
1170
|
}
|
|
1014
1171
|
|
|
1015
1172
|
// Look up each callee in the symbol table
|
|
@@ -1018,16 +1175,24 @@ class ProjectIndex {
|
|
|
1018
1175
|
const defDir = path.dirname(def.file);
|
|
1019
1176
|
const defReceiver = def.receiver;
|
|
1020
1177
|
|
|
1021
|
-
for (const
|
|
1178
|
+
for (const { name: calleeName, bindingId, count } of callees.values()) {
|
|
1022
1179
|
const symbols = this.symbols.get(calleeName);
|
|
1023
1180
|
if (symbols && symbols.length > 0) {
|
|
1024
1181
|
let callee = symbols[0];
|
|
1025
1182
|
|
|
1026
|
-
// If
|
|
1027
|
-
if (symbols.length > 1) {
|
|
1028
|
-
|
|
1183
|
+
// If we have a binding ID, find the exact matching symbol
|
|
1184
|
+
if (bindingId && symbols.length > 1) {
|
|
1185
|
+
const exactMatch = symbols.find(s => s.bindingId === bindingId);
|
|
1186
|
+
if (exactMatch) {
|
|
1187
|
+
callee = exactMatch;
|
|
1188
|
+
}
|
|
1189
|
+
} else if (symbols.length > 1) {
|
|
1190
|
+
// Priority 1: Same file, but different definition (for overloads)
|
|
1191
|
+
const sameFileDifferent = symbols.find(s => s.file === def.file && s.startLine !== def.startLine);
|
|
1029
1192
|
const sameFile = symbols.find(s => s.file === def.file);
|
|
1030
|
-
if (
|
|
1193
|
+
if (sameFileDifferent && calleeName === def.name) {
|
|
1194
|
+
callee = sameFileDifferent;
|
|
1195
|
+
} else if (sameFile) {
|
|
1031
1196
|
callee = sameFile;
|
|
1032
1197
|
} else {
|
|
1033
1198
|
// Priority 2: Same directory (package)
|
|
@@ -1075,18 +1240,27 @@ class ProjectIndex {
|
|
|
1075
1240
|
* Smart extraction: function + dependencies
|
|
1076
1241
|
*/
|
|
1077
1242
|
smart(name, options = {}) {
|
|
1078
|
-
const
|
|
1079
|
-
if (
|
|
1243
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
1244
|
+
if (!def) {
|
|
1080
1245
|
return null;
|
|
1081
1246
|
}
|
|
1082
|
-
|
|
1083
|
-
const def = definitions[0];
|
|
1084
1247
|
const code = this.extractCode(def);
|
|
1085
|
-
const
|
|
1248
|
+
const stats = { uncertain: 0 };
|
|
1249
|
+
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
1086
1250
|
|
|
1087
|
-
|
|
1251
|
+
const filesInScope = new Set([def.file]);
|
|
1252
|
+
callees.forEach(c => filesInScope.add(c.file));
|
|
1253
|
+
let dynamicImports = 0;
|
|
1254
|
+
for (const f of filesInScope) {
|
|
1255
|
+
const fe = this.files.get(f);
|
|
1256
|
+
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Extract code for each dependency, excluding the exact same function
|
|
1260
|
+
// (but keeping same-name overloads, e.g. Java toJson(Object) vs toJson(Object, Class))
|
|
1261
|
+
const defBindingId = def.bindingId;
|
|
1088
1262
|
const dependencies = callees
|
|
1089
|
-
.filter(callee => callee.
|
|
1263
|
+
.filter(callee => callee.bindingId !== defBindingId)
|
|
1090
1264
|
.map(callee => ({
|
|
1091
1265
|
...callee,
|
|
1092
1266
|
code: this.extractCode(callee)
|
|
@@ -1118,7 +1292,13 @@ class ProjectIndex {
|
|
|
1118
1292
|
code
|
|
1119
1293
|
},
|
|
1120
1294
|
dependencies,
|
|
1121
|
-
types
|
|
1295
|
+
types,
|
|
1296
|
+
meta: {
|
|
1297
|
+
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
1298
|
+
skipped: 0,
|
|
1299
|
+
dynamicImports,
|
|
1300
|
+
uncertain: stats.uncertain
|
|
1301
|
+
}
|
|
1122
1302
|
};
|
|
1123
1303
|
}
|
|
1124
1304
|
|
|
@@ -1240,20 +1420,53 @@ class ProjectIndex {
|
|
|
1240
1420
|
/**
|
|
1241
1421
|
* Check if a name is a language keyword
|
|
1242
1422
|
*/
|
|
1243
|
-
isKeyword(name) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1423
|
+
isKeyword(name, language) {
|
|
1424
|
+
if (!LANGUAGE_KEYWORDS) {
|
|
1425
|
+
// Initialize on first use
|
|
1426
|
+
LANGUAGE_KEYWORDS = {
|
|
1427
|
+
javascript: new Set([
|
|
1428
|
+
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
|
|
1429
|
+
'continue', 'return', 'function', 'class', 'const', 'let', 'var',
|
|
1430
|
+
'new', 'this', 'super', 'import', 'export', 'default', 'from',
|
|
1431
|
+
'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
|
|
1432
|
+
'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'with'
|
|
1433
|
+
]),
|
|
1434
|
+
python: new Set([
|
|
1435
|
+
'if', 'else', 'elif', 'for', 'while', 'def', 'class', 'return',
|
|
1436
|
+
'import', 'from', 'try', 'except', 'finally', 'raise', 'async',
|
|
1437
|
+
'await', 'yield', 'with', 'as', 'lambda', 'pass', 'break',
|
|
1438
|
+
'continue', 'del', 'global', 'nonlocal', 'assert', 'is', 'not',
|
|
1439
|
+
'and', 'or', 'in', 'True', 'False', 'None', 'self', 'cls'
|
|
1440
|
+
]),
|
|
1441
|
+
go: new Set([
|
|
1442
|
+
'if', 'else', 'for', 'switch', 'case', 'break', 'continue',
|
|
1443
|
+
'return', 'func', 'type', 'struct', 'interface', 'package',
|
|
1444
|
+
'import', 'go', 'defer', 'select', 'chan', 'map', 'range',
|
|
1445
|
+
'fallthrough', 'goto', 'var', 'const', 'default'
|
|
1446
|
+
]),
|
|
1447
|
+
rust: new Set([
|
|
1448
|
+
'if', 'else', 'for', 'while', 'loop', 'fn', 'impl', 'pub',
|
|
1449
|
+
'mod', 'use', 'crate', 'self', 'super', 'match', 'unsafe',
|
|
1450
|
+
'move', 'ref', 'mut', 'where', 'let', 'const', 'struct',
|
|
1451
|
+
'enum', 'trait', 'async', 'await', 'return', 'break',
|
|
1452
|
+
'continue', 'type', 'as', 'in', 'dyn', 'static'
|
|
1453
|
+
]),
|
|
1454
|
+
java: new Set([
|
|
1455
|
+
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
|
|
1456
|
+
'continue', 'return', 'class', 'interface', 'enum', 'extends',
|
|
1457
|
+
'implements', 'new', 'this', 'super', 'import', 'package',
|
|
1458
|
+
'try', 'catch', 'finally', 'throw', 'throws', 'abstract',
|
|
1459
|
+
'static', 'final', 'synchronized', 'volatile', 'transient',
|
|
1460
|
+
'native', 'void', 'instanceof', 'default'
|
|
1461
|
+
])
|
|
1462
|
+
};
|
|
1463
|
+
// TypeScript/TSX share JavaScript keywords
|
|
1464
|
+
LANGUAGE_KEYWORDS.typescript = LANGUAGE_KEYWORDS.javascript;
|
|
1465
|
+
LANGUAGE_KEYWORDS.tsx = LANGUAGE_KEYWORDS.javascript;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const keywords = LANGUAGE_KEYWORDS[language];
|
|
1469
|
+
return keywords ? keywords.has(name) : false;
|
|
1257
1470
|
}
|
|
1258
1471
|
|
|
1259
1472
|
/**
|
|
@@ -2009,13 +2222,11 @@ class ProjectIndex {
|
|
|
2009
2222
|
* @param {string} name - Function name
|
|
2010
2223
|
* @returns {object} Related functions grouped by relationship type
|
|
2011
2224
|
*/
|
|
2012
|
-
related(name) {
|
|
2013
|
-
const
|
|
2014
|
-
if (!
|
|
2225
|
+
related(name, options = {}) {
|
|
2226
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2227
|
+
if (!def) {
|
|
2015
2228
|
return null;
|
|
2016
2229
|
}
|
|
2017
|
-
|
|
2018
|
-
const def = definitions[0];
|
|
2019
2230
|
const related = {
|
|
2020
2231
|
target: {
|
|
2021
2232
|
name: def.name,
|
|
@@ -2149,22 +2360,10 @@ class ProjectIndex {
|
|
|
2149
2360
|
const maxDepth = Math.max(0, rawDepth);
|
|
2150
2361
|
const direction = options.direction || 'down'; // 'down' = callees, 'up' = callers, 'both'
|
|
2151
2362
|
|
|
2152
|
-
|
|
2153
|
-
if (!
|
|
2363
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2364
|
+
if (!def) {
|
|
2154
2365
|
return null;
|
|
2155
2366
|
}
|
|
2156
|
-
|
|
2157
|
-
// Filter by file if specified
|
|
2158
|
-
if (options.file) {
|
|
2159
|
-
const filtered = definitions.filter(d =>
|
|
2160
|
-
d.relativePath && d.relativePath.includes(options.file)
|
|
2161
|
-
);
|
|
2162
|
-
if (filtered.length > 0) {
|
|
2163
|
-
definitions = filtered;
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
const def = definitions[0];
|
|
2168
2367
|
const visited = new Set();
|
|
2169
2368
|
const defDir = path.dirname(def.file);
|
|
2170
2369
|
|
|
@@ -2234,22 +2433,10 @@ class ProjectIndex {
|
|
|
2234
2433
|
* @returns {object} Impact analysis
|
|
2235
2434
|
*/
|
|
2236
2435
|
impact(name, options = {}) {
|
|
2237
|
-
|
|
2238
|
-
if (!
|
|
2436
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2437
|
+
if (!def) {
|
|
2239
2438
|
return null;
|
|
2240
2439
|
}
|
|
2241
|
-
|
|
2242
|
-
// Filter by file if specified
|
|
2243
|
-
if (options.file) {
|
|
2244
|
-
const filtered = definitions.filter(d =>
|
|
2245
|
-
d.relativePath && d.relativePath.includes(options.file)
|
|
2246
|
-
);
|
|
2247
|
-
if (filtered.length > 0) {
|
|
2248
|
-
definitions = filtered;
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
const def = definitions[0];
|
|
2253
2440
|
const usages = this.usages(name, { codeOnly: true });
|
|
2254
2441
|
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2255
2442
|
|
|
@@ -2264,6 +2451,7 @@ class ProjectIndex {
|
|
|
2264
2451
|
...analysis
|
|
2265
2452
|
};
|
|
2266
2453
|
});
|
|
2454
|
+
this._clearTreeCache();
|
|
2267
2455
|
|
|
2268
2456
|
// Group by file if requested
|
|
2269
2457
|
const byFile = new Map();
|
|
@@ -2729,13 +2917,11 @@ class ProjectIndex {
|
|
|
2729
2917
|
* @param {string} name - Function name
|
|
2730
2918
|
* @returns {object} Verification results with mismatches
|
|
2731
2919
|
*/
|
|
2732
|
-
verify(name) {
|
|
2733
|
-
const
|
|
2734
|
-
if (!
|
|
2920
|
+
verify(name, options = {}) {
|
|
2921
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
2922
|
+
if (!def) {
|
|
2735
2923
|
return { found: false, function: name };
|
|
2736
2924
|
}
|
|
2737
|
-
|
|
2738
|
-
const def = definitions[0];
|
|
2739
2925
|
const expectedParamCount = def.paramsStructured?.length || 0;
|
|
2740
2926
|
const optionalCount = (def.paramsStructured || []).filter(p => p.optional || p.default !== undefined).length;
|
|
2741
2927
|
const minArgs = expectedParamCount - optionalCount;
|
|
@@ -2749,9 +2935,18 @@ class ProjectIndex {
|
|
|
2749
2935
|
const mismatches = [];
|
|
2750
2936
|
const uncertain = [];
|
|
2751
2937
|
|
|
2938
|
+
// If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
|
|
2939
|
+
// This prevents false positives where a standalone function name matches method calls
|
|
2940
|
+
const defIsMethod = def.isMethod || def.type === 'method' || def.className;
|
|
2941
|
+
|
|
2752
2942
|
for (const call of calls) {
|
|
2753
2943
|
const analysis = this.analyzeCallSite(call, name);
|
|
2754
2944
|
|
|
2945
|
+
// Skip method calls when verifying a non-method definition
|
|
2946
|
+
if (analysis.isMethodCall && !defIsMethod) {
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2755
2950
|
if (analysis.args === null) {
|
|
2756
2951
|
// Couldn't parse arguments
|
|
2757
2952
|
uncertain.push({
|
|
@@ -2809,6 +3004,7 @@ class ProjectIndex {
|
|
|
2809
3004
|
}
|
|
2810
3005
|
}
|
|
2811
3006
|
}
|
|
3007
|
+
this._clearTreeCache();
|
|
2812
3008
|
|
|
2813
3009
|
return {
|
|
2814
3010
|
found: true,
|
|
@@ -2822,7 +3018,7 @@ class ProjectIndex {
|
|
|
2822
3018
|
hasDefault: p.default !== undefined
|
|
2823
3019
|
})) || [],
|
|
2824
3020
|
expectedArgs: { min: minArgs, max: hasRest ? '∞' : expectedParamCount },
|
|
2825
|
-
totalCalls:
|
|
3021
|
+
totalCalls: valid.length + mismatches.length + uncertain.length,
|
|
2826
3022
|
valid: valid.length,
|
|
2827
3023
|
mismatches: mismatches.length,
|
|
2828
3024
|
uncertain: uncertain.length,
|
|
@@ -2832,87 +3028,105 @@ class ProjectIndex {
|
|
|
2832
3028
|
}
|
|
2833
3029
|
|
|
2834
3030
|
/**
|
|
2835
|
-
* Analyze a call site to understand how it's being called
|
|
3031
|
+
* Analyze a call site to understand how it's being called (AST-based)
|
|
3032
|
+
* @param {object} call - Usage object with file, line, content
|
|
3033
|
+
* @param {string} funcName - Function name to find
|
|
3034
|
+
* @returns {object} { args, argCount, hasSpread, hasVariable }
|
|
2836
3035
|
*/
|
|
2837
3036
|
analyzeCallSite(call, funcName) {
|
|
2838
|
-
|
|
3037
|
+
try {
|
|
3038
|
+
const language = detectLanguage(call.file);
|
|
3039
|
+
if (!language) return { args: null, argCount: 0 };
|
|
2839
3040
|
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3041
|
+
const parser = getParser(language);
|
|
3042
|
+
if (!parser) return { args: null, argCount: 0 };
|
|
3043
|
+
|
|
3044
|
+
// Use tree cache to avoid re-parsing the same file in batch operations
|
|
3045
|
+
let tree = this._treeCache?.get(call.file);
|
|
3046
|
+
if (!tree) {
|
|
3047
|
+
const content = fs.readFileSync(call.file, 'utf-8');
|
|
3048
|
+
tree = safeParse(parser, content);
|
|
3049
|
+
if (!tree) return { args: null, argCount: 0 };
|
|
3050
|
+
if (!this._treeCache) this._treeCache = new Map();
|
|
3051
|
+
this._treeCache.set(call.file, tree);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// Call node types vary by language
|
|
3055
|
+
const callTypes = new Set(['call_expression', 'call', 'method_invocation']);
|
|
3056
|
+
const targetRow = call.line - 1; // tree-sitter is 0-indexed
|
|
3057
|
+
|
|
3058
|
+
// Find the call expression at the target line matching funcName
|
|
3059
|
+
const callNode = this._findCallNode(tree.rootNode, callTypes, targetRow, funcName);
|
|
3060
|
+
if (!callNode) return { args: null, argCount: 0 };
|
|
3061
|
+
|
|
3062
|
+
// Check if this is a method call (obj.func()) vs a direct call (func())
|
|
3063
|
+
const funcNode = callNode.childForFieldName('function') ||
|
|
3064
|
+
callNode.childForFieldName('name');
|
|
3065
|
+
let isMethodCall = false;
|
|
3066
|
+
if (funcNode) {
|
|
3067
|
+
// member_expression (JS), attribute (Python), selector_expression (Go), field_expression (Rust)
|
|
3068
|
+
if (['member_expression', 'attribute', 'selector_expression', 'field_expression'].includes(funcNode.type)) {
|
|
3069
|
+
isMethodCall = true;
|
|
3070
|
+
}
|
|
3071
|
+
// Java method_invocation with object
|
|
3072
|
+
if (callNode.type === 'method_invocation' && callNode.childForFieldName('object')) {
|
|
3073
|
+
isMethodCall = true;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
2845
3076
|
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
return { args: [], argCount: 0 };
|
|
2849
|
-
}
|
|
3077
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
3078
|
+
if (!argsNode) return { args: [], argCount: 0, isMethodCall };
|
|
2850
3079
|
|
|
2851
|
-
|
|
2852
|
-
|
|
3080
|
+
const args = [];
|
|
3081
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
3082
|
+
args.push(argsNode.namedChild(i).text.trim());
|
|
3083
|
+
}
|
|
2853
3084
|
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3085
|
+
return {
|
|
3086
|
+
args,
|
|
3087
|
+
argCount: args.length,
|
|
3088
|
+
hasSpread: args.some(a => a.startsWith('...')),
|
|
3089
|
+
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
|
|
3090
|
+
isMethodCall
|
|
3091
|
+
};
|
|
3092
|
+
} catch (e) {
|
|
3093
|
+
return { args: null, argCount: 0 };
|
|
3094
|
+
}
|
|
2860
3095
|
}
|
|
2861
3096
|
|
|
2862
3097
|
/**
|
|
2863
|
-
*
|
|
3098
|
+
* Find a call expression node at the target line matching funcName
|
|
2864
3099
|
*/
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
let inString = false;
|
|
2870
|
-
let stringChar = '';
|
|
2871
|
-
|
|
2872
|
-
for (let i = 0; i < argsStr.length; i++) {
|
|
2873
|
-
const ch = argsStr[i];
|
|
2874
|
-
|
|
2875
|
-
if (inString) {
|
|
2876
|
-
current += ch;
|
|
2877
|
-
if (ch === stringChar && argsStr[i - 1] !== '\\') {
|
|
2878
|
-
inString = false;
|
|
2879
|
-
}
|
|
2880
|
-
continue;
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2884
|
-
inString = true;
|
|
2885
|
-
stringChar = ch;
|
|
2886
|
-
current += ch;
|
|
2887
|
-
continue;
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
if (ch === '(' || ch === '[' || ch === '{') {
|
|
2891
|
-
depth++;
|
|
2892
|
-
current += ch;
|
|
2893
|
-
continue;
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
if (ch === ')' || ch === ']' || ch === '}') {
|
|
2897
|
-
depth--;
|
|
2898
|
-
current += ch;
|
|
2899
|
-
continue;
|
|
2900
|
-
}
|
|
3100
|
+
_findCallNode(node, callTypes, targetRow, funcName) {
|
|
3101
|
+
if (node.startPosition.row > targetRow || node.endPosition.row < targetRow) {
|
|
3102
|
+
return null; // Skip nodes that don't contain the target line
|
|
3103
|
+
}
|
|
2901
3104
|
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3105
|
+
if (callTypes.has(node.type) && node.startPosition.row === targetRow) {
|
|
3106
|
+
// Check if this call is for our target function
|
|
3107
|
+
const funcNode = node.childForFieldName('function') ||
|
|
3108
|
+
node.childForFieldName('name'); // Java method_invocation uses 'name'
|
|
3109
|
+
if (funcNode) {
|
|
3110
|
+
const funcText = funcNode.type === 'member_expression' || funcNode.type === 'selector_expression' || funcNode.type === 'field_expression' || funcNode.type === 'attribute'
|
|
3111
|
+
? (funcNode.childForFieldName('property') || funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
3112
|
+
: funcNode.text;
|
|
3113
|
+
if (funcText === funcName) return node;
|
|
2906
3114
|
}
|
|
2907
|
-
|
|
2908
|
-
current += ch;
|
|
2909
3115
|
}
|
|
2910
3116
|
|
|
2911
|
-
|
|
2912
|
-
|
|
3117
|
+
// Recurse into children
|
|
3118
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3119
|
+
const result = this._findCallNode(node.child(i), callTypes, targetRow, funcName);
|
|
3120
|
+
if (result) return result;
|
|
2913
3121
|
}
|
|
3122
|
+
return null;
|
|
3123
|
+
}
|
|
2914
3124
|
|
|
2915
|
-
|
|
3125
|
+
/**
|
|
3126
|
+
* Clear the AST tree cache (call after batch operations)
|
|
3127
|
+
*/
|
|
3128
|
+
_clearTreeCache() {
|
|
3129
|
+
this._treeCache = null;
|
|
2916
3130
|
}
|
|
2917
3131
|
|
|
2918
3132
|
/**
|
|
@@ -2979,9 +3193,12 @@ class ProjectIndex {
|
|
|
2979
3193
|
};
|
|
2980
3194
|
}
|
|
2981
3195
|
|
|
2982
|
-
// Use
|
|
2983
|
-
const
|
|
2984
|
-
const
|
|
3196
|
+
// Use resolveSymbol for consistent primary selection (prefers non-test files)
|
|
3197
|
+
const { def: resolved } = this.resolveSymbol(name, { file: options.file });
|
|
3198
|
+
const primary = resolved || definitions[0];
|
|
3199
|
+
const others = definitions.filter(d =>
|
|
3200
|
+
d.relativePath !== primary.relativePath || d.startLine !== primary.startLine
|
|
3201
|
+
);
|
|
2985
3202
|
|
|
2986
3203
|
// Use the actual symbol name (may differ from query if fuzzy matched)
|
|
2987
3204
|
const symbolName = primary.name;
|
|
@@ -3247,42 +3464,89 @@ class ProjectIndex {
|
|
|
3247
3464
|
/**
|
|
3248
3465
|
* Get TOC for all files
|
|
3249
3466
|
*/
|
|
3250
|
-
getToc() {
|
|
3467
|
+
getToc(options = {}) {
|
|
3251
3468
|
const files = [];
|
|
3252
3469
|
let totalFunctions = 0;
|
|
3253
3470
|
let totalClasses = 0;
|
|
3254
3471
|
let totalState = 0;
|
|
3255
3472
|
let totalLines = 0;
|
|
3473
|
+
let totalDynamic = 0;
|
|
3474
|
+
let totalTests = 0;
|
|
3256
3475
|
|
|
3257
3476
|
for (const [filePath, fileEntry] of this.files) {
|
|
3258
|
-
|
|
3477
|
+
let functions = fileEntry.symbols.filter(s => s.type === 'function');
|
|
3259
3478
|
const classes = fileEntry.symbols.filter(s =>
|
|
3260
3479
|
['class', 'interface', 'type', 'enum', 'struct', 'trait', 'impl'].includes(s.type)
|
|
3261
3480
|
);
|
|
3262
3481
|
const state = fileEntry.symbols.filter(s => s.type === 'state');
|
|
3263
3482
|
|
|
3483
|
+
if (options.topLevel) {
|
|
3484
|
+
functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3264
3487
|
totalFunctions += functions.length;
|
|
3265
3488
|
totalClasses += classes.length;
|
|
3266
3489
|
totalState += state.length;
|
|
3267
3490
|
totalLines += fileEntry.lines;
|
|
3491
|
+
totalDynamic += fileEntry.dynamicImports || 0;
|
|
3492
|
+
if (isTestFile(filePath)) totalTests += 1;
|
|
3268
3493
|
|
|
3269
|
-
|
|
3494
|
+
const entry = {
|
|
3270
3495
|
file: fileEntry.relativePath,
|
|
3271
3496
|
language: fileEntry.language,
|
|
3272
3497
|
lines: fileEntry.lines,
|
|
3273
|
-
functions,
|
|
3274
|
-
classes,
|
|
3275
|
-
state
|
|
3276
|
-
}
|
|
3498
|
+
functions: functions.length,
|
|
3499
|
+
classes: classes.length,
|
|
3500
|
+
state: state.length
|
|
3501
|
+
};
|
|
3502
|
+
|
|
3503
|
+
if (options.detailed) {
|
|
3504
|
+
entry.symbols = { functions, classes, state };
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
files.push(entry);
|
|
3277
3508
|
}
|
|
3278
3509
|
|
|
3510
|
+
// Hints: top files by function count and lines
|
|
3511
|
+
const topFunctionFiles = [...files]
|
|
3512
|
+
.sort((a, b) => b.functions - a.functions || b.lines - a.lines)
|
|
3513
|
+
.filter(f => f.functions > 0)
|
|
3514
|
+
.slice(0, 3)
|
|
3515
|
+
.map(f => ({ file: f.file, functions: f.functions }));
|
|
3516
|
+
|
|
3517
|
+
const topLineFiles = [...files]
|
|
3518
|
+
.sort((a, b) => b.lines - a.lines)
|
|
3519
|
+
.slice(0, 3)
|
|
3520
|
+
.map(f => ({ file: f.file, lines: f.lines }));
|
|
3521
|
+
|
|
3522
|
+
// Entry point candidates
|
|
3523
|
+
const entryPattern = /(main|index|server|app)\.(js|jsx|ts|tsx|py|go|rs|java)$/i;
|
|
3524
|
+
const entryFiles = files
|
|
3525
|
+
.filter(f => entryPattern.test(f.file))
|
|
3526
|
+
.slice(0, 5)
|
|
3527
|
+
.map(f => f.file);
|
|
3528
|
+
|
|
3279
3529
|
return {
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3530
|
+
meta: {
|
|
3531
|
+
complete: totalDynamic === 0,
|
|
3532
|
+
skipped: 0,
|
|
3533
|
+
dynamicImports: totalDynamic,
|
|
3534
|
+
uncertain: 0
|
|
3535
|
+
},
|
|
3536
|
+
totals: {
|
|
3537
|
+
files: files.length,
|
|
3538
|
+
lines: totalLines,
|
|
3539
|
+
functions: totalFunctions,
|
|
3540
|
+
classes: totalClasses,
|
|
3541
|
+
state: totalState,
|
|
3542
|
+
testFiles: totalTests
|
|
3543
|
+
},
|
|
3544
|
+
summary: {
|
|
3545
|
+
topFunctionFiles,
|
|
3546
|
+
topLineFiles,
|
|
3547
|
+
entryFiles
|
|
3548
|
+
},
|
|
3549
|
+
files
|
|
3286
3550
|
};
|
|
3287
3551
|
}
|
|
3288
3552
|
|