ucn 3.7.24 → 3.7.26
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/README.md +192 -463
- package/cli/index.js +285 -1054
- package/core/cache.js +193 -0
- package/core/callers.js +817 -0
- package/core/deadcode.js +320 -0
- package/core/discovery.js +1 -1
- package/core/execute.js +207 -10
- package/core/expand-cache.js +16 -5
- package/core/imports.js +21 -15
- package/core/output.js +370 -35
- package/core/project.js +365 -2272
- package/core/shared.js +11 -1
- package/core/stacktrace.js +313 -0
- package/core/verify.js +533 -0
- package/languages/go.js +57 -21
- package/languages/html.js +14 -3
- package/languages/java.js +4 -2
- package/languages/javascript.js +36 -9
- package/languages/rust.js +49 -17
- package/mcp/server.js +39 -172
- package/package.json +1 -1
package/core/project.js
CHANGED
|
@@ -14,23 +14,16 @@ const { extractImports, extractExports, resolveImport } = require('./imports');
|
|
|
14
14
|
const { parse, parseFile, cleanHtmlScriptTags } = require('./parser');
|
|
15
15
|
const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
|
|
16
16
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
17
|
+
const { escapeRegExp, NON_CALLABLE_TYPES } = require('./shared');
|
|
18
|
+
const stacktrace = require('./stacktrace');
|
|
19
|
+
const indexCache = require('./cache');
|
|
20
|
+
const deadcodeModule = require('./deadcode');
|
|
21
|
+
const verifyModule = require('./verify');
|
|
22
|
+
const callersModule = require('./callers');
|
|
20
23
|
|
|
21
24
|
// Lazy-initialized per-language keyword sets (populated on first isKeyword call)
|
|
22
25
|
let LANGUAGE_KEYWORDS = null;
|
|
23
26
|
|
|
24
|
-
// Symbol types that are not callable (used to filter class/struct/type declarations from call analysis)
|
|
25
|
-
const NON_CALLABLE_TYPES = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'state', 'impl']);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Escape special regex characters
|
|
29
|
-
*/
|
|
30
|
-
function escapeRegExp(text) {
|
|
31
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
/**
|
|
35
28
|
* ProjectIndex - Manages symbol table for a project
|
|
36
29
|
*/
|
|
@@ -50,8 +43,10 @@ class ProjectIndex {
|
|
|
50
43
|
this.config = this.loadConfig();
|
|
51
44
|
this.buildTime = null;
|
|
52
45
|
this.callsCache = new Map(); // filePath -> { mtime, hash, calls, content }
|
|
46
|
+
this.callsCacheDirty = false; // set by getCachedCalls when entries are added or mutated
|
|
53
47
|
this.failedFiles = new Set(); // files that failed to index (e.g. large minified bundles)
|
|
54
48
|
this._opContentCache = null; // per-operation file content cache (Map<filePath, string>)
|
|
49
|
+
this._opUsagesCache = null; // per-operation findUsagesInCode cache (Map<"file:name", usages[]>)
|
|
55
50
|
}
|
|
56
51
|
|
|
57
52
|
/**
|
|
@@ -70,16 +65,65 @@ class ProjectIndex {
|
|
|
70
65
|
return fs.readFileSync(filePath, 'utf-8');
|
|
71
66
|
}
|
|
72
67
|
|
|
73
|
-
/** Start a per-operation content cache scope */
|
|
68
|
+
/** Start a per-operation content cache scope (supports nesting) */
|
|
74
69
|
_beginOp() {
|
|
75
70
|
if (!this._opContentCache) {
|
|
76
71
|
this._opContentCache = new Map();
|
|
72
|
+
this._opUsagesCache = new Map();
|
|
73
|
+
this._opDepth = 0;
|
|
77
74
|
}
|
|
75
|
+
this._opDepth++;
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
/** End a per-operation content cache scope */
|
|
78
|
+
/** End a per-operation content cache scope (only clears when outermost scope ends) */
|
|
81
79
|
_endOp() {
|
|
82
|
-
this.
|
|
80
|
+
if (--this._opDepth <= 0) {
|
|
81
|
+
this._opContentCache = null;
|
|
82
|
+
this._opUsagesCache = null;
|
|
83
|
+
this._opDepth = 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get findUsagesInCode results with per-operation caching.
|
|
89
|
+
* Avoids redundant tree-sitter parsing when the same (file, name) is queried
|
|
90
|
+
* multiple times within one operation (e.g., about() calls both countSymbolUsages and usages).
|
|
91
|
+
* @param {string} filePath - File to scan
|
|
92
|
+
* @param {string} name - Symbol name to find
|
|
93
|
+
* @returns {Array|null} Array of usage objects or null if parsing failed
|
|
94
|
+
*/
|
|
95
|
+
_getCachedUsages(filePath, name) {
|
|
96
|
+
const cacheKey = `${filePath}\0${name}`;
|
|
97
|
+
if (this._opUsagesCache) {
|
|
98
|
+
const cached = this._opUsagesCache.get(cacheKey);
|
|
99
|
+
if (cached !== undefined) return cached;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const lang = detectLanguage(filePath);
|
|
103
|
+
const langModule = getLanguageModule(lang);
|
|
104
|
+
if (!langModule || typeof langModule.findUsagesInCode !== 'function') return null;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Fast pre-check: skip tree-sitter parsing if name doesn't appear in file
|
|
108
|
+
const content = this._readFile(filePath);
|
|
109
|
+
if (!content.includes(name)) {
|
|
110
|
+
const empty = [];
|
|
111
|
+
if (this._opUsagesCache) {
|
|
112
|
+
this._opUsagesCache.set(cacheKey, empty);
|
|
113
|
+
}
|
|
114
|
+
return empty;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const parser = getParser(lang);
|
|
118
|
+
if (!parser) return null;
|
|
119
|
+
const usages = langModule.findUsagesInCode(content, name, parser);
|
|
120
|
+
if (this._opUsagesCache) {
|
|
121
|
+
this._opUsagesCache.set(cacheKey, usages);
|
|
122
|
+
}
|
|
123
|
+
return usages;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
83
127
|
}
|
|
84
128
|
|
|
85
129
|
/**
|
|
@@ -144,8 +188,9 @@ class ProjectIndex {
|
|
|
144
188
|
}
|
|
145
189
|
}
|
|
146
190
|
|
|
147
|
-
// Always invalidate
|
|
191
|
+
// Always invalidate caches on rebuild
|
|
148
192
|
this._completenessCache = null;
|
|
193
|
+
this._attrTypeCache = null;
|
|
149
194
|
|
|
150
195
|
let indexed = 0;
|
|
151
196
|
if (!this.failedFiles) this.failedFiles = new Set();
|
|
@@ -172,6 +217,19 @@ class ProjectIndex {
|
|
|
172
217
|
}
|
|
173
218
|
}
|
|
174
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Build a minimal index for a single file (no glob, no cache, no import graph).
|
|
222
|
+
* Used by CLI file mode to route through execute().
|
|
223
|
+
*/
|
|
224
|
+
buildSingleFile(filePath) {
|
|
225
|
+
const absPath = path.resolve(filePath);
|
|
226
|
+
if (!fs.existsSync(absPath)) {
|
|
227
|
+
throw new Error(`File not found: ${filePath}`);
|
|
228
|
+
}
|
|
229
|
+
this.indexFile(absPath);
|
|
230
|
+
this.buildTime = 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
175
233
|
/**
|
|
176
234
|
* Index a single file
|
|
177
235
|
*/
|
|
@@ -314,6 +372,9 @@ class ProjectIndex {
|
|
|
314
372
|
|
|
315
373
|
// Invalidate cached call data for this file
|
|
316
374
|
this.callsCache.delete(filePath);
|
|
375
|
+
|
|
376
|
+
// Invalidate attribute type cache for this file
|
|
377
|
+
if (this._attrTypeCache) this._attrTypeCache.delete(filePath);
|
|
317
378
|
}
|
|
318
379
|
|
|
319
380
|
/**
|
|
@@ -556,9 +617,12 @@ class ProjectIndex {
|
|
|
556
617
|
}
|
|
557
618
|
}
|
|
558
619
|
|
|
559
|
-
// Check inclusion (must be within specified directory)
|
|
620
|
+
// Check inclusion (must be within specified directory, path-boundary-aware)
|
|
560
621
|
if (filters.in) {
|
|
561
|
-
|
|
622
|
+
const inPattern = filters.in;
|
|
623
|
+
// Match at path boundaries: start of string or after /
|
|
624
|
+
// e.g. --in=src matches "src/foo.js" and "lib/src/foo.js" but NOT "my-src-backup/foo.js"
|
|
625
|
+
if (!(filePath.startsWith(inPattern + '/') || filePath.includes('/' + inPattern + '/'))) {
|
|
562
626
|
return false;
|
|
563
627
|
}
|
|
564
628
|
}
|
|
@@ -602,7 +666,7 @@ class ProjectIndex {
|
|
|
602
666
|
if (lowerTarget.includes(lowerQuery)) return 400 + (query.length / target.length) * 100;
|
|
603
667
|
|
|
604
668
|
// Word boundary match (parse -> parseFile, fileParse)
|
|
605
|
-
const words =
|
|
669
|
+
const words = target.split(/(?=[A-Z])|_|-/).map(w => w.toLowerCase());
|
|
606
670
|
if (words.some(w => w.startsWith(lowerQuery))) return 300;
|
|
607
671
|
|
|
608
672
|
return 0;
|
|
@@ -690,9 +754,16 @@ class ProjectIndex {
|
|
|
690
754
|
}
|
|
691
755
|
|
|
692
756
|
find(name, options = {}) {
|
|
757
|
+
this._beginOp();
|
|
758
|
+
try {
|
|
693
759
|
// Glob pattern matching (e.g., _update*, handle*Request, get?ata)
|
|
694
760
|
const isGlob = name.includes('*') || name.includes('?');
|
|
695
761
|
if (isGlob && !options.exact) {
|
|
762
|
+
// Guard against bare wildcards that would match everything
|
|
763
|
+
const stripped = name.replace(/[*?]/g, '');
|
|
764
|
+
if (stripped.length === 0) {
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
696
767
|
const globRegex = new RegExp('^' + name.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
|
|
697
768
|
const matches = [];
|
|
698
769
|
for (const [symName, symbols] of this.symbols) {
|
|
@@ -725,6 +796,7 @@ class ProjectIndex {
|
|
|
725
796
|
}
|
|
726
797
|
|
|
727
798
|
return this._applyFindFilters(matches, options);
|
|
799
|
+
} finally { this._endOp(); }
|
|
728
800
|
}
|
|
729
801
|
|
|
730
802
|
/**
|
|
@@ -747,6 +819,11 @@ class ProjectIndex {
|
|
|
747
819
|
);
|
|
748
820
|
}
|
|
749
821
|
|
|
822
|
+
// Skip expensive usage counting when caller doesn't need it
|
|
823
|
+
if (options.skipCounts) {
|
|
824
|
+
return filtered;
|
|
825
|
+
}
|
|
826
|
+
|
|
750
827
|
// Add per-symbol usage counts for disambiguation
|
|
751
828
|
const withCounts = filtered.map(m => {
|
|
752
829
|
const counts = this.countSymbolUsages(m);
|
|
@@ -806,38 +883,27 @@ class ProjectIndex {
|
|
|
806
883
|
if (!this.files.has(filePath)) continue;
|
|
807
884
|
|
|
808
885
|
try {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const key = `${filePath}:${u.line}:${u.usageType}`;
|
|
824
|
-
if (seen.has(key)) continue;
|
|
825
|
-
seen.add(key);
|
|
826
|
-
switch (u.usageType) {
|
|
827
|
-
case 'call': calls++; break;
|
|
828
|
-
case 'definition': definitions++; break;
|
|
829
|
-
case 'import': imports++; break;
|
|
830
|
-
default: references++; break;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
continue; // Skip to next file
|
|
886
|
+
// Try AST-based counting first (with per-operation cache)
|
|
887
|
+
const astUsages = this._getCachedUsages(filePath, name);
|
|
888
|
+
if (astUsages !== null) {
|
|
889
|
+
// Deduplicate same-line same-type entries (e.g., `name: obj.name` has two AST nodes)
|
|
890
|
+
const seen = new Set();
|
|
891
|
+
for (const u of astUsages) {
|
|
892
|
+
const key = `${filePath}:${u.line}:${u.usageType}`;
|
|
893
|
+
if (seen.has(key)) continue;
|
|
894
|
+
seen.add(key);
|
|
895
|
+
switch (u.usageType) {
|
|
896
|
+
case 'call': calls++; break;
|
|
897
|
+
case 'definition': definitions++; break;
|
|
898
|
+
case 'import': imports++; break;
|
|
899
|
+
default: references++; break;
|
|
834
900
|
}
|
|
835
|
-
} catch (e) {
|
|
836
|
-
// Fall through to regex-based counting
|
|
837
901
|
}
|
|
902
|
+
continue; // Skip to next file
|
|
838
903
|
}
|
|
839
904
|
|
|
840
905
|
// Fallback: count regex matches as references (unsupported language)
|
|
906
|
+
const content = this._readFile(filePath);
|
|
841
907
|
const lines = content.split('\n');
|
|
842
908
|
lines.forEach((line) => {
|
|
843
909
|
if (regex.test(line)) {
|
|
@@ -895,56 +961,48 @@ class ProjectIndex {
|
|
|
895
961
|
|
|
896
962
|
try {
|
|
897
963
|
const content = this._readFile(filePath);
|
|
898
|
-
const lines = content.split('\n');
|
|
899
964
|
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
const langModule = getLanguageModule(lang);
|
|
965
|
+
// Fast pre-check: skip if name doesn't appear in file at all
|
|
966
|
+
if (!content.includes(name)) continue;
|
|
903
967
|
|
|
904
|
-
|
|
905
|
-
// AST-based detection
|
|
906
|
-
try {
|
|
907
|
-
const parser = getParser(lang);
|
|
908
|
-
if (parser) {
|
|
909
|
-
const astUsages = langModule.findUsagesInCode(content, name, parser);
|
|
910
|
-
|
|
911
|
-
for (const u of astUsages) {
|
|
912
|
-
// Skip if this is a definition line (already added above)
|
|
913
|
-
if (definitions.some(d => d.file === filePath && d.startLine === u.line)) {
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
968
|
+
const lines = content.split('\n');
|
|
916
969
|
|
|
917
|
-
|
|
970
|
+
// Try AST-based detection first (with per-operation cache)
|
|
971
|
+
const astUsages = this._getCachedUsages(filePath, name);
|
|
972
|
+
if (astUsages !== null) {
|
|
973
|
+
for (const u of astUsages) {
|
|
974
|
+
// Skip if this is a definition line (already added above)
|
|
975
|
+
if (definitions.some(d => d.file === filePath && d.startLine === u.line)) {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
918
978
|
|
|
919
|
-
|
|
920
|
-
file: filePath,
|
|
921
|
-
relativePath: fileEntry.relativePath,
|
|
922
|
-
line: u.line,
|
|
923
|
-
content: lineContent,
|
|
924
|
-
usageType: u.usageType,
|
|
925
|
-
isDefinition: false
|
|
926
|
-
};
|
|
979
|
+
const lineContent = lines[u.line - 1] || '';
|
|
927
980
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
usage.before = before;
|
|
938
|
-
usage.after = after;
|
|
939
|
-
}
|
|
981
|
+
const usage = {
|
|
982
|
+
file: filePath,
|
|
983
|
+
relativePath: fileEntry.relativePath,
|
|
984
|
+
line: u.line,
|
|
985
|
+
content: lineContent,
|
|
986
|
+
usageType: u.usageType,
|
|
987
|
+
isDefinition: false
|
|
988
|
+
};
|
|
940
989
|
|
|
941
|
-
|
|
990
|
+
// Add context lines if requested
|
|
991
|
+
if (options.context && options.context > 0) {
|
|
992
|
+
const idx = u.line - 1;
|
|
993
|
+
const before = [];
|
|
994
|
+
const after = [];
|
|
995
|
+
for (let i = 1; i <= options.context; i++) {
|
|
996
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
997
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
942
998
|
}
|
|
943
|
-
|
|
999
|
+
usage.before = before;
|
|
1000
|
+
usage.after = after;
|
|
944
1001
|
}
|
|
945
|
-
|
|
946
|
-
|
|
1002
|
+
|
|
1003
|
+
usages.push(usage);
|
|
947
1004
|
}
|
|
1005
|
+
continue; // Skip to next file
|
|
948
1006
|
}
|
|
949
1007
|
|
|
950
1008
|
// Fallback to regex-based detection
|
|
@@ -1037,7 +1095,8 @@ class ProjectIndex {
|
|
|
1037
1095
|
}
|
|
1038
1096
|
|
|
1039
1097
|
// Check Go/Rust-style receiver (e.g., func (r *Router) Method())
|
|
1040
|
-
|
|
1098
|
+
// Also matches Rust associated functions (have receiver but isMethod=false)
|
|
1099
|
+
if (symbol.receiver) {
|
|
1041
1100
|
const receiverBase = symbol.receiver.replace(/^\*/, '');
|
|
1042
1101
|
if (receiverBase === baseTypeName) {
|
|
1043
1102
|
methods.push(symbol);
|
|
@@ -1080,6 +1139,12 @@ class ProjectIndex {
|
|
|
1080
1139
|
if (['class', 'struct', 'interface', 'type'].includes(def.type)) {
|
|
1081
1140
|
const methods = this.findMethodsForType(name);
|
|
1082
1141
|
|
|
1142
|
+
let typeCallers = this.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain });
|
|
1143
|
+
// Apply exclude filter
|
|
1144
|
+
if (options.exclude && options.exclude.length > 0) {
|
|
1145
|
+
typeCallers = typeCallers.filter(c => this.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1083
1148
|
const result = {
|
|
1084
1149
|
type: def.type,
|
|
1085
1150
|
name: name,
|
|
@@ -1095,7 +1160,7 @@ class ProjectIndex {
|
|
|
1095
1160
|
receiver: m.receiver
|
|
1096
1161
|
})),
|
|
1097
1162
|
// Also include places where the type is used in function parameters/returns
|
|
1098
|
-
callers:
|
|
1163
|
+
callers: typeCallers
|
|
1099
1164
|
};
|
|
1100
1165
|
|
|
1101
1166
|
if (warnings.length > 0) {
|
|
@@ -1154,343 +1219,11 @@ class ProjectIndex {
|
|
|
1154
1219
|
} finally { this._endOp(); }
|
|
1155
1220
|
}
|
|
1156
1221
|
|
|
1157
|
-
/**
|
|
1158
|
-
|
|
1159
|
-
* Uses mtime for fast cache validation, falls back to hash if mtime matches but content changed
|
|
1160
|
-
* @param {string} filePath - Path to the file
|
|
1161
|
-
* @param {object} [options] - Options
|
|
1162
|
-
* @param {boolean} [options.includeContent] - Also return file content (avoids double read)
|
|
1163
|
-
* @returns {Array|null|{calls: Array, content: string}} Array of calls, or object with content if requested
|
|
1164
|
-
*/
|
|
1165
|
-
getCachedCalls(filePath, options = {}) {
|
|
1166
|
-
try {
|
|
1167
|
-
const cached = this.callsCache.get(filePath);
|
|
1168
|
-
|
|
1169
|
-
// Fast path: check mtime first (stat is much faster than read+hash)
|
|
1170
|
-
const stat = fs.statSync(filePath);
|
|
1171
|
-
const mtime = stat.mtimeMs;
|
|
1172
|
-
|
|
1173
|
-
if (cached && cached.mtime === mtime) {
|
|
1174
|
-
// mtime matches - cache is likely valid
|
|
1175
|
-
if (options.includeContent) {
|
|
1176
|
-
// Need content, read if not cached
|
|
1177
|
-
const content = cached.content || this._readFile(filePath);
|
|
1178
|
-
return { calls: cached.calls, content };
|
|
1179
|
-
}
|
|
1180
|
-
return cached.calls;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// mtime changed or no cache - need to read and possibly reparse
|
|
1184
|
-
const content = this._readFile(filePath);
|
|
1185
|
-
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
1186
|
-
|
|
1187
|
-
// Check if content actually changed (mtime can change without content change)
|
|
1188
|
-
if (cached && cached.hash === hash) {
|
|
1189
|
-
// Content unchanged, just update mtime
|
|
1190
|
-
cached.mtime = mtime;
|
|
1191
|
-
cached.content = options.includeContent ? content : undefined;
|
|
1192
|
-
if (options.includeContent) {
|
|
1193
|
-
return { calls: cached.calls, content };
|
|
1194
|
-
}
|
|
1195
|
-
return cached.calls;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// Content changed - need to reparse
|
|
1199
|
-
const language = detectLanguage(filePath);
|
|
1200
|
-
if (!language) return null;
|
|
1201
|
-
|
|
1202
|
-
const langModule = getLanguageModule(language);
|
|
1203
|
-
if (!langModule.findCallsInCode) return null;
|
|
1204
|
-
|
|
1205
|
-
const parser = getParser(language);
|
|
1206
|
-
const calls = langModule.findCallsInCode(content, parser);
|
|
1207
|
-
|
|
1208
|
-
this.callsCache.set(filePath, {
|
|
1209
|
-
mtime,
|
|
1210
|
-
hash,
|
|
1211
|
-
calls,
|
|
1212
|
-
content: options.includeContent ? content : undefined
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
if (options.includeContent) {
|
|
1216
|
-
return { calls, content };
|
|
1217
|
-
}
|
|
1218
|
-
return calls;
|
|
1219
|
-
} catch (e) {
|
|
1220
|
-
return null;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
* Find all callers of a function using AST-based detection
|
|
1226
|
-
* @param {string} name - Function name to find callers for
|
|
1227
|
-
* @param {object} [options] - Options
|
|
1228
|
-
* @param {boolean} [options.includeMethods] - Include method calls (default: false)
|
|
1229
|
-
*/
|
|
1230
|
-
findCallers(name, options = {}) {
|
|
1231
|
-
this._beginOp();
|
|
1232
|
-
try {
|
|
1233
|
-
const callers = [];
|
|
1234
|
-
const stats = options.stats;
|
|
1235
|
-
|
|
1236
|
-
// Get definition lines to exclude them
|
|
1237
|
-
const definitions = this.symbols.get(name) || [];
|
|
1238
|
-
const definitionLines = new Set();
|
|
1239
|
-
for (const def of definitions) {
|
|
1240
|
-
definitionLines.add(`${def.file}:${def.startLine}`);
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
1244
|
-
try {
|
|
1245
|
-
const result = this.getCachedCalls(filePath, { includeContent: true });
|
|
1246
|
-
if (!result) continue;
|
|
1247
|
-
|
|
1248
|
-
const { calls, content } = result;
|
|
1249
|
-
const lines = content.split('\n');
|
|
1250
|
-
|
|
1251
|
-
for (const call of calls) {
|
|
1252
|
-
// Skip if not matching our target name (also check alias resolution)
|
|
1253
|
-
if (call.name !== name && call.resolvedName !== name &&
|
|
1254
|
-
!(call.resolvedNames && call.resolvedNames.includes(name))) continue;
|
|
1255
|
-
|
|
1256
|
-
// For potential callbacks (function passed as arg), validate against symbol table
|
|
1257
|
-
// and skip complex binding resolution — just check the name exists
|
|
1258
|
-
if (call.isPotentialCallback) {
|
|
1259
|
-
const syms = definitions;
|
|
1260
|
-
if (!syms || syms.length === 0) continue;
|
|
1261
|
-
// Find the enclosing function
|
|
1262
|
-
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
1263
|
-
callers.push({
|
|
1264
|
-
file: filePath,
|
|
1265
|
-
relativePath: fileEntry.relativePath,
|
|
1266
|
-
line: call.line,
|
|
1267
|
-
content: lines[call.line - 1] || '',
|
|
1268
|
-
callerName: callerSymbol ? callerSymbol.name : null,
|
|
1269
|
-
callerFile: callerSymbol ? filePath : null,
|
|
1270
|
-
callerStartLine: callerSymbol ? callerSymbol.startLine : null,
|
|
1271
|
-
callerEndLine: callerSymbol ? callerSymbol.endLine : null,
|
|
1272
|
-
isMethod: false,
|
|
1273
|
-
isFunctionReference: true
|
|
1274
|
-
});
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// Resolve binding within this file (without mutating cached call objects)
|
|
1279
|
-
let bindingId = call.bindingId;
|
|
1280
|
-
let isUncertain = call.uncertain;
|
|
1281
|
-
if (!bindingId) {
|
|
1282
|
-
let bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
|
|
1283
|
-
// For Go, also check sibling files in same directory (same package scope)
|
|
1284
|
-
if (bindings.length === 0 && fileEntry.language === 'go') {
|
|
1285
|
-
const dir = path.dirname(filePath);
|
|
1286
|
-
for (const [fp, fe] of this.files) {
|
|
1287
|
-
if (fp !== filePath && path.dirname(fp) === dir) {
|
|
1288
|
-
const sibling = (fe.bindings || []).filter(b => b.name === call.name);
|
|
1289
|
-
bindings = bindings.concat(sibling);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
if (bindings.length === 1) {
|
|
1294
|
-
bindingId = bindings[0].id;
|
|
1295
|
-
} else if (bindings.length > 1 && !call.isMethod) {
|
|
1296
|
-
// For implicit same-class calls (Java: execute() means this.execute()),
|
|
1297
|
-
// try to resolve via caller's className before marking uncertain
|
|
1298
|
-
const callerSym = this.findEnclosingFunction(filePath, call.line, true);
|
|
1299
|
-
if (callerSym?.className) {
|
|
1300
|
-
const callSymbols = this.symbols.get(call.name);
|
|
1301
|
-
const sameClassSym = callSymbols?.find(s => s.className === callerSym.className);
|
|
1302
|
-
if (sameClassSym) {
|
|
1303
|
-
const matchingBinding = bindings.find(b => b.startLine === sameClassSym.startLine);
|
|
1304
|
-
bindingId = matchingBinding?.id || sameClassSym.bindingId;
|
|
1305
|
-
} else {
|
|
1306
|
-
isUncertain = true;
|
|
1307
|
-
}
|
|
1308
|
-
} else {
|
|
1309
|
-
// Scope-based disambiguation for shadowed functions:
|
|
1310
|
-
// When multiple bindings exist, use indent level to determine
|
|
1311
|
-
// which binding is in scope at the call site
|
|
1312
|
-
const defs = this.symbols.get(call.name);
|
|
1313
|
-
let resolved = false;
|
|
1314
|
-
if (defs) {
|
|
1315
|
-
// Sort bindings by indent desc (most nested first)
|
|
1316
|
-
const scopedBindings = bindings.map(b => {
|
|
1317
|
-
const sym = defs.find(s => s.startLine === b.startLine && s.file === filePath);
|
|
1318
|
-
return { ...b, indent: sym?.indent ?? 0, endLine: sym?.endLine ?? b.startLine };
|
|
1319
|
-
}).sort((a, b) => b.indent - a.indent);
|
|
1320
|
-
|
|
1321
|
-
for (const sb of scopedBindings) {
|
|
1322
|
-
if (sb.indent === 0) {
|
|
1323
|
-
// Module-level binding — always in scope, use as fallback
|
|
1324
|
-
bindingId = sb.id;
|
|
1325
|
-
resolved = true;
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
// Nested binding — check if call is inside its enclosing function
|
|
1329
|
-
const enclosing = this.findEnclosingFunction(filePath, sb.startLine, true);
|
|
1330
|
-
if (enclosing && call.line >= enclosing.startLine && call.line <= enclosing.endLine) {
|
|
1331
|
-
// Call is inside the same function as this binding
|
|
1332
|
-
bindingId = sb.id;
|
|
1333
|
-
resolved = true;
|
|
1334
|
-
break;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
if (!resolved) isUncertain = true;
|
|
1339
|
-
}
|
|
1340
|
-
} else if (bindings.length > 1 && call.isMethod) {
|
|
1341
|
-
// Multiple method bindings (e.g. Go String() on Reader vs Writer):
|
|
1342
|
-
// Don't mark uncertain — include them even if conflated.
|
|
1343
|
-
// Better to over-report than lose all callers.
|
|
1344
|
-
} else if (bindings.length !== 0) {
|
|
1345
|
-
isUncertain = true;
|
|
1346
|
-
}
|
|
1347
|
-
// Method call with no binding for the method name (JS/TS/Python only):
|
|
1348
|
-
// Mark uncertain unless receiver has binding evidence in file scope.
|
|
1349
|
-
// Go/Java/Rust excluded: callers are used for impact analysis where
|
|
1350
|
-
// over-reporting is preferred to losing callers. These languages' nominal
|
|
1351
|
-
// type systems also make method links more reliable.
|
|
1352
|
-
if (bindings.length === 0 && call.isMethod &&
|
|
1353
|
-
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
|
|
1354
|
-
const hasReceiverEvidence = call.receiver &&
|
|
1355
|
-
(fileEntry.bindings || []).some(b => b.name === call.receiver);
|
|
1356
|
-
if (!hasReceiverEvidence) {
|
|
1357
|
-
isUncertain = true;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// Smart method call handling — do this BEFORE uncertain check so
|
|
1363
|
-
// self/this.method() calls can be resolved by same-class matching
|
|
1364
|
-
// even when binding is ambiguous (e.g. method exists in multiple classes)
|
|
1365
|
-
let resolvedBySameClass = false;
|
|
1366
|
-
if (call.isMethod) {
|
|
1367
|
-
if (call.selfAttribute && fileEntry.language === 'python') {
|
|
1368
|
-
// self.attr.method() — resolve via attribute type inference
|
|
1369
|
-
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
1370
|
-
if (!callerSymbol?.className) continue;
|
|
1371
|
-
const attrTypes = this.getInstanceAttributeTypes(filePath, callerSymbol.className);
|
|
1372
|
-
if (!attrTypes) continue;
|
|
1373
|
-
const targetClass = attrTypes.get(call.selfAttribute);
|
|
1374
|
-
if (!targetClass) continue;
|
|
1375
|
-
// Check if any definition of searched function belongs to targetClass
|
|
1376
|
-
const matchesDef = definitions.some(d => d.className === targetClass);
|
|
1377
|
-
if (!matchesDef) continue;
|
|
1378
|
-
resolvedBySameClass = true;
|
|
1379
|
-
// Falls through to add as caller
|
|
1380
|
-
} else if (['self', 'cls', 'this', 'super'].includes(call.receiver)) {
|
|
1381
|
-
// self/this/super.method() — resolve to same-class or parent method
|
|
1382
|
-
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
1383
|
-
if (!callerSymbol?.className) continue;
|
|
1384
|
-
// For super(), skip same-class — only check parent chain
|
|
1385
|
-
let matchesDef = call.receiver === 'super'
|
|
1386
|
-
? false
|
|
1387
|
-
: definitions.some(d => d.className === callerSymbol.className);
|
|
1388
|
-
// Walk inheritance chain using BFS if not found in same class
|
|
1389
|
-
if (!matchesDef) {
|
|
1390
|
-
const visited = new Set([callerSymbol.className]);
|
|
1391
|
-
const callerFile = callerSymbol.file || filePath;
|
|
1392
|
-
const startParents = this._getInheritanceParents(callerSymbol.className, callerFile) || [];
|
|
1393
|
-
const queue = startParents.map(p => ({ name: p, contextFile: callerFile }));
|
|
1394
|
-
while (queue.length > 0 && !matchesDef) {
|
|
1395
|
-
const { name: current, contextFile } = queue.shift();
|
|
1396
|
-
if (visited.has(current)) continue;
|
|
1397
|
-
visited.add(current);
|
|
1398
|
-
matchesDef = definitions.some(d => d.className === current);
|
|
1399
|
-
if (!matchesDef) {
|
|
1400
|
-
const resolvedFile = this._resolveClassFile(current, contextFile);
|
|
1401
|
-
const grandparents = this._getInheritanceParents(current, resolvedFile) || [];
|
|
1402
|
-
for (const gp of grandparents) {
|
|
1403
|
-
if (!visited.has(gp)) queue.push({ name: gp, contextFile: resolvedFile });
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
if (!matchesDef) continue;
|
|
1409
|
-
resolvedBySameClass = true;
|
|
1410
|
-
// Falls through to add as caller
|
|
1411
|
-
} else {
|
|
1412
|
-
// Go doesn't use this/self/cls - always include Go method calls
|
|
1413
|
-
// Java method calls are always obj.method() - include by default
|
|
1414
|
-
// Rust Type::method() calls - include by default (associated functions)
|
|
1415
|
-
// For other languages, skip method calls unless explicitly requested
|
|
1416
|
-
if (fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust' && !options.includeMethods) continue;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
// Skip uncertain calls unless resolved by same-class matching or explicitly requested
|
|
1421
|
-
if (isUncertain && !resolvedBySameClass && !options.includeUncertain) {
|
|
1422
|
-
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
1423
|
-
continue;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Skip definition lines
|
|
1427
|
-
if (definitionLines.has(`${filePath}:${call.line}`)) continue;
|
|
1428
|
-
|
|
1429
|
-
// If we have a binding id on definition, require match when available
|
|
1430
|
-
// When targetDefinitions is provided, only those definitions' bindings are valid targets
|
|
1431
|
-
const targetDefs = options.targetDefinitions || definitions;
|
|
1432
|
-
const targetBindingIds = new Set(targetDefs.map(d => d.bindingId).filter(Boolean));
|
|
1433
|
-
if (targetBindingIds.size > 0 && bindingId && !targetBindingIds.has(bindingId)) {
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
// Java/Go/Rust receiver-class disambiguation:
|
|
1438
|
-
// When targetDefinitions narrows to specific class(es) and the call has a
|
|
1439
|
-
// receiver (e.g. javascriptFileService.createDataFile()), check if the
|
|
1440
|
-
// receiver name better matches a non-target class definition.
|
|
1441
|
-
// This prevents false positives like reporting obj.save() as a caller of
|
|
1442
|
-
// TargetClass.save() when obj is clearly a different type.
|
|
1443
|
-
if (call.isMethod && call.receiver && !resolvedBySameClass && !bindingId &&
|
|
1444
|
-
options.targetDefinitions && definitions.length > 1 &&
|
|
1445
|
-
(fileEntry.language === 'java' || fileEntry.language === 'go' || fileEntry.language === 'rust')) {
|
|
1446
|
-
const targetClassNames = new Set(targetDefs.map(d => d.className).filter(Boolean));
|
|
1447
|
-
if (targetClassNames.size > 0) {
|
|
1448
|
-
const receiverLower = call.receiver.toLowerCase();
|
|
1449
|
-
// Check if receiver matches any target class (camelCase convention)
|
|
1450
|
-
const matchesTarget = [...targetClassNames].some(cn => cn.toLowerCase() === receiverLower);
|
|
1451
|
-
if (!matchesTarget) {
|
|
1452
|
-
// Check if receiver matches a non-target class instead
|
|
1453
|
-
const nonTargetClasses = definitions
|
|
1454
|
-
.filter(d => d.className && !targetClassNames.has(d.className))
|
|
1455
|
-
.map(d => d.className);
|
|
1456
|
-
const matchesOther = nonTargetClasses.some(cn => cn.toLowerCase() === receiverLower);
|
|
1457
|
-
if (matchesOther) {
|
|
1458
|
-
// Receiver clearly belongs to a different class
|
|
1459
|
-
isUncertain = true;
|
|
1460
|
-
if (!options.includeUncertain) {
|
|
1461
|
-
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
1462
|
-
continue;
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// Find the enclosing function (get full symbol info)
|
|
1470
|
-
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
1471
|
-
|
|
1472
|
-
callers.push({
|
|
1473
|
-
file: filePath,
|
|
1474
|
-
relativePath: fileEntry.relativePath,
|
|
1475
|
-
line: call.line,
|
|
1476
|
-
content: lines[call.line - 1] || '',
|
|
1477
|
-
callerName: callerSymbol ? callerSymbol.name : null,
|
|
1478
|
-
callerFile: callerSymbol ? filePath : null,
|
|
1479
|
-
callerStartLine: callerSymbol ? callerSymbol.startLine : null,
|
|
1480
|
-
callerEndLine: callerSymbol ? callerSymbol.endLine : null,
|
|
1481
|
-
isMethod: call.isMethod || false,
|
|
1482
|
-
receiver: call.receiver
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
} catch (e) {
|
|
1486
|
-
// Expected: minified files exceed tree-sitter buffer, binary files fail to parse.
|
|
1487
|
-
// These are not actionable errors — silently skip.
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1222
|
+
/** Get cached call sites for a file, with mtime/hash validation */
|
|
1223
|
+
getCachedCalls(filePath, options) { return callersModule.getCachedCalls(this, filePath, options); }
|
|
1490
1224
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
}
|
|
1225
|
+
/** Find all callers of a function using AST-based detection */
|
|
1226
|
+
findCallers(name, options) { return callersModule.findCallers(this, name, options); }
|
|
1494
1227
|
|
|
1495
1228
|
/**
|
|
1496
1229
|
* Check if a name appears inside a string literal using AST
|
|
@@ -1537,481 +1270,91 @@ class ProjectIndex {
|
|
|
1537
1270
|
}
|
|
1538
1271
|
}
|
|
1539
1272
|
|
|
1273
|
+
/** Find all functions called by a function using AST-based detection */
|
|
1274
|
+
findCallees(def, options) { return callersModule.findCallees(this, def, options); }
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Calculate dependency weight based on usage
|
|
1278
|
+
*/
|
|
1279
|
+
calculateWeight(callCount) {
|
|
1280
|
+
if (callCount >= 10) return 'core';
|
|
1281
|
+
if (callCount >= 3) return 'regular';
|
|
1282
|
+
if (callCount === 1) return 'utility';
|
|
1283
|
+
return 'normal';
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1540
1286
|
/**
|
|
1541
|
-
*
|
|
1542
|
-
* @param {object} def - Symbol definition with file, name, startLine, endLine
|
|
1543
|
-
* @param {object} [options] - Options
|
|
1544
|
-
* @param {boolean} [options.includeMethods] - Include method calls (default: false)
|
|
1287
|
+
* Smart extraction: function + dependencies
|
|
1545
1288
|
*/
|
|
1546
|
-
|
|
1289
|
+
smart(name, options = {}) {
|
|
1547
1290
|
this._beginOp();
|
|
1548
1291
|
try {
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
const language = fileEntry?.language;
|
|
1557
|
-
|
|
1558
|
-
// Build list of inner class/struct method ranges to exclude from callee detection.
|
|
1559
|
-
// Only class methods are excluded — they are independently addressable symbols.
|
|
1560
|
-
// Calls within closures (named functions without className) ARE included as
|
|
1561
|
-
// callees of the parent function, since closures are part of the parent's behavior.
|
|
1562
|
-
const innerSymbolRanges = fileEntry ? fileEntry.symbols
|
|
1563
|
-
.filter(s => !NON_CALLABLE_TYPES.has(s.type) &&
|
|
1564
|
-
s.className && // Only exclude class methods, not closures
|
|
1565
|
-
s.startLine > def.startLine && s.endLine <= def.endLine &&
|
|
1566
|
-
s.startLine !== def.startLine)
|
|
1567
|
-
.map(s => [s.startLine, s.endLine]) : [];
|
|
1568
|
-
|
|
1569
|
-
const callees = new Map(); // key -> { name, bindingId, count }
|
|
1570
|
-
let selfAttrCalls = null; // collected for Python self.attr.method() resolution
|
|
1571
|
-
let selfMethodCalls = null; // collected for Python self.method() resolution
|
|
1572
|
-
|
|
1573
|
-
for (const call of calls) {
|
|
1574
|
-
// Filter to calls within this function's scope
|
|
1575
|
-
// Method 1: Direct match via enclosingFunction (fast path for direct calls)
|
|
1576
|
-
const isDirectMatch = call.enclosingFunction &&
|
|
1577
|
-
call.enclosingFunction.startLine === def.startLine;
|
|
1578
|
-
// Method 2: Line-range containment (catches calls inside nested callbacks/closures)
|
|
1579
|
-
// A call is in our scope if it's within our line range AND not inside a named inner symbol
|
|
1580
|
-
const isInRange = call.line >= def.startLine && call.line <= def.endLine;
|
|
1581
|
-
const isInInnerSymbol = isInRange && innerSymbolRanges.some(
|
|
1582
|
-
([start, end]) => call.line >= start && call.line <= end);
|
|
1583
|
-
const isNestedCallback = isInRange && !isInInnerSymbol && !isDirectMatch;
|
|
1584
|
-
|
|
1585
|
-
if (!isDirectMatch && !isNestedCallback) continue;
|
|
1586
|
-
|
|
1587
|
-
// Smart method call handling:
|
|
1588
|
-
// - Go: include all method calls (Go doesn't use this/self/cls)
|
|
1589
|
-
// - self/this.method(): resolve to same-class method (handled below)
|
|
1590
|
-
// - Python self.attr.method(): resolve via selfAttribute (handled below)
|
|
1591
|
-
// - Other languages: skip method calls unless explicitly requested
|
|
1592
|
-
if (call.isMethod) {
|
|
1593
|
-
if (call.selfAttribute && language === 'python') {
|
|
1594
|
-
// Will be resolved in second pass below
|
|
1595
|
-
} else if (['self', 'cls', 'this'].includes(call.receiver)) {
|
|
1596
|
-
// self.method() / cls.method() / this.method() — resolve to same-class method below
|
|
1597
|
-
} else if (call.receiver === 'super') {
|
|
1598
|
-
// super().method() — resolve to parent class method below
|
|
1599
|
-
} else if (language !== 'go' && language !== 'java' && language !== 'rust' && !options.includeMethods) {
|
|
1600
|
-
continue;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1292
|
+
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
1293
|
+
if (!def) {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
const code = this.extractCode(def);
|
|
1297
|
+
const stats = { uncertain: 0 };
|
|
1298
|
+
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
1603
1299
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1300
|
+
const filesInScope = new Set([def.file]);
|
|
1301
|
+
callees.forEach(c => filesInScope.add(c.file));
|
|
1302
|
+
let dynamicImports = 0;
|
|
1303
|
+
for (const f of filesInScope) {
|
|
1304
|
+
const fe = this.files.get(f);
|
|
1305
|
+
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
1306
|
+
}
|
|
1606
1307
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1308
|
+
// Extract code for each dependency, excluding the exact same function
|
|
1309
|
+
// (but keeping same-name overloads, e.g. Java toJson(Object) vs toJson(Object, Class))
|
|
1310
|
+
const defBindingId = def.bindingId;
|
|
1311
|
+
const dependencies = callees
|
|
1312
|
+
.filter(callee => callee.bindingId !== defBindingId)
|
|
1313
|
+
.map(callee => ({
|
|
1314
|
+
...callee,
|
|
1315
|
+
code: this.extractCode(callee)
|
|
1316
|
+
}));
|
|
1615
1317
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1318
|
+
// Find type definitions if requested
|
|
1319
|
+
const types = [];
|
|
1320
|
+
if (options.withTypes) {
|
|
1321
|
+
// Look for type annotations in params/return type
|
|
1322
|
+
const typeNames = this.extractTypeNames(def);
|
|
1323
|
+
for (const typeName of typeNames) {
|
|
1324
|
+
const typeSymbols = this.symbols.get(typeName);
|
|
1325
|
+
if (typeSymbols) {
|
|
1326
|
+
for (const sym of typeSymbols) {
|
|
1327
|
+
if (['type', 'interface', 'class', 'struct'].includes(sym.type)) {
|
|
1328
|
+
types.push({
|
|
1329
|
+
...sym,
|
|
1330
|
+
code: this.extractCode(sym)
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1631
1333
|
}
|
|
1632
1334
|
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1633
1337
|
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1338
|
+
return {
|
|
1339
|
+
target: {
|
|
1340
|
+
...def,
|
|
1341
|
+
code
|
|
1342
|
+
},
|
|
1343
|
+
dependencies,
|
|
1344
|
+
types,
|
|
1345
|
+
meta: {
|
|
1346
|
+
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
1347
|
+
skipped: 0,
|
|
1348
|
+
dynamicImports,
|
|
1349
|
+
uncertain: stats.uncertain
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
} finally { this._endOp(); }
|
|
1353
|
+
}
|
|
1647
1354
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
selfMethodCalls.push(call);
|
|
1652
|
-
continue;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// Resolve binding within this file (without mutating cached call objects)
|
|
1656
|
-
let calleeKey = call.bindingId || effectiveName;
|
|
1657
|
-
let bindingResolved = call.bindingId;
|
|
1658
|
-
let isUncertain = call.uncertain;
|
|
1659
|
-
if (!call.bindingId && fileEntry?.bindings) {
|
|
1660
|
-
let bindings = fileEntry.bindings.filter(b => b.name === call.name);
|
|
1661
|
-
// For Go, also check sibling files in same directory (same package scope)
|
|
1662
|
-
if (bindings.length === 0 && language === 'go') {
|
|
1663
|
-
const dir = path.dirname(def.file);
|
|
1664
|
-
for (const [fp, fe] of this.files) {
|
|
1665
|
-
if (fp !== def.file && path.dirname(fp) === dir) {
|
|
1666
|
-
const sibling = (fe.bindings || []).filter(b => b.name === call.name);
|
|
1667
|
-
bindings = bindings.concat(sibling);
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
// Method call with no binding for the method name:
|
|
1672
|
-
// Different strategies by language family:
|
|
1673
|
-
if (bindings.length === 0 && call.isMethod) {
|
|
1674
|
-
if (language !== 'go' && language !== 'java' && language !== 'rust') {
|
|
1675
|
-
// JS/TS/Python: mark uncertain unless receiver has import/binding
|
|
1676
|
-
// evidence in file scope. Prevents false positives like m.get() →
|
|
1677
|
-
// repository.get() when m is just a parameter with no type info.
|
|
1678
|
-
const hasReceiverEvidence = call.receiver &&
|
|
1679
|
-
fileEntry?.bindings?.some(b => b.name === call.receiver);
|
|
1680
|
-
if (!hasReceiverEvidence) {
|
|
1681
|
-
isUncertain = true;
|
|
1682
|
-
}
|
|
1683
|
-
} else {
|
|
1684
|
-
// Go/Java/Rust: nominal type systems make single-def method links
|
|
1685
|
-
// reliable. Only mark uncertain when multiple definitions exist
|
|
1686
|
-
// (cross-type ambiguity, e.g. TypeA.Length vs TypeB.Length).
|
|
1687
|
-
const defs = this.symbols.get(call.name);
|
|
1688
|
-
if (defs && defs.length > 1) {
|
|
1689
|
-
isUncertain = true;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
if (bindings.length === 1) {
|
|
1694
|
-
bindingResolved = bindings[0].id;
|
|
1695
|
-
calleeKey = bindingResolved;
|
|
1696
|
-
} else if (bindings.length > 1) {
|
|
1697
|
-
if (call.name === def.name) {
|
|
1698
|
-
// Calling same-name function (e.g., Java overloads)
|
|
1699
|
-
// Add ALL other overloads as potential callees
|
|
1700
|
-
const otherBindings = bindings.filter(b =>
|
|
1701
|
-
b.startLine !== def.startLine
|
|
1702
|
-
);
|
|
1703
|
-
for (const ob of otherBindings) {
|
|
1704
|
-
const existing = callees.get(ob.id);
|
|
1705
|
-
if (existing) {
|
|
1706
|
-
existing.count += 1;
|
|
1707
|
-
} else {
|
|
1708
|
-
callees.set(ob.id, {
|
|
1709
|
-
name: effectiveName,
|
|
1710
|
-
bindingId: ob.id,
|
|
1711
|
-
count: 1
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
continue; // Already added all overloads, skip normal add
|
|
1716
|
-
} else if (def.className && !call.isMethod) {
|
|
1717
|
-
// Implicit same-class call (Java: execute() means this.execute())
|
|
1718
|
-
// Try to resolve to a binding in the same class via symbol lookup
|
|
1719
|
-
const callSymbols = this.symbols.get(call.name);
|
|
1720
|
-
if (callSymbols) {
|
|
1721
|
-
const sameClassSym = callSymbols.find(s => s.className === def.className);
|
|
1722
|
-
if (sameClassSym) {
|
|
1723
|
-
// Find the binding that matches this symbol's line
|
|
1724
|
-
const matchingBinding = bindings.find(b => b.startLine === sameClassSym.startLine);
|
|
1725
|
-
if (matchingBinding) {
|
|
1726
|
-
bindingResolved = matchingBinding.id;
|
|
1727
|
-
calleeKey = bindingResolved;
|
|
1728
|
-
} else {
|
|
1729
|
-
bindingResolved = sameClassSym.bindingId;
|
|
1730
|
-
calleeKey = bindingResolved || `${def.className}.${call.name}`;
|
|
1731
|
-
}
|
|
1732
|
-
} else {
|
|
1733
|
-
isUncertain = true;
|
|
1734
|
-
}
|
|
1735
|
-
} else {
|
|
1736
|
-
isUncertain = true;
|
|
1737
|
-
}
|
|
1738
|
-
} else {
|
|
1739
|
-
// Try to resolve to a binding defined within the parent function's
|
|
1740
|
-
// scope (inner closure). E.g., hookRunnerApplication defines next()
|
|
1741
|
-
// internally — prefer that over other next() in the same file.
|
|
1742
|
-
const innerBinding = bindings.find(b =>
|
|
1743
|
-
b.startLine > def.startLine && b.startLine <= def.endLine);
|
|
1744
|
-
if (innerBinding) {
|
|
1745
|
-
bindingResolved = innerBinding.id;
|
|
1746
|
-
calleeKey = bindingResolved;
|
|
1747
|
-
} else {
|
|
1748
|
-
isUncertain = true;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
if (isUncertain && !options.includeUncertain) {
|
|
1755
|
-
if (options.stats) options.stats.uncertain = (options.stats.uncertain || 0) + 1;
|
|
1756
|
-
continue;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
const existing = callees.get(calleeKey);
|
|
1760
|
-
if (existing) {
|
|
1761
|
-
existing.count += 1;
|
|
1762
|
-
} else {
|
|
1763
|
-
callees.set(calleeKey, {
|
|
1764
|
-
name: effectiveName,
|
|
1765
|
-
bindingId: bindingResolved,
|
|
1766
|
-
count: 1
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
// Second pass: resolve Python self.attr.method() calls
|
|
1772
|
-
if (selfAttrCalls && def.className) {
|
|
1773
|
-
const attrTypes = this.getInstanceAttributeTypes(def.file, def.className);
|
|
1774
|
-
if (attrTypes) {
|
|
1775
|
-
for (const call of selfAttrCalls) {
|
|
1776
|
-
const targetClass = attrTypes.get(call.selfAttribute);
|
|
1777
|
-
if (!targetClass) continue;
|
|
1778
|
-
|
|
1779
|
-
// Find method in symbol table where className matches
|
|
1780
|
-
const symbols = this.symbols.get(call.name);
|
|
1781
|
-
if (!symbols) continue;
|
|
1782
|
-
|
|
1783
|
-
const match = symbols.find(s => s.className === targetClass);
|
|
1784
|
-
if (!match) continue;
|
|
1785
|
-
|
|
1786
|
-
const key = match.bindingId || `${targetClass}.${call.name}`;
|
|
1787
|
-
const existing = callees.get(key);
|
|
1788
|
-
if (existing) {
|
|
1789
|
-
existing.count += 1;
|
|
1790
|
-
} else {
|
|
1791
|
-
callees.set(key, {
|
|
1792
|
-
name: call.name,
|
|
1793
|
-
bindingId: match.bindingId,
|
|
1794
|
-
count: 1
|
|
1795
|
-
});
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
// Third pass: resolve self/this/super.method() calls to same-class or parent methods
|
|
1802
|
-
// Falls back to walking the inheritance chain if not found in same class
|
|
1803
|
-
if (selfMethodCalls && def.className) {
|
|
1804
|
-
for (const call of selfMethodCalls) {
|
|
1805
|
-
const symbols = this.symbols.get(call.name);
|
|
1806
|
-
if (!symbols) continue;
|
|
1807
|
-
|
|
1808
|
-
// For super().method(), skip same-class — start from parent
|
|
1809
|
-
let match = call.receiver === 'super'
|
|
1810
|
-
? null
|
|
1811
|
-
: symbols.find(s => s.className === def.className);
|
|
1812
|
-
|
|
1813
|
-
// Walk inheritance chain using BFS if not found in same class
|
|
1814
|
-
if (!match) {
|
|
1815
|
-
const visited = new Set([def.className]);
|
|
1816
|
-
const defFile = def.file;
|
|
1817
|
-
const startParents = this._getInheritanceParents(def.className, defFile) || [];
|
|
1818
|
-
const queue = startParents.map(p => ({ name: p, contextFile: defFile }));
|
|
1819
|
-
while (queue.length > 0 && !match) {
|
|
1820
|
-
const { name: current, contextFile } = queue.shift();
|
|
1821
|
-
if (visited.has(current)) continue;
|
|
1822
|
-
visited.add(current);
|
|
1823
|
-
match = symbols.find(s => s.className === current);
|
|
1824
|
-
if (!match) {
|
|
1825
|
-
const resolvedFile = this._resolveClassFile(current, contextFile);
|
|
1826
|
-
const grandparents = this._getInheritanceParents(current, resolvedFile) || [];
|
|
1827
|
-
for (const gp of grandparents) {
|
|
1828
|
-
if (!visited.has(gp)) queue.push({ name: gp, contextFile: resolvedFile });
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
if (!match) continue;
|
|
1835
|
-
|
|
1836
|
-
const key = match.bindingId || `${match.className}.${call.name}`;
|
|
1837
|
-
const existing = callees.get(key);
|
|
1838
|
-
if (existing) {
|
|
1839
|
-
existing.count += 1;
|
|
1840
|
-
} else {
|
|
1841
|
-
callees.set(key, {
|
|
1842
|
-
name: call.name,
|
|
1843
|
-
bindingId: match.bindingId,
|
|
1844
|
-
count: 1
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
// Look up each callee in the symbol table
|
|
1851
|
-
// For methods, prefer callees from: 1) same file, 2) same package, 3) same receiver type
|
|
1852
|
-
// Also deprioritize test-file definitions when caller is in production code
|
|
1853
|
-
const result = [];
|
|
1854
|
-
const defDir = path.dirname(def.file);
|
|
1855
|
-
const defReceiver = def.receiver;
|
|
1856
|
-
const defFileEntry = fileEntry;
|
|
1857
|
-
const callerIsTest = defFileEntry && isTestFile(defFileEntry.relativePath, defFileEntry.language);
|
|
1858
|
-
|
|
1859
|
-
for (const { name: calleeName, bindingId, count } of callees.values()) {
|
|
1860
|
-
const symbols = this.symbols.get(calleeName);
|
|
1861
|
-
if (symbols && symbols.length > 0) {
|
|
1862
|
-
let callee = symbols[0];
|
|
1863
|
-
|
|
1864
|
-
// If we have a binding ID, find the exact matching symbol
|
|
1865
|
-
if (bindingId && symbols.length > 1) {
|
|
1866
|
-
const exactMatch = symbols.find(s => s.bindingId === bindingId);
|
|
1867
|
-
if (exactMatch) {
|
|
1868
|
-
callee = exactMatch;
|
|
1869
|
-
}
|
|
1870
|
-
} else if (symbols.length > 1) {
|
|
1871
|
-
// Priority 1: Same file, but different definition (for overloads)
|
|
1872
|
-
const sameFileDifferent = symbols.find(s => s.file === def.file && s.startLine !== def.startLine);
|
|
1873
|
-
const sameFile = symbols.find(s => s.file === def.file);
|
|
1874
|
-
if (sameFileDifferent && calleeName === def.name) {
|
|
1875
|
-
callee = sameFileDifferent;
|
|
1876
|
-
} else if (sameFile) {
|
|
1877
|
-
callee = sameFile;
|
|
1878
|
-
} else {
|
|
1879
|
-
// Priority 2: Same directory (package)
|
|
1880
|
-
const sameDir = symbols.find(s => path.dirname(s.file) === defDir);
|
|
1881
|
-
if (sameDir) {
|
|
1882
|
-
callee = sameDir;
|
|
1883
|
-
} else if (defReceiver) {
|
|
1884
|
-
// Priority 3: Same receiver type (for methods)
|
|
1885
|
-
const sameReceiver = symbols.find(s => s.receiver === defReceiver);
|
|
1886
|
-
if (sameReceiver) {
|
|
1887
|
-
callee = sameReceiver;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
// Priority 4: If default (symbols[0]) is a test file, prefer non-test
|
|
1892
|
-
if (!bindingId) {
|
|
1893
|
-
const calleeFileEntry = this.files.get(callee.file);
|
|
1894
|
-
if (calleeFileEntry && isTestFile(calleeFileEntry.relativePath, calleeFileEntry.language)) {
|
|
1895
|
-
const nonTest = symbols.find(s => {
|
|
1896
|
-
const fe = this.files.get(s.file);
|
|
1897
|
-
return fe && !isTestFile(fe.relativePath, fe.language);
|
|
1898
|
-
});
|
|
1899
|
-
if (nonTest) callee = nonTest;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
// Skip test-file callees when caller is production code and
|
|
1905
|
-
// there's no binding (import) evidence linking them
|
|
1906
|
-
if (!callerIsTest && !bindingId) {
|
|
1907
|
-
const calleeFileEntry = this.files.get(callee.file);
|
|
1908
|
-
if (calleeFileEntry && isTestFile(calleeFileEntry.relativePath, calleeFileEntry.language)) {
|
|
1909
|
-
continue;
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
result.push({
|
|
1914
|
-
...callee,
|
|
1915
|
-
callCount: count,
|
|
1916
|
-
weight: this.calculateWeight(count)
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
// Sort by call count (core dependencies first)
|
|
1922
|
-
result.sort((a, b) => b.callCount - a.callCount);
|
|
1923
|
-
|
|
1924
|
-
return result;
|
|
1925
|
-
} catch (e) {
|
|
1926
|
-
// Expected: file read/parse failures (minified, binary, buffer exceeded).
|
|
1927
|
-
// Return empty callees rather than crashing the entire query.
|
|
1928
|
-
return [];
|
|
1929
|
-
}
|
|
1930
|
-
} finally { this._endOp(); }
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
/**
|
|
1934
|
-
* Calculate dependency weight based on usage
|
|
1935
|
-
*/
|
|
1936
|
-
calculateWeight(callCount) {
|
|
1937
|
-
if (callCount >= 10) return 'core';
|
|
1938
|
-
if (callCount >= 3) return 'regular';
|
|
1939
|
-
if (callCount === 1) return 'utility';
|
|
1940
|
-
return 'normal';
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
/**
|
|
1944
|
-
* Smart extraction: function + dependencies
|
|
1945
|
-
*/
|
|
1946
|
-
smart(name, options = {}) {
|
|
1947
|
-
this._beginOp();
|
|
1948
|
-
try {
|
|
1949
|
-
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
1950
|
-
if (!def) {
|
|
1951
|
-
return null;
|
|
1952
|
-
}
|
|
1953
|
-
const code = this.extractCode(def);
|
|
1954
|
-
const stats = { uncertain: 0 };
|
|
1955
|
-
const callees = this.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
1956
|
-
|
|
1957
|
-
const filesInScope = new Set([def.file]);
|
|
1958
|
-
callees.forEach(c => filesInScope.add(c.file));
|
|
1959
|
-
let dynamicImports = 0;
|
|
1960
|
-
for (const f of filesInScope) {
|
|
1961
|
-
const fe = this.files.get(f);
|
|
1962
|
-
if (fe?.dynamicImports) dynamicImports += fe.dynamicImports;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
// Extract code for each dependency, excluding the exact same function
|
|
1966
|
-
// (but keeping same-name overloads, e.g. Java toJson(Object) vs toJson(Object, Class))
|
|
1967
|
-
const defBindingId = def.bindingId;
|
|
1968
|
-
const dependencies = callees
|
|
1969
|
-
.filter(callee => callee.bindingId !== defBindingId)
|
|
1970
|
-
.map(callee => ({
|
|
1971
|
-
...callee,
|
|
1972
|
-
code: this.extractCode(callee)
|
|
1973
|
-
}));
|
|
1974
|
-
|
|
1975
|
-
// Find type definitions if requested
|
|
1976
|
-
const types = [];
|
|
1977
|
-
if (options.withTypes) {
|
|
1978
|
-
// Look for type annotations in params/return type
|
|
1979
|
-
const typeNames = this.extractTypeNames(def);
|
|
1980
|
-
for (const typeName of typeNames) {
|
|
1981
|
-
const typeSymbols = this.symbols.get(typeName);
|
|
1982
|
-
if (typeSymbols) {
|
|
1983
|
-
for (const sym of typeSymbols) {
|
|
1984
|
-
if (['type', 'interface', 'class', 'struct'].includes(sym.type)) {
|
|
1985
|
-
types.push({
|
|
1986
|
-
...sym,
|
|
1987
|
-
code: this.extractCode(sym)
|
|
1988
|
-
});
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
return {
|
|
1996
|
-
target: {
|
|
1997
|
-
...def,
|
|
1998
|
-
code
|
|
1999
|
-
},
|
|
2000
|
-
dependencies,
|
|
2001
|
-
types,
|
|
2002
|
-
meta: {
|
|
2003
|
-
complete: stats.uncertain === 0 && dynamicImports === 0,
|
|
2004
|
-
skipped: 0,
|
|
2005
|
-
dynamicImports,
|
|
2006
|
-
uncertain: stats.uncertain
|
|
2007
|
-
}
|
|
2008
|
-
};
|
|
2009
|
-
} finally { this._endOp(); }
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
// ========================================================================
|
|
2013
|
-
// HELPER METHODS
|
|
2014
|
-
// ========================================================================
|
|
1355
|
+
// ========================================================================
|
|
1356
|
+
// HELPER METHODS
|
|
1357
|
+
// ========================================================================
|
|
2015
1358
|
|
|
2016
1359
|
/**
|
|
2017
1360
|
* Get line content from a file
|
|
@@ -2206,34 +1549,8 @@ class ProjectIndex {
|
|
|
2206
1549
|
return returnSymbol ? best : best.name;
|
|
2207
1550
|
}
|
|
2208
1551
|
|
|
2209
|
-
/**
|
|
2210
|
-
|
|
2211
|
-
* Returns Map<attrName, typeName> for a given className.
|
|
2212
|
-
* Caches results per file.
|
|
2213
|
-
*/
|
|
2214
|
-
getInstanceAttributeTypes(filePath, className) {
|
|
2215
|
-
if (!this._attrTypeCache) this._attrTypeCache = new Map();
|
|
2216
|
-
|
|
2217
|
-
let fileCache = this._attrTypeCache.get(filePath);
|
|
2218
|
-
if (!fileCache) {
|
|
2219
|
-
const fileEntry = this.files.get(filePath);
|
|
2220
|
-
if (!fileEntry || fileEntry.language !== 'python') return null;
|
|
2221
|
-
|
|
2222
|
-
const langModule = getLanguageModule('python');
|
|
2223
|
-
if (!langModule?.findInstanceAttributeTypes) return null;
|
|
2224
|
-
|
|
2225
|
-
try {
|
|
2226
|
-
const content = this._readFile(filePath);
|
|
2227
|
-
const parser = getParser('python');
|
|
2228
|
-
fileCache = langModule.findInstanceAttributeTypes(content, parser);
|
|
2229
|
-
this._attrTypeCache.set(filePath, fileCache);
|
|
2230
|
-
} catch {
|
|
2231
|
-
return null;
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
return fileCache.get(className) || null;
|
|
2236
|
-
}
|
|
1552
|
+
/** Get instance attribute types for a class in a file */
|
|
1553
|
+
getInstanceAttributeTypes(filePath, className) { return callersModule.getInstanceAttributeTypes(this, filePath, className); }
|
|
2237
1554
|
|
|
2238
1555
|
/**
|
|
2239
1556
|
* Extract type names from a function definition
|
|
@@ -2284,16 +1601,30 @@ class ProjectIndex {
|
|
|
2284
1601
|
const content = this._readFile(normalizedPath);
|
|
2285
1602
|
const { imports: rawImports } = extractImports(content, fileEntry.language);
|
|
2286
1603
|
|
|
1604
|
+
const contentLines = content.split('\n');
|
|
1605
|
+
|
|
2287
1606
|
return rawImports.map(imp => {
|
|
1607
|
+
// Skip imports with null module (e.g. Rust include! with dynamic path)
|
|
1608
|
+
if (!imp.module) {
|
|
1609
|
+
return {
|
|
1610
|
+
module: null,
|
|
1611
|
+
names: imp.names,
|
|
1612
|
+
type: imp.type,
|
|
1613
|
+
resolved: null,
|
|
1614
|
+
isExternal: false,
|
|
1615
|
+
isDynamic: true,
|
|
1616
|
+
line: null
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
2288
1620
|
// Dynamic imports with variable path (e.g. require(varName), import(varExpr)) can't be resolved.
|
|
2289
1621
|
// Only JS/TS require()/import() with dynamic=true has unresolvable paths.
|
|
2290
1622
|
// Go side-effect/dot imports and Rust glob uses also set dynamic=true but have valid module paths.
|
|
2291
1623
|
const isUnresolvableDynamic = imp.dynamic && (imp.type === 'require' || imp.type === 'dynamic');
|
|
2292
1624
|
if (isUnresolvableDynamic) {
|
|
2293
|
-
const lines = content.split('\n');
|
|
2294
1625
|
let line = null;
|
|
2295
|
-
for (let i = 0; i <
|
|
2296
|
-
if (
|
|
1626
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
1627
|
+
if (contentLines[i].includes(imp.module || 'require')) {
|
|
2297
1628
|
line = i + 1;
|
|
2298
1629
|
break;
|
|
2299
1630
|
}
|
|
@@ -2322,10 +1653,9 @@ class ProjectIndex {
|
|
|
2322
1653
|
}
|
|
2323
1654
|
|
|
2324
1655
|
// Find line number of import
|
|
2325
|
-
const lines = content.split('\n');
|
|
2326
1656
|
let line = null;
|
|
2327
|
-
for (let i = 0; i <
|
|
2328
|
-
if (
|
|
1657
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
1658
|
+
if (contentLines[i].includes(imp.module)) {
|
|
2329
1659
|
line = i + 1;
|
|
2330
1660
|
break;
|
|
2331
1661
|
}
|
|
@@ -2616,11 +1946,15 @@ class ProjectIndex {
|
|
|
2616
1946
|
* @param {string} filePath - File path
|
|
2617
1947
|
* @returns {Array} Exported symbols from that file
|
|
2618
1948
|
*/
|
|
2619
|
-
fileExports(filePath) {
|
|
1949
|
+
fileExports(filePath, _visited) {
|
|
2620
1950
|
const resolved = this.resolveFilePathForQuery(filePath);
|
|
2621
1951
|
if (typeof resolved !== 'string') return resolved;
|
|
2622
1952
|
|
|
2623
1953
|
const absPath = resolved;
|
|
1954
|
+
const visited = _visited || new Set();
|
|
1955
|
+
if (visited.has(absPath)) return [];
|
|
1956
|
+
visited.add(absPath);
|
|
1957
|
+
|
|
2624
1958
|
const fileEntry = this.files.get(absPath);
|
|
2625
1959
|
if (!fileEntry) {
|
|
2626
1960
|
return [];
|
|
@@ -2683,7 +2017,7 @@ class ProjectIndex {
|
|
|
2683
2017
|
if (sourceEntry) {
|
|
2684
2018
|
// For star re-exports, include all exported symbols from source
|
|
2685
2019
|
if (exp.type === 're-export-all') {
|
|
2686
|
-
const sourceExports = this.fileExports(resolved);
|
|
2020
|
+
const sourceExports = this.fileExports(resolved, visited);
|
|
2687
2021
|
for (const srcExp of sourceExports) {
|
|
2688
2022
|
if (!matchedNames.has(srcExp.name)) {
|
|
2689
2023
|
matchedNames.add(srcExp.name);
|
|
@@ -2731,396 +2065,64 @@ class ProjectIndex {
|
|
|
2731
2065
|
return results;
|
|
2732
2066
|
}
|
|
2733
2067
|
|
|
2734
|
-
/**
|
|
2735
|
-
|
|
2736
|
-
* @param {string} name - Function name
|
|
2737
|
-
* @returns {Array} Callback usages
|
|
2738
|
-
*/
|
|
2739
|
-
findCallbackUsages(name) {
|
|
2740
|
-
const usages = [];
|
|
2068
|
+
/** Check if a function is used as a callback anywhere in the codebase */
|
|
2069
|
+
findCallbackUsages(name) { return callersModule.findCallbackUsages(this, name); }
|
|
2741
2070
|
|
|
2742
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
2743
|
-
try {
|
|
2744
|
-
const content = this._readFile(filePath);
|
|
2745
|
-
const language = detectLanguage(filePath);
|
|
2746
|
-
if (!language) continue;
|
|
2747
2071
|
|
|
2748
|
-
|
|
2749
|
-
|
|
2072
|
+
/** Build a usage index for all identifiers in the codebase (optimized for deadcode) */
|
|
2073
|
+
buildUsageIndex() { return deadcodeModule.buildUsageIndex(this); }
|
|
2750
2074
|
|
|
2751
|
-
|
|
2752
|
-
|
|
2075
|
+
/** Find dead code (unused functions/classes) */
|
|
2076
|
+
deadcode(options) { return deadcodeModule.deadcode(this, options); }
|
|
2753
2077
|
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2078
|
+
/**
|
|
2079
|
+
* Get dependency graph for a file
|
|
2080
|
+
* @param {string} filePath - Starting file
|
|
2081
|
+
* @param {object} options - { direction: 'imports' | 'importers' | 'both', maxDepth }
|
|
2082
|
+
* @returns {object} - Graph structure with root, nodes, edges
|
|
2083
|
+
*/
|
|
2084
|
+
graph(filePath, options = {}) {
|
|
2085
|
+
const direction = options.direction || 'both';
|
|
2086
|
+
// Sanitize depth: use default for null/undefined, clamp negative to 0
|
|
2087
|
+
const rawDepth = options.maxDepth ?? 5;
|
|
2088
|
+
const maxDepth = Math.max(0, rawDepth);
|
|
2765
2089
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2090
|
+
const resolved = this.resolveFilePathForQuery(filePath);
|
|
2091
|
+
if (typeof resolved !== 'string') return resolved;
|
|
2768
2092
|
|
|
2093
|
+
const targetPath = resolved;
|
|
2769
2094
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
*/
|
|
2775
|
-
buildUsageIndex() {
|
|
2776
|
-
const usageIndex = new Map(); // name -> [{file, line}]
|
|
2095
|
+
const buildSubgraph = (dir) => {
|
|
2096
|
+
const visited = new Set();
|
|
2097
|
+
const nodes = [];
|
|
2098
|
+
const edges = [];
|
|
2777
2099
|
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
if (!language) continue;
|
|
2100
|
+
const traverse = (file, depth) => {
|
|
2101
|
+
if (visited.has(file)) return;
|
|
2102
|
+
visited.add(file);
|
|
2782
2103
|
|
|
2783
|
-
const
|
|
2104
|
+
const fileEntry = this.files.get(file);
|
|
2105
|
+
const relPath = fileEntry ? fileEntry.relativePath : path.relative(this.root, file);
|
|
2106
|
+
nodes.push({ file, relativePath: relPath, depth });
|
|
2784
2107
|
|
|
2785
|
-
//
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
const jsParser = getParser('javascript');
|
|
2792
|
-
const blocks = htmlModule.extractScriptBlocks(content, htmlParser);
|
|
2793
|
-
if (blocks.length === 0 && !htmlModule.extractEventHandlerCalls) continue;
|
|
2794
|
-
if (blocks.length > 0) {
|
|
2795
|
-
const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
|
|
2796
|
-
tree = safeParse(jsParser, virtualJS);
|
|
2797
|
-
}
|
|
2108
|
+
// Stop traversal at max depth but still register the node above
|
|
2109
|
+
if (depth >= maxDepth) return;
|
|
2110
|
+
|
|
2111
|
+
let neighbors = [];
|
|
2112
|
+
if (dir === 'imports') {
|
|
2113
|
+
neighbors = this.importGraph.get(file) || [];
|
|
2798
2114
|
} else {
|
|
2799
|
-
|
|
2800
|
-
if (!parser) continue;
|
|
2801
|
-
tree = safeParse(parser, content);
|
|
2115
|
+
neighbors = this.exportGraph.get(file) || [];
|
|
2802
2116
|
}
|
|
2803
2117
|
|
|
2804
|
-
//
|
|
2805
|
-
const
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
node.type === 'shorthand_property_identifier' ||
|
|
2811
|
-
node.type === 'shorthand_property_identifier_pattern' ||
|
|
2812
|
-
node.type === 'field_identifier') {
|
|
2813
|
-
const name = node.text;
|
|
2814
|
-
if (!usageIndex.has(name)) {
|
|
2815
|
-
usageIndex.set(name, []);
|
|
2816
|
-
}
|
|
2817
|
-
usageIndex.get(name).push({
|
|
2818
|
-
file: filePath,
|
|
2819
|
-
line: node.startPosition.row + 1,
|
|
2820
|
-
relativePath: fileEntry.relativePath
|
|
2821
|
-
});
|
|
2822
|
-
}
|
|
2823
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
2824
|
-
traverse(node.child(i));
|
|
2825
|
-
}
|
|
2826
|
-
};
|
|
2827
|
-
if (tree) traverse(tree.rootNode);
|
|
2828
|
-
|
|
2829
|
-
// For HTML files, also extract identifiers from event handler attributes
|
|
2830
|
-
// (onclick="foo()" etc. — these are in HTML, not in <script> blocks)
|
|
2831
|
-
if (language === 'html') {
|
|
2832
|
-
const htmlModule = getLanguageModule('html');
|
|
2833
|
-
const htmlParser = getParser('html');
|
|
2834
|
-
const handlerCalls = htmlModule.extractEventHandlerCalls(content, htmlParser);
|
|
2835
|
-
for (const call of handlerCalls) {
|
|
2836
|
-
if (!usageIndex.has(call.name)) {
|
|
2837
|
-
usageIndex.set(call.name, []);
|
|
2838
|
-
}
|
|
2839
|
-
usageIndex.get(call.name).push({
|
|
2840
|
-
file: filePath,
|
|
2841
|
-
line: call.line,
|
|
2842
|
-
relativePath: fileEntry.relativePath
|
|
2843
|
-
});
|
|
2844
|
-
}
|
|
2118
|
+
// Deduplicate neighbors (same file may be imported multiple times, e.g. Java inner classes)
|
|
2119
|
+
const uniqueNeighbors = [...new Set(neighbors)];
|
|
2120
|
+
|
|
2121
|
+
for (const neighbor of uniqueNeighbors) {
|
|
2122
|
+
edges.push({ from: file, to: neighbor });
|
|
2123
|
+
traverse(neighbor, depth + 1);
|
|
2845
2124
|
}
|
|
2846
|
-
}
|
|
2847
|
-
// Skip files that can't be processed
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
return usageIndex;
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
/**
|
|
2855
|
-
* Find dead code (unused functions/classes)
|
|
2856
|
-
* @param {object} options - { includeExported, includeTests }
|
|
2857
|
-
* @returns {Array} Unused symbols
|
|
2858
|
-
*/
|
|
2859
|
-
deadcode(options = {}) {
|
|
2860
|
-
this._beginOp();
|
|
2861
|
-
try {
|
|
2862
|
-
const results = [];
|
|
2863
|
-
let excludedDecorated = 0;
|
|
2864
|
-
let excludedExported = 0;
|
|
2865
|
-
|
|
2866
|
-
// Build usage index once (instead of per-symbol)
|
|
2867
|
-
const usageIndex = this.buildUsageIndex();
|
|
2868
|
-
|
|
2869
|
-
for (const [name, symbols] of this.symbols) {
|
|
2870
|
-
for (const symbol of symbols) {
|
|
2871
|
-
// Skip non-function/class types
|
|
2872
|
-
// Include various method types from different languages:
|
|
2873
|
-
// - function: standalone functions
|
|
2874
|
-
// - class, struct, interface: type definitions (skip them in deadcode)
|
|
2875
|
-
// - method: class methods
|
|
2876
|
-
// - static, public, abstract: Java method modifiers used as types
|
|
2877
|
-
// - constructor: constructors
|
|
2878
|
-
const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
|
|
2879
|
-
if (!callableTypes.includes(symbol.type)) {
|
|
2880
|
-
continue;
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
const fileEntry = this.files.get(symbol.file);
|
|
2884
|
-
const lang = fileEntry?.language;
|
|
2885
|
-
|
|
2886
|
-
// Skip bundled/minified files (webpack bundles, build artifacts)
|
|
2887
|
-
if (fileEntry?.isBundled) {
|
|
2888
|
-
continue;
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
// Skip test files unless requested
|
|
2892
|
-
if (!options.includeTests && isTestFile(symbol.relativePath, lang)) {
|
|
2893
|
-
continue;
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
// Apply exclude and in filters
|
|
2897
|
-
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
2898
|
-
if (!this.matchesFilters(symbol.relativePath, { exclude: options.exclude, in: options.in })) {
|
|
2899
|
-
continue;
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
const mods = symbol.modifiers || [];
|
|
2904
|
-
|
|
2905
|
-
// Language-specific entry points (called by runtime, no AST-visible callers)
|
|
2906
|
-
// Go: main() and init() are called by runtime
|
|
2907
|
-
const isGoEntryPoint = lang === 'go' && (name === 'main' || name === 'init');
|
|
2908
|
-
|
|
2909
|
-
// Java: public static void main(String[] args) is the entry point
|
|
2910
|
-
const isJavaEntryPoint = lang === 'java' && name === 'main' &&
|
|
2911
|
-
mods.includes('public') && mods.includes('static');
|
|
2912
|
-
|
|
2913
|
-
// Python: Magic/dunder methods are called by the interpreter, not user code
|
|
2914
|
-
// test_* functions/methods are called by pytest/unittest via reflection
|
|
2915
|
-
// setUp/tearDown are unittest.TestCase framework methods called by test runner
|
|
2916
|
-
// pytest_* are pytest plugin hooks called by the framework
|
|
2917
|
-
const isPythonEntryPoint = lang === 'python' &&
|
|
2918
|
-
(/^__\w+__$/.test(name) || /^test_/.test(name) ||
|
|
2919
|
-
/^(setUp|tearDown)(Class|Module)?$/.test(name) ||
|
|
2920
|
-
/^pytest_/.test(name));
|
|
2921
|
-
|
|
2922
|
-
// Rust: main() is entry point, #[test] and #[bench] functions are called by test/bench runner
|
|
2923
|
-
const isRustEntryPoint = lang === 'rust' &&
|
|
2924
|
-
(name === 'main' || mods.includes('test') || mods.includes('bench'));
|
|
2925
|
-
|
|
2926
|
-
// Rust: trait impl methods are invoked via trait dispatch, not direct calls
|
|
2927
|
-
// They can never be "dead" - the trait contract requires them to exist
|
|
2928
|
-
// className for trait impls contains " for " (e.g., "PartialEq for Glob")
|
|
2929
|
-
const isRustTraitImpl = lang === 'rust' && symbol.isMethod &&
|
|
2930
|
-
symbol.className && symbol.className.includes(' for ');
|
|
2931
|
-
|
|
2932
|
-
// Go: Test*, Benchmark*, Example* functions are called by go test
|
|
2933
|
-
const isGoTestFunc = lang === 'go' &&
|
|
2934
|
-
/^(Test|Benchmark|Example)[A-Z]/.test(name);
|
|
2935
|
-
|
|
2936
|
-
// Java: @Test annotated methods are called by JUnit
|
|
2937
|
-
const isJavaTestMethod = lang === 'java' && mods.includes('test');
|
|
2938
|
-
|
|
2939
|
-
// Java: @Override methods are invoked via polymorphic dispatch
|
|
2940
|
-
// They implement interface/superclass contracts and can't be dead
|
|
2941
|
-
const isJavaOverride = lang === 'java' && mods.includes('override');
|
|
2942
|
-
|
|
2943
|
-
// Skip trait impl / @Override methods entirely - they're required by the type system
|
|
2944
|
-
if (isRustTraitImpl || isJavaOverride) {
|
|
2945
|
-
continue;
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
// JavaScript/TypeScript: framework lifecycle methods called by runtime
|
|
2949
|
-
// React class components, Web Components, Angular, Vue
|
|
2950
|
-
const jsLifecycleMethods = new Set([
|
|
2951
|
-
// React class component lifecycle
|
|
2952
|
-
'render', 'componentDidMount', 'componentDidUpdate', 'componentWillUnmount',
|
|
2953
|
-
'getDerivedStateFromProps', 'getDerivedStateFromError', 'componentDidCatch',
|
|
2954
|
-
'getSnapshotBeforeUpdate', 'shouldComponentUpdate',
|
|
2955
|
-
// Web Components lifecycle
|
|
2956
|
-
'connectedCallback', 'disconnectedCallback', 'attributeChangedCallback', 'adoptedCallback'
|
|
2957
|
-
]);
|
|
2958
|
-
const isJsEntryPoint = (lang === 'javascript' || lang === 'typescript' || lang === 'tsx') &&
|
|
2959
|
-
symbol.isMethod && jsLifecycleMethods.has(name);
|
|
2960
|
-
|
|
2961
|
-
const isEntryPoint = isGoEntryPoint || isGoTestFunc ||
|
|
2962
|
-
isJavaEntryPoint || isJavaTestMethod ||
|
|
2963
|
-
isPythonEntryPoint || isRustEntryPoint || isJsEntryPoint;
|
|
2964
|
-
|
|
2965
|
-
// Entry points are always excluded — they're invoked by the runtime, not user code
|
|
2966
|
-
if (isEntryPoint) {
|
|
2967
|
-
continue;
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
|
-
// Framework registration decorators — excluded by default to reduce noise
|
|
2971
|
-
// Python: decorators with '.' (attribute access) like @router.get, @app.route, @celery.task
|
|
2972
|
-
// Java: non-standard annotations like @Bean, @Scheduled, @GetMapping
|
|
2973
|
-
// These functions are invoked by frameworks, not by user code — AST can't see the call path
|
|
2974
|
-
const javaKeywords = new Set(['public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native', 'default']);
|
|
2975
|
-
const hasRegistrationDecorator = (() => {
|
|
2976
|
-
if (lang === 'python') {
|
|
2977
|
-
const decorators = symbol.decorators || [];
|
|
2978
|
-
return decorators.some(d => d.includes('.'));
|
|
2979
|
-
}
|
|
2980
|
-
if (lang === 'java') {
|
|
2981
|
-
return mods.some(m => !javaKeywords.has(m));
|
|
2982
|
-
}
|
|
2983
|
-
return false;
|
|
2984
|
-
})();
|
|
2985
|
-
|
|
2986
|
-
if (hasRegistrationDecorator && !options.includeDecorated) {
|
|
2987
|
-
excludedDecorated++;
|
|
2988
|
-
continue;
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
const isExported = fileEntry && (
|
|
2992
|
-
fileEntry.exports.includes(name) ||
|
|
2993
|
-
mods.includes('export') ||
|
|
2994
|
-
mods.includes('public') ||
|
|
2995
|
-
(lang === 'go' && /^[A-Z]/.test(name))
|
|
2996
|
-
);
|
|
2997
|
-
|
|
2998
|
-
// Skip exported unless requested
|
|
2999
|
-
if (isExported && !options.includeExported) {
|
|
3000
|
-
excludedExported++;
|
|
3001
|
-
continue;
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
// Use pre-built index for O(1) lookup instead of O(files) scan
|
|
3005
|
-
const allUsages = usageIndex.get(name) || [];
|
|
3006
|
-
|
|
3007
|
-
// Filter out usages that are at the definition location
|
|
3008
|
-
// nameLine: when decorators/annotations are present, startLine is the decorator line
|
|
3009
|
-
// but the name identifier is on a different line (nameLine). Check both.
|
|
3010
|
-
let nonDefUsages = allUsages.filter(u =>
|
|
3011
|
-
!(u.file === symbol.file && (u.line === symbol.startLine || u.line === symbol.nameLine))
|
|
3012
|
-
);
|
|
3013
|
-
|
|
3014
|
-
// For exported symbols in --include-exported mode, also filter out export-site
|
|
3015
|
-
// references (e.g., `module.exports = { helperC }` or `export { helperC }`).
|
|
3016
|
-
// These are just re-statements of the export, not actual consumption.
|
|
3017
|
-
if (isExported && options.includeExported) {
|
|
3018
|
-
nonDefUsages = nonDefUsages.filter(u => {
|
|
3019
|
-
if (u.file !== symbol.file) return true; // cross-file usage always counts
|
|
3020
|
-
// Check if same-file usage is on an export line
|
|
3021
|
-
const content = this._readFile(u.file);
|
|
3022
|
-
if (!content) return true;
|
|
3023
|
-
const lines = content.split('\n');
|
|
3024
|
-
const line = lines[u.line - 1] || '';
|
|
3025
|
-
const trimmed = line.trim();
|
|
3026
|
-
// CJS: module.exports = { ... } or exports.name = ...
|
|
3027
|
-
if (trimmed.startsWith('module.exports') || /^exports\.\w+\s*=/.test(trimmed)) return false;
|
|
3028
|
-
// ESM: export { ... } or export default
|
|
3029
|
-
if (/^export\s*\{/.test(trimmed) || /^export\s+default\s/.test(trimmed)) return false;
|
|
3030
|
-
return true;
|
|
3031
|
-
});
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
// Total includes all usage types (calls, references, callbacks, re-exports)
|
|
3035
|
-
const totalUsages = nonDefUsages.length;
|
|
3036
|
-
|
|
3037
|
-
if (totalUsages === 0) {
|
|
3038
|
-
// Collect decorators/annotations for hint display
|
|
3039
|
-
// Python: symbol.decorators (e.g., ['app.route("/path")', 'login_required'])
|
|
3040
|
-
// Java/Rust/Go: symbol.modifiers may contain annotations (e.g., 'bean', 'scheduled')
|
|
3041
|
-
const decorators = symbol.decorators || [];
|
|
3042
|
-
// For Java, extract annotation-like modifiers (javaKeywords defined above)
|
|
3043
|
-
const annotations = lang === 'java'
|
|
3044
|
-
? mods.filter(m => !javaKeywords.has(m))
|
|
3045
|
-
: [];
|
|
3046
|
-
|
|
3047
|
-
results.push({
|
|
3048
|
-
name: symbol.name,
|
|
3049
|
-
type: symbol.type,
|
|
3050
|
-
file: symbol.relativePath,
|
|
3051
|
-
startLine: symbol.startLine,
|
|
3052
|
-
endLine: symbol.endLine,
|
|
3053
|
-
isExported,
|
|
3054
|
-
usageCount: 0,
|
|
3055
|
-
...(decorators.length > 0 && { decorators }),
|
|
3056
|
-
...(annotations.length > 0 && { annotations })
|
|
3057
|
-
});
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
// Sort by file then line
|
|
3063
|
-
results.sort((a, b) => {
|
|
3064
|
-
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
3065
|
-
return a.startLine - b.startLine;
|
|
3066
|
-
});
|
|
3067
|
-
|
|
3068
|
-
// Attach exclusion counts as array properties (backwards-compatible)
|
|
3069
|
-
results.excludedDecorated = excludedDecorated;
|
|
3070
|
-
results.excludedExported = excludedExported;
|
|
3071
|
-
|
|
3072
|
-
return results;
|
|
3073
|
-
} finally { this._endOp(); }
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
/**
|
|
3077
|
-
* Get dependency graph for a file
|
|
3078
|
-
* @param {string} filePath - Starting file
|
|
3079
|
-
* @param {object} options - { direction: 'imports' | 'importers' | 'both', maxDepth }
|
|
3080
|
-
* @returns {object} - Graph structure with root, nodes, edges
|
|
3081
|
-
*/
|
|
3082
|
-
graph(filePath, options = {}) {
|
|
3083
|
-
const direction = options.direction || 'both';
|
|
3084
|
-
// Sanitize depth: use default for null/undefined, clamp negative to 0
|
|
3085
|
-
const rawDepth = options.maxDepth ?? 5;
|
|
3086
|
-
const maxDepth = Math.max(0, rawDepth);
|
|
3087
|
-
|
|
3088
|
-
const resolved = this.resolveFilePathForQuery(filePath);
|
|
3089
|
-
if (typeof resolved !== 'string') return resolved;
|
|
3090
|
-
|
|
3091
|
-
const targetPath = resolved;
|
|
3092
|
-
|
|
3093
|
-
const buildSubgraph = (dir) => {
|
|
3094
|
-
const visited = new Set();
|
|
3095
|
-
const nodes = [];
|
|
3096
|
-
const edges = [];
|
|
3097
|
-
|
|
3098
|
-
const traverse = (file, depth) => {
|
|
3099
|
-
if (visited.has(file)) return;
|
|
3100
|
-
visited.add(file);
|
|
3101
|
-
|
|
3102
|
-
const fileEntry = this.files.get(file);
|
|
3103
|
-
const relPath = fileEntry ? fileEntry.relativePath : path.relative(this.root, file);
|
|
3104
|
-
nodes.push({ file, relativePath: relPath, depth });
|
|
3105
|
-
|
|
3106
|
-
// Stop traversal at max depth but still register the node above
|
|
3107
|
-
if (depth >= maxDepth) return;
|
|
3108
|
-
|
|
3109
|
-
let neighbors = [];
|
|
3110
|
-
if (dir === 'imports') {
|
|
3111
|
-
neighbors = this.importGraph.get(file) || [];
|
|
3112
|
-
} else {
|
|
3113
|
-
neighbors = this.exportGraph.get(file) || [];
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
// Deduplicate neighbors (same file may be imported multiple times, e.g. Java inner classes)
|
|
3117
|
-
const uniqueNeighbors = [...new Set(neighbors)];
|
|
3118
|
-
|
|
3119
|
-
for (const neighbor of uniqueNeighbors) {
|
|
3120
|
-
edges.push({ from: file, to: neighbor });
|
|
3121
|
-
traverse(neighbor, depth + 1);
|
|
3122
|
-
}
|
|
3123
|
-
};
|
|
2125
|
+
};
|
|
3124
2126
|
|
|
3125
2127
|
traverse(targetPath, 0);
|
|
3126
2128
|
return { nodes, edges };
|
|
@@ -3257,7 +2259,7 @@ class ProjectIndex {
|
|
|
3257
2259
|
const fileEntry = this.files.get(def.file);
|
|
3258
2260
|
if (fileEntry) {
|
|
3259
2261
|
for (const sym of fileEntry.symbols) {
|
|
3260
|
-
if (sym.name !== name && sym.type
|
|
2262
|
+
if (sym.name !== name && !NON_CALLABLE_TYPES.has(sym.type)) {
|
|
3261
2263
|
related.sameFile.push({
|
|
3262
2264
|
name: sym.name,
|
|
3263
2265
|
line: sym.startLine,
|
|
@@ -3586,737 +2588,43 @@ class ProjectIndex {
|
|
|
3586
2588
|
} finally { this._endOp(); }
|
|
3587
2589
|
}
|
|
3588
2590
|
|
|
3589
|
-
/**
|
|
3590
|
-
|
|
3591
|
-
* @param {string} name - Function name
|
|
3592
|
-
* @param {object} options - { addParam, removeParam, renameTo, defaultValue }
|
|
3593
|
-
* @returns {object} Plan with before/after signatures and affected call sites
|
|
3594
|
-
*/
|
|
3595
|
-
plan(name, options = {}) {
|
|
3596
|
-
this._beginOp();
|
|
3597
|
-
try {
|
|
3598
|
-
const definitions = this.symbols.get(name);
|
|
3599
|
-
if (!definitions || definitions.length === 0) {
|
|
3600
|
-
return { found: false, function: name };
|
|
3601
|
-
}
|
|
3602
|
-
|
|
3603
|
-
const resolved = this.resolveSymbol(name, { file: options.file });
|
|
3604
|
-
const def = resolved.def || definitions[0];
|
|
3605
|
-
const impact = this.impact(name, { file: options.file });
|
|
3606
|
-
const currentParams = def.paramsStructured || [];
|
|
3607
|
-
const currentSignature = this.formatSignature(def);
|
|
3608
|
-
|
|
3609
|
-
let newParams = [...currentParams];
|
|
3610
|
-
let newSignature = currentSignature;
|
|
3611
|
-
let operation = null;
|
|
3612
|
-
let changes = [];
|
|
3613
|
-
|
|
3614
|
-
if (options.addParam) {
|
|
3615
|
-
operation = 'add-param';
|
|
3616
|
-
const newParam = {
|
|
3617
|
-
name: options.addParam,
|
|
3618
|
-
...(options.defaultValue && { default: options.defaultValue })
|
|
3619
|
-
};
|
|
3620
|
-
newParams.push(newParam);
|
|
3621
|
-
|
|
3622
|
-
// Generate new signature
|
|
3623
|
-
const paramsList = newParams.map(p => {
|
|
3624
|
-
let str = p.name;
|
|
3625
|
-
if (p.type) str += `: ${p.type}`;
|
|
3626
|
-
if (p.default) str += ` = ${p.default}`;
|
|
3627
|
-
return str;
|
|
3628
|
-
}).join(', ');
|
|
3629
|
-
newSignature = `${name}(${paramsList})`;
|
|
3630
|
-
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
3631
|
-
|
|
3632
|
-
// Describe changes needed at each call site
|
|
3633
|
-
for (const fileGroup of impact.byFile) {
|
|
3634
|
-
for (const site of fileGroup.sites) {
|
|
3635
|
-
const suggestion = options.defaultValue
|
|
3636
|
-
? `No change needed (has default value)`
|
|
3637
|
-
: `Add argument: ${options.addParam}`;
|
|
3638
|
-
changes.push({
|
|
3639
|
-
file: site.file,
|
|
3640
|
-
line: site.line,
|
|
3641
|
-
expression: site.expression,
|
|
3642
|
-
suggestion,
|
|
3643
|
-
args: site.args
|
|
3644
|
-
});
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
if (options.removeParam) {
|
|
3650
|
-
operation = 'remove-param';
|
|
3651
|
-
const paramIndex = currentParams.findIndex(p => p.name === options.removeParam);
|
|
3652
|
-
if (paramIndex === -1) {
|
|
3653
|
-
return {
|
|
3654
|
-
found: true,
|
|
3655
|
-
error: `Parameter "${options.removeParam}" not found in ${name}`,
|
|
3656
|
-
currentParams: currentParams.map(p => p.name)
|
|
3657
|
-
};
|
|
3658
|
-
}
|
|
3659
|
-
|
|
3660
|
-
newParams = currentParams.filter(p => p.name !== options.removeParam);
|
|
3661
|
-
|
|
3662
|
-
// Generate new signature
|
|
3663
|
-
const paramsList = newParams.map(p => {
|
|
3664
|
-
let str = p.name;
|
|
3665
|
-
if (p.type) str += `: ${p.type}`;
|
|
3666
|
-
if (p.default) str += ` = ${p.default}`;
|
|
3667
|
-
return str;
|
|
3668
|
-
}).join(', ');
|
|
3669
|
-
newSignature = `${name}(${paramsList})`;
|
|
3670
|
-
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
3671
|
-
|
|
3672
|
-
// For Python/Rust methods, self/cls/&self/&mut self is in paramsStructured
|
|
3673
|
-
// but callers don't pass it. Adjust paramIndex to caller-side position.
|
|
3674
|
-
const fileEntry = this.files.get(def.file);
|
|
3675
|
-
const lang = fileEntry?.language;
|
|
3676
|
-
let selfOffset = 0;
|
|
3677
|
-
if ((lang === 'python' || lang === 'rust') && currentParams.length > 0) {
|
|
3678
|
-
const firstName = currentParams[0].name;
|
|
3679
|
-
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
3680
|
-
selfOffset = 1;
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
const callerArgIndex = paramIndex - selfOffset;
|
|
3684
|
-
|
|
3685
|
-
// Describe changes at each call site
|
|
3686
|
-
for (const fileGroup of impact.byFile) {
|
|
3687
|
-
for (const site of fileGroup.sites) {
|
|
3688
|
-
if (site.args && site.argCount > callerArgIndex) {
|
|
3689
|
-
changes.push({
|
|
3690
|
-
file: site.file,
|
|
3691
|
-
line: site.line,
|
|
3692
|
-
expression: site.expression,
|
|
3693
|
-
suggestion: `Remove argument ${callerArgIndex + 1}: ${site.args[callerArgIndex] || '?'}`,
|
|
3694
|
-
args: site.args
|
|
3695
|
-
});
|
|
3696
|
-
}
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
|
|
3701
|
-
if (options.renameTo) {
|
|
3702
|
-
operation = 'rename';
|
|
3703
|
-
newSignature = currentSignature.replace(name, options.renameTo);
|
|
3704
|
-
|
|
3705
|
-
// All call sites need renaming
|
|
3706
|
-
for (const fileGroup of impact.byFile) {
|
|
3707
|
-
for (const site of fileGroup.sites) {
|
|
3708
|
-
const newExpression = site.expression.replace(
|
|
3709
|
-
new RegExp('\\b' + escapeRegExp(name) + '\\b'),
|
|
3710
|
-
options.renameTo
|
|
3711
|
-
);
|
|
3712
|
-
changes.push({
|
|
3713
|
-
file: site.file,
|
|
3714
|
-
line: site.line,
|
|
3715
|
-
expression: site.expression,
|
|
3716
|
-
suggestion: `Rename to: ${newExpression}`,
|
|
3717
|
-
newExpression
|
|
3718
|
-
});
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
|
|
3723
|
-
return {
|
|
3724
|
-
found: true,
|
|
3725
|
-
function: name,
|
|
3726
|
-
file: def.relativePath,
|
|
3727
|
-
startLine: def.startLine,
|
|
3728
|
-
operation,
|
|
3729
|
-
before: {
|
|
3730
|
-
signature: currentSignature,
|
|
3731
|
-
params: currentParams.map(p => p.name)
|
|
3732
|
-
},
|
|
3733
|
-
after: {
|
|
3734
|
-
signature: newSignature,
|
|
3735
|
-
params: newParams.map(p => p.name)
|
|
3736
|
-
},
|
|
3737
|
-
totalChanges: changes.length,
|
|
3738
|
-
filesAffected: new Set(changes.map(c => c.file)).size,
|
|
3739
|
-
changes
|
|
3740
|
-
};
|
|
3741
|
-
} finally { this._endOp(); }
|
|
3742
|
-
}
|
|
2591
|
+
/** Plan a refactoring operation */
|
|
2592
|
+
plan(name, options) { return verifyModule.plan(this, name, options); }
|
|
3743
2593
|
|
|
3744
|
-
/**
|
|
3745
|
-
* Parse a stack trace and show code for each frame
|
|
3746
|
-
* @param {string} stackText - Stack trace text
|
|
3747
|
-
* @returns {object} Parsed frames with code context
|
|
3748
|
-
*/
|
|
2594
|
+
/** Parse a stack trace and show code for each frame */
|
|
3749
2595
|
parseStackTrace(stackText) {
|
|
3750
|
-
|
|
3751
|
-
const lines = stackText.split(/\\n|\n/);
|
|
3752
|
-
|
|
3753
|
-
// Stack trace patterns for different languages/runtimes
|
|
3754
|
-
// Order matters - more specific patterns first
|
|
3755
|
-
const patterns = [
|
|
3756
|
-
// JavaScript Node.js: "at functionName (file.js:line:col)" or "at file.js:line:col"
|
|
3757
|
-
{ regex: /at\s+(?:async\s+)?(?:(.+?)\s+\()?([^():]+):(\d+)(?::(\d+))?\)?/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
3758
|
-
// Deno: "at functionName (file:///path/to/file.ts:line:col)"
|
|
3759
|
-
{ regex: /at\s+(?:async\s+)?(?:(.+?)\s+\()?file:\/\/([^:]+):(\d+)(?::(\d+))?\)?/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
3760
|
-
// Bun: "at functionName (file.js:line:col)" - similar to Node but may have different formatting
|
|
3761
|
-
{ regex: /^\s+at\s+(.+?)\s+\[as\s+\w+\]\s+\(([^:]+):(\d+):(\d+)\)/, extract: (m) => ({ funcName: m[1], file: m[2], line: parseInt(m[3]), col: parseInt(m[4]) }) },
|
|
3762
|
-
// Browser Chrome/V8: "at functionName (http://... or file:// ...)"
|
|
3763
|
-
{ regex: /at\s+(?:async\s+)?(?:(.+?)\s+\()?(?:https?:\/\/[^/]+)?([^():]+):(\d+)(?::(\d+))?\)?/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
3764
|
-
// Firefox: "functionName@file:line:col"
|
|
3765
|
-
{ regex: /^(.+)@(.+):(\d+):(\d+)$/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: parseInt(m[4]) }) },
|
|
3766
|
-
// Safari: "functionName@file:line:col" (similar to Firefox)
|
|
3767
|
-
{ regex: /^(.+)@(?:https?:\/\/[^/]+)?([^:]+):(\d+)(?::(\d+))?$/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
3768
|
-
// Python: "File \"file.py\", line N, in function"
|
|
3769
|
-
{ regex: /File\s+"([^"]+)",\s+line\s+(\d+)(?:,\s+in\s+(.+))?/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), funcName: m[3] || null, col: null }) },
|
|
3770
|
-
// Go: "file.go:line" or "package/file.go:line +0x..."
|
|
3771
|
-
{ regex: /^\s*([^\s:]+\.go):(\d+)(?:\s|$)/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), funcName: null, col: null }) },
|
|
3772
|
-
// Go with function: "package.FunctionName()\n\tfile.go:line"
|
|
3773
|
-
{ regex: /^\s*([^\s(]+)\(\)$/, extract: null }, // Skip function-only lines
|
|
3774
|
-
// Java: "at package.Class.method(File.java:line)"
|
|
3775
|
-
{ regex: /at\s+([^\(]+)\(([^:]+):(\d+)\)/, extract: (m) => ({ funcName: m[1].split('.').pop(), file: m[2], line: parseInt(m[3]), col: null }) },
|
|
3776
|
-
// Rust: "at src/main.rs:line:col" or panic location
|
|
3777
|
-
{ regex: /(?:at\s+)?([^\s:]+\.rs):(\d+)(?::(\d+))?/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), col: m[3] ? parseInt(m[3]) : null, funcName: null }) },
|
|
3778
|
-
// Generic: "file:line" as last resort
|
|
3779
|
-
{ regex: /([^\s:]+\.\w+):(\d+)(?::(\d+))?/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), col: m[3] ? parseInt(m[3]) : null, funcName: null }) }
|
|
3780
|
-
];
|
|
3781
|
-
|
|
3782
|
-
for (const line of lines) {
|
|
3783
|
-
const trimmed = line.trim();
|
|
3784
|
-
if (!trimmed) continue;
|
|
3785
|
-
|
|
3786
|
-
// Try each pattern until one matches
|
|
3787
|
-
for (const pattern of patterns) {
|
|
3788
|
-
const match = pattern.regex.exec(trimmed);
|
|
3789
|
-
if (match && pattern.extract) {
|
|
3790
|
-
const extracted = pattern.extract(match);
|
|
3791
|
-
if (extracted && extracted.file && extracted.line) {
|
|
3792
|
-
frames.push(this.createStackFrame(
|
|
3793
|
-
extracted.file,
|
|
3794
|
-
extracted.line,
|
|
3795
|
-
extracted.funcName,
|
|
3796
|
-
extracted.col,
|
|
3797
|
-
trimmed
|
|
3798
|
-
));
|
|
3799
|
-
break; // Move to next line
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
|
|
3805
|
-
return {
|
|
3806
|
-
frameCount: frames.length,
|
|
3807
|
-
frames
|
|
3808
|
-
};
|
|
2596
|
+
return stacktrace.parseStackTrace(this, stackText);
|
|
3809
2597
|
}
|
|
3810
2598
|
|
|
3811
|
-
/**
|
|
3812
|
-
* Calculate path similarity score between two file paths
|
|
3813
|
-
* Higher score = better match
|
|
3814
|
-
* @param {string} query - The path from stack trace
|
|
3815
|
-
* @param {string} candidate - The candidate file path
|
|
3816
|
-
* @returns {number} Similarity score
|
|
3817
|
-
*/
|
|
2599
|
+
/** Calculate path similarity score between two file paths */
|
|
3818
2600
|
calculatePathSimilarity(query, candidate) {
|
|
3819
|
-
|
|
3820
|
-
const queryParts = query.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
3821
|
-
const candidateParts = candidate.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
3822
|
-
|
|
3823
|
-
let score = 0;
|
|
3824
|
-
|
|
3825
|
-
// Exact match on full path
|
|
3826
|
-
if (candidate.endsWith(query)) {
|
|
3827
|
-
score += 100;
|
|
3828
|
-
}
|
|
3829
|
-
|
|
3830
|
-
// Compare from the end (most important part)
|
|
3831
|
-
let matches = 0;
|
|
3832
|
-
const minLen = Math.min(queryParts.length, candidateParts.length);
|
|
3833
|
-
for (let i = 0; i < minLen; i++) {
|
|
3834
|
-
const queryPart = queryParts[queryParts.length - 1 - i];
|
|
3835
|
-
const candPart = candidateParts[candidateParts.length - 1 - i];
|
|
3836
|
-
if (queryPart === candPart) {
|
|
3837
|
-
matches++;
|
|
3838
|
-
// Earlier parts (closer to filename) score more
|
|
3839
|
-
score += (10 - i) * 5;
|
|
3840
|
-
} else {
|
|
3841
|
-
break; // Stop at first mismatch
|
|
3842
|
-
}
|
|
3843
|
-
}
|
|
3844
|
-
|
|
3845
|
-
// Bonus for matching most of the query path
|
|
3846
|
-
if (matches === queryParts.length) {
|
|
3847
|
-
score += 50;
|
|
3848
|
-
}
|
|
3849
|
-
|
|
3850
|
-
// Filename match is essential
|
|
3851
|
-
const queryFile = queryParts[queryParts.length - 1];
|
|
3852
|
-
const candFile = candidateParts[candidateParts.length - 1];
|
|
3853
|
-
if (queryFile !== candFile) {
|
|
3854
|
-
score = 0; // No match if filename doesn't match
|
|
3855
|
-
}
|
|
3856
|
-
|
|
3857
|
-
return score;
|
|
2601
|
+
return stacktrace.calculatePathSimilarity(query, candidate);
|
|
3858
2602
|
}
|
|
3859
2603
|
|
|
3860
|
-
/**
|
|
3861
|
-
* Find the best matching file for a stack trace path
|
|
3862
|
-
* @param {string} filePath - Path from stack trace
|
|
3863
|
-
* @param {string|null} funcName - Function name for verification
|
|
3864
|
-
* @param {number} lineNum - Line number for verification
|
|
3865
|
-
* @returns {{path: string, relativePath: string, confidence: number}|null}
|
|
3866
|
-
*/
|
|
2604
|
+
/** Find the best matching file for a stack trace path */
|
|
3867
2605
|
findBestMatchingFile(filePath, funcName, lineNum) {
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
// Collect all potential matches with scores
|
|
3871
|
-
for (const [absPath, fileEntry] of this.files) {
|
|
3872
|
-
const score = this.calculatePathSimilarity(filePath, absPath);
|
|
3873
|
-
const relScore = this.calculatePathSimilarity(filePath, fileEntry.relativePath);
|
|
3874
|
-
const bestScore = Math.max(score, relScore);
|
|
3875
|
-
|
|
3876
|
-
if (bestScore > 0) {
|
|
3877
|
-
candidates.push({
|
|
3878
|
-
absPath,
|
|
3879
|
-
relativePath: fileEntry.relativePath,
|
|
3880
|
-
score: bestScore,
|
|
3881
|
-
fileEntry
|
|
3882
|
-
});
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
3885
|
-
|
|
3886
|
-
if (candidates.length === 0) {
|
|
3887
|
-
// Try absolute path
|
|
3888
|
-
const absPath = path.isAbsolute(filePath) ? filePath : path.join(this.root, filePath);
|
|
3889
|
-
if (fs.existsSync(absPath)) {
|
|
3890
|
-
return {
|
|
3891
|
-
path: absPath,
|
|
3892
|
-
relativePath: path.relative(this.root, absPath),
|
|
3893
|
-
confidence: 0.5 // Low confidence for unindexed files
|
|
3894
|
-
};
|
|
3895
|
-
}
|
|
3896
|
-
return null;
|
|
3897
|
-
}
|
|
3898
|
-
|
|
3899
|
-
// Sort by score descending
|
|
3900
|
-
candidates.sort((a, b) => b.score - a.score);
|
|
3901
|
-
|
|
3902
|
-
// If there's a function name, verify it exists at the line
|
|
3903
|
-
if (funcName && candidates.length > 1) {
|
|
3904
|
-
for (const cand of candidates) {
|
|
3905
|
-
const symbols = this.symbols.get(funcName);
|
|
3906
|
-
if (symbols) {
|
|
3907
|
-
const match = symbols.find(s =>
|
|
3908
|
-
s.file === cand.absPath &&
|
|
3909
|
-
s.startLine <= lineNum && s.endLine >= lineNum
|
|
3910
|
-
);
|
|
3911
|
-
if (match) {
|
|
3912
|
-
// This candidate has the function at the right line - strong match
|
|
3913
|
-
return {
|
|
3914
|
-
path: cand.absPath,
|
|
3915
|
-
relativePath: cand.relativePath,
|
|
3916
|
-
confidence: 1.0,
|
|
3917
|
-
verifiedFunction: true
|
|
3918
|
-
};
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
|
|
3924
|
-
// Return best scoring candidate
|
|
3925
|
-
const best = candidates[0];
|
|
3926
|
-
const confidence = candidates.length === 1 ? 0.9 :
|
|
3927
|
-
(best.score > 100 ? 0.8 : 0.6);
|
|
3928
|
-
|
|
3929
|
-
return {
|
|
3930
|
-
path: best.absPath,
|
|
3931
|
-
relativePath: best.relativePath,
|
|
3932
|
-
confidence
|
|
3933
|
-
};
|
|
2606
|
+
return stacktrace.findBestMatchingFile(this, filePath, funcName, lineNum);
|
|
3934
2607
|
}
|
|
3935
2608
|
|
|
3936
|
-
/**
|
|
3937
|
-
* Create a stack frame with code context
|
|
3938
|
-
*/
|
|
2609
|
+
/** Create a stack frame with code context */
|
|
3939
2610
|
createStackFrame(filePath, lineNum, funcName, col, rawLine) {
|
|
3940
|
-
|
|
3941
|
-
file: filePath,
|
|
3942
|
-
line: lineNum,
|
|
3943
|
-
function: funcName,
|
|
3944
|
-
column: col,
|
|
3945
|
-
raw: rawLine,
|
|
3946
|
-
found: false,
|
|
3947
|
-
code: null,
|
|
3948
|
-
context: null,
|
|
3949
|
-
confidence: 0
|
|
3950
|
-
};
|
|
3951
|
-
|
|
3952
|
-
// Find the best matching file using improved algorithm
|
|
3953
|
-
const match = this.findBestMatchingFile(filePath, funcName, lineNum);
|
|
3954
|
-
|
|
3955
|
-
if (match) {
|
|
3956
|
-
const resolvedPath = match.path;
|
|
3957
|
-
frame.found = true;
|
|
3958
|
-
frame.resolvedFile = match.relativePath;
|
|
3959
|
-
frame.confidence = match.confidence;
|
|
3960
|
-
if (match.verifiedFunction) {
|
|
3961
|
-
frame.verifiedFunction = true;
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
|
-
try {
|
|
3965
|
-
const content = this._readFile(resolvedPath);
|
|
3966
|
-
const lines = content.split('\n');
|
|
3967
|
-
|
|
3968
|
-
// Get the exact line
|
|
3969
|
-
if (lineNum > 0 && lineNum <= lines.length) {
|
|
3970
|
-
frame.code = lines[lineNum - 1];
|
|
3971
|
-
|
|
3972
|
-
// Get context (2 lines before, 2 after)
|
|
3973
|
-
const contextLines = [];
|
|
3974
|
-
for (let i = Math.max(0, lineNum - 3); i < Math.min(lines.length, lineNum + 2); i++) {
|
|
3975
|
-
contextLines.push({
|
|
3976
|
-
line: i + 1,
|
|
3977
|
-
code: lines[i],
|
|
3978
|
-
isCurrent: i + 1 === lineNum
|
|
3979
|
-
});
|
|
3980
|
-
}
|
|
3981
|
-
frame.context = contextLines;
|
|
3982
|
-
}
|
|
3983
|
-
|
|
3984
|
-
// Try to find function info (verify it contains the line)
|
|
3985
|
-
if (funcName) {
|
|
3986
|
-
const symbols = this.symbols.get(funcName);
|
|
3987
|
-
if (symbols) {
|
|
3988
|
-
const funcMatch = symbols.find(s =>
|
|
3989
|
-
s.file === resolvedPath &&
|
|
3990
|
-
s.startLine <= lineNum && s.endLine >= lineNum
|
|
3991
|
-
);
|
|
3992
|
-
if (funcMatch) {
|
|
3993
|
-
frame.functionInfo = {
|
|
3994
|
-
name: funcMatch.name,
|
|
3995
|
-
startLine: funcMatch.startLine,
|
|
3996
|
-
endLine: funcMatch.endLine,
|
|
3997
|
-
params: funcMatch.params
|
|
3998
|
-
};
|
|
3999
|
-
frame.confidence = 1.0; // High confidence when function verified
|
|
4000
|
-
} else {
|
|
4001
|
-
// Function exists but line doesn't match - lower confidence
|
|
4002
|
-
const anyMatch = symbols.find(s => s.file === resolvedPath);
|
|
4003
|
-
if (anyMatch) {
|
|
4004
|
-
frame.functionInfo = {
|
|
4005
|
-
name: anyMatch.name,
|
|
4006
|
-
startLine: anyMatch.startLine,
|
|
4007
|
-
endLine: anyMatch.endLine,
|
|
4008
|
-
params: anyMatch.params,
|
|
4009
|
-
lineMismatch: true
|
|
4010
|
-
};
|
|
4011
|
-
frame.confidence = Math.min(frame.confidence, 0.5);
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
}
|
|
4015
|
-
} else {
|
|
4016
|
-
// No function name in stack - find enclosing function
|
|
4017
|
-
const enclosing = this.findEnclosingFunction(resolvedPath, lineNum, true);
|
|
4018
|
-
if (enclosing) {
|
|
4019
|
-
frame.functionInfo = {
|
|
4020
|
-
name: enclosing.name,
|
|
4021
|
-
startLine: enclosing.startLine,
|
|
4022
|
-
endLine: enclosing.endLine,
|
|
4023
|
-
params: enclosing.params,
|
|
4024
|
-
inferred: true
|
|
4025
|
-
};
|
|
4026
|
-
}
|
|
4027
|
-
}
|
|
4028
|
-
} catch (e) {
|
|
4029
|
-
frame.error = e.message;
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
return frame;
|
|
4034
|
-
}
|
|
4035
|
-
|
|
4036
|
-
/**
|
|
4037
|
-
* Verify that all call sites match a function's signature
|
|
4038
|
-
* @param {string} name - Function name
|
|
4039
|
-
* @returns {object} Verification results with mismatches
|
|
4040
|
-
*/
|
|
4041
|
-
verify(name, options = {}) {
|
|
4042
|
-
this._beginOp();
|
|
4043
|
-
try {
|
|
4044
|
-
const { def } = this.resolveSymbol(name, { file: options.file });
|
|
4045
|
-
if (!def) {
|
|
4046
|
-
return { found: false, function: name };
|
|
4047
|
-
}
|
|
4048
|
-
// For Python/Rust methods, exclude self/cls from parameter count
|
|
4049
|
-
// (callers don't pass self/cls explicitly: obj.method(a, b) not obj.method(obj, a, b))
|
|
4050
|
-
const fileEntry = this.files.get(def.file);
|
|
4051
|
-
const lang = fileEntry?.language;
|
|
4052
|
-
let params = def.paramsStructured || [];
|
|
4053
|
-
if ((lang === 'python' || lang === 'rust') && params.length > 0) {
|
|
4054
|
-
const firstName = params[0].name;
|
|
4055
|
-
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
4056
|
-
params = params.slice(1);
|
|
4057
|
-
}
|
|
4058
|
-
}
|
|
4059
|
-
const hasRest = params.some(p => p.rest);
|
|
4060
|
-
// Rest params don't count toward expected/min — they accept 0+ extra args
|
|
4061
|
-
const nonRestParams = params.filter(p => !p.rest);
|
|
4062
|
-
const expectedParamCount = nonRestParams.length;
|
|
4063
|
-
const optionalCount = nonRestParams.filter(p => p.optional || p.default !== undefined).length;
|
|
4064
|
-
const minArgs = expectedParamCount - optionalCount;
|
|
4065
|
-
|
|
4066
|
-
// Get all call sites
|
|
4067
|
-
const usages = this.usages(name, { codeOnly: true });
|
|
4068
|
-
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
4069
|
-
|
|
4070
|
-
const valid = [];
|
|
4071
|
-
const mismatches = [];
|
|
4072
|
-
const uncertain = [];
|
|
4073
|
-
|
|
4074
|
-
// If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
|
|
4075
|
-
// This prevents false positives where a standalone function name matches method calls
|
|
4076
|
-
const defIsMethod = def.isMethod || def.type === 'method' || def.className;
|
|
4077
|
-
|
|
4078
|
-
for (const call of calls) {
|
|
4079
|
-
const analysis = this.analyzeCallSite(call, name);
|
|
4080
|
-
|
|
4081
|
-
// Skip method calls when verifying a non-method definition
|
|
4082
|
-
if (analysis.isMethodCall && !defIsMethod) {
|
|
4083
|
-
continue;
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
if (analysis.args === null) {
|
|
4087
|
-
// Couldn't parse arguments
|
|
4088
|
-
uncertain.push({
|
|
4089
|
-
file: call.relativePath,
|
|
4090
|
-
line: call.line,
|
|
4091
|
-
expression: call.content.trim(),
|
|
4092
|
-
reason: 'Could not parse call arguments'
|
|
4093
|
-
});
|
|
4094
|
-
continue;
|
|
4095
|
-
}
|
|
4096
|
-
|
|
4097
|
-
if (analysis.hasSpread) {
|
|
4098
|
-
// Spread args - can't verify count
|
|
4099
|
-
uncertain.push({
|
|
4100
|
-
file: call.relativePath,
|
|
4101
|
-
line: call.line,
|
|
4102
|
-
expression: call.content.trim(),
|
|
4103
|
-
reason: 'Uses spread operator'
|
|
4104
|
-
});
|
|
4105
|
-
continue;
|
|
4106
|
-
}
|
|
4107
|
-
|
|
4108
|
-
const argCount = analysis.argCount;
|
|
4109
|
-
|
|
4110
|
-
// Check if arg count is valid
|
|
4111
|
-
if (hasRest) {
|
|
4112
|
-
// With rest param, need at least minArgs
|
|
4113
|
-
if (argCount >= minArgs) {
|
|
4114
|
-
valid.push({ file: call.relativePath, line: call.line });
|
|
4115
|
-
} else {
|
|
4116
|
-
mismatches.push({
|
|
4117
|
-
file: call.relativePath,
|
|
4118
|
-
line: call.line,
|
|
4119
|
-
expression: call.content.trim(),
|
|
4120
|
-
expected: `at least ${minArgs} arg(s)`,
|
|
4121
|
-
actual: argCount,
|
|
4122
|
-
args: analysis.args
|
|
4123
|
-
});
|
|
4124
|
-
}
|
|
4125
|
-
} else {
|
|
4126
|
-
// Without rest, need between minArgs and expectedParamCount
|
|
4127
|
-
if (argCount >= minArgs && argCount <= expectedParamCount) {
|
|
4128
|
-
valid.push({ file: call.relativePath, line: call.line });
|
|
4129
|
-
} else {
|
|
4130
|
-
mismatches.push({
|
|
4131
|
-
file: call.relativePath,
|
|
4132
|
-
line: call.line,
|
|
4133
|
-
expression: call.content.trim(),
|
|
4134
|
-
expected: minArgs === expectedParamCount
|
|
4135
|
-
? `${expectedParamCount} arg(s)`
|
|
4136
|
-
: `${minArgs}-${expectedParamCount} arg(s)`,
|
|
4137
|
-
actual: argCount,
|
|
4138
|
-
args: analysis.args
|
|
4139
|
-
});
|
|
4140
|
-
}
|
|
4141
|
-
}
|
|
4142
|
-
}
|
|
4143
|
-
this._clearTreeCache();
|
|
4144
|
-
|
|
4145
|
-
return {
|
|
4146
|
-
found: true,
|
|
4147
|
-
function: name,
|
|
4148
|
-
file: def.relativePath,
|
|
4149
|
-
startLine: def.startLine,
|
|
4150
|
-
signature: this.formatSignature(def),
|
|
4151
|
-
params: params.map(p => ({
|
|
4152
|
-
name: p.name,
|
|
4153
|
-
optional: p.optional || p.default !== undefined,
|
|
4154
|
-
hasDefault: p.default !== undefined
|
|
4155
|
-
})),
|
|
4156
|
-
expectedArgs: { min: minArgs, max: hasRest ? '∞' : expectedParamCount },
|
|
4157
|
-
totalCalls: valid.length + mismatches.length + uncertain.length,
|
|
4158
|
-
valid: valid.length,
|
|
4159
|
-
mismatches: mismatches.length,
|
|
4160
|
-
uncertain: uncertain.length,
|
|
4161
|
-
mismatchDetails: mismatches,
|
|
4162
|
-
uncertainDetails: uncertain
|
|
4163
|
-
};
|
|
4164
|
-
} finally { this._endOp(); }
|
|
2611
|
+
return stacktrace.createStackFrame(this, filePath, lineNum, funcName, col, rawLine);
|
|
4165
2612
|
}
|
|
4166
2613
|
|
|
4167
|
-
/**
|
|
4168
|
-
|
|
4169
|
-
* @param {object} call - Usage object with file, line, content
|
|
4170
|
-
* @param {string} funcName - Function name to find
|
|
4171
|
-
* @returns {object} { args, argCount, hasSpread, hasVariable }
|
|
4172
|
-
*/
|
|
4173
|
-
analyzeCallSite(call, funcName) {
|
|
4174
|
-
try {
|
|
4175
|
-
const language = detectLanguage(call.file);
|
|
4176
|
-
if (!language) return { args: null, argCount: 0 };
|
|
4177
|
-
|
|
4178
|
-
// Use tree cache to avoid re-parsing the same file in batch operations
|
|
4179
|
-
let tree = this._treeCache?.get(call.file);
|
|
4180
|
-
if (!tree) {
|
|
4181
|
-
const content = this._readFile(call.file);
|
|
4182
|
-
// HTML files need special handling: parse script blocks as JS
|
|
4183
|
-
if (language === 'html') {
|
|
4184
|
-
const htmlModule = getLanguageModule('html');
|
|
4185
|
-
const htmlParser = getParser('html');
|
|
4186
|
-
const jsParser = getParser('javascript');
|
|
4187
|
-
if (!htmlParser || !jsParser) return { args: null, argCount: 0 };
|
|
4188
|
-
const blocks = htmlModule.extractScriptBlocks(content, htmlParser);
|
|
4189
|
-
if (blocks.length === 0) return { args: null, argCount: 0 };
|
|
4190
|
-
const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
|
|
4191
|
-
tree = safeParse(jsParser, virtualJS);
|
|
4192
|
-
} else {
|
|
4193
|
-
const parser = getParser(language);
|
|
4194
|
-
if (!parser) return { args: null, argCount: 0 };
|
|
4195
|
-
tree = safeParse(parser, content);
|
|
4196
|
-
}
|
|
4197
|
-
if (!tree) return { args: null, argCount: 0 };
|
|
4198
|
-
if (!this._treeCache) this._treeCache = new Map();
|
|
4199
|
-
this._treeCache.set(call.file, tree);
|
|
4200
|
-
}
|
|
4201
|
-
|
|
4202
|
-
// Call node types vary by language
|
|
4203
|
-
const callTypes = new Set(['call_expression', 'call', 'method_invocation', 'object_creation_expression']);
|
|
4204
|
-
const targetRow = call.line - 1; // tree-sitter is 0-indexed
|
|
4205
|
-
|
|
4206
|
-
// Find the call expression at the target line matching funcName
|
|
4207
|
-
const callNode = this._findCallNode(tree.rootNode, callTypes, targetRow, funcName);
|
|
4208
|
-
if (!callNode) return { args: null, argCount: 0 };
|
|
4209
|
-
|
|
4210
|
-
// Check if this is a method call (obj.func()) vs a direct call (func())
|
|
4211
|
-
const funcNode = callNode.childForFieldName('function') ||
|
|
4212
|
-
callNode.childForFieldName('name');
|
|
4213
|
-
let isMethodCall = false;
|
|
4214
|
-
if (funcNode) {
|
|
4215
|
-
// member_expression (JS), attribute (Python), selector_expression (Go), field_expression (Rust)
|
|
4216
|
-
if (['member_expression', 'attribute', 'selector_expression', 'field_expression'].includes(funcNode.type)) {
|
|
4217
|
-
isMethodCall = true;
|
|
4218
|
-
}
|
|
4219
|
-
// Java method_invocation with object
|
|
4220
|
-
if (callNode.type === 'method_invocation' && callNode.childForFieldName('object')) {
|
|
4221
|
-
isMethodCall = true;
|
|
4222
|
-
}
|
|
4223
|
-
}
|
|
4224
|
-
|
|
4225
|
-
const argsNode = callNode.childForFieldName('arguments');
|
|
4226
|
-
if (!argsNode) return { args: [], argCount: 0, isMethodCall };
|
|
2614
|
+
/** Verify that all call sites match a function's signature */
|
|
2615
|
+
verify(name, options) { return verifyModule.verify(this, name, options); }
|
|
4227
2616
|
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
args.push(argsNode.namedChild(i).text.trim());
|
|
4231
|
-
}
|
|
4232
|
-
|
|
4233
|
-
return {
|
|
4234
|
-
args,
|
|
4235
|
-
argCount: args.length,
|
|
4236
|
-
hasSpread: args.some(a => a.startsWith('...')),
|
|
4237
|
-
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
|
|
4238
|
-
isMethodCall
|
|
4239
|
-
};
|
|
4240
|
-
} catch (e) {
|
|
4241
|
-
return { args: null, argCount: 0 };
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
2617
|
+
/** Analyze a call site to understand how it's being called (AST-based) */
|
|
2618
|
+
analyzeCallSite(call, funcName) { return verifyModule.analyzeCallSite(this, call, funcName); }
|
|
4244
2619
|
|
|
4245
|
-
/**
|
|
4246
|
-
|
|
4247
|
-
*/
|
|
4248
|
-
_findCallNode(node, callTypes, targetRow, funcName) {
|
|
4249
|
-
if (node.startPosition.row > targetRow || node.endPosition.row < targetRow) {
|
|
4250
|
-
return null; // Skip nodes that don't contain the target line
|
|
4251
|
-
}
|
|
4252
|
-
|
|
4253
|
-
if (callTypes.has(node.type) && node.startPosition.row === targetRow) {
|
|
4254
|
-
// Java constructor: new ClassName(args) — name is in 'type' field
|
|
4255
|
-
if (node.type === 'object_creation_expression') {
|
|
4256
|
-
const typeNode = node.childForFieldName('type');
|
|
4257
|
-
if (typeNode) {
|
|
4258
|
-
// Strip generics and package qualifiers: com.foo.Bar<T> -> Bar
|
|
4259
|
-
const typeName = typeNode.text.replace(/<.*>$/, '').split('.').pop();
|
|
4260
|
-
if (typeName === funcName) return node;
|
|
4261
|
-
}
|
|
4262
|
-
} else {
|
|
4263
|
-
// Check if this call is for our target function
|
|
4264
|
-
const funcNode = node.childForFieldName('function') ||
|
|
4265
|
-
node.childForFieldName('name'); // Java method_invocation uses 'name'
|
|
4266
|
-
if (funcNode) {
|
|
4267
|
-
const funcText = funcNode.type === 'member_expression' || funcNode.type === 'selector_expression' || funcNode.type === 'field_expression' || funcNode.type === 'attribute'
|
|
4268
|
-
? (funcNode.childForFieldName('property') || funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
4269
|
-
: funcNode.text;
|
|
4270
|
-
if (funcText === funcName) return node;
|
|
4271
|
-
}
|
|
4272
|
-
}
|
|
4273
|
-
}
|
|
2620
|
+
/** Find a call expression node at the target line matching funcName */
|
|
2621
|
+
_findCallNode(node, callTypes, targetRow, funcName) { return verifyModule.findCallNode(node, callTypes, targetRow, funcName); }
|
|
4274
2622
|
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
const result = this._findCallNode(node.child(i), callTypes, targetRow, funcName);
|
|
4278
|
-
if (result) return result;
|
|
4279
|
-
}
|
|
4280
|
-
return null;
|
|
4281
|
-
}
|
|
2623
|
+
/** Clear the AST tree cache (call after batch operations) */
|
|
2624
|
+
_clearTreeCache() { verifyModule.clearTreeCache(this); }
|
|
4282
2625
|
|
|
4283
|
-
/**
|
|
4284
|
-
|
|
4285
|
-
*/
|
|
4286
|
-
_clearTreeCache() {
|
|
4287
|
-
this._treeCache = null;
|
|
4288
|
-
}
|
|
4289
|
-
|
|
4290
|
-
/**
|
|
4291
|
-
* Identify common calling patterns
|
|
4292
|
-
*/
|
|
4293
|
-
identifyCallPatterns(callSites, funcName) {
|
|
4294
|
-
const patterns = {
|
|
4295
|
-
constantArgs: 0, // Call sites with literal/constant arguments
|
|
4296
|
-
variableArgs: 0, // Call sites passing variables
|
|
4297
|
-
chainedCalls: 0, // Calls that are part of method chains
|
|
4298
|
-
awaitedCalls: 0, // Async calls with await
|
|
4299
|
-
spreadCalls: 0 // Calls using spread operator
|
|
4300
|
-
};
|
|
4301
|
-
|
|
4302
|
-
for (const site of callSites) {
|
|
4303
|
-
const expr = site.expression;
|
|
4304
|
-
|
|
4305
|
-
if (site.hasSpread) patterns.spreadCalls++;
|
|
4306
|
-
if (/await\s/.test(expr)) patterns.awaitedCalls++;
|
|
4307
|
-
if (new RegExp('\\.' + escapeRegExp(funcName) + '\\s*\\(').test(expr)) patterns.chainedCalls++;
|
|
4308
|
-
|
|
4309
|
-
if (site.args && site.args.length > 0) {
|
|
4310
|
-
const hasLiteral = site.args.some(a =>
|
|
4311
|
-
/^[\d'"{\[]/.test(a) || a === 'true' || a === 'false' || a === 'null'
|
|
4312
|
-
);
|
|
4313
|
-
if (hasLiteral) patterns.constantArgs++;
|
|
4314
|
-
if (site.hasVariable) patterns.variableArgs++;
|
|
4315
|
-
}
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
|
-
return patterns;
|
|
4319
|
-
}
|
|
2626
|
+
/** Identify common calling patterns */
|
|
2627
|
+
identifyCallPatterns(callSites, funcName) { return verifyModule.identifyCallPatterns(callSites, funcName); }
|
|
4320
2628
|
|
|
4321
2629
|
/**
|
|
4322
2630
|
* Get complete information about a symbol - definition, usages, callers, callees, tests, code
|
|
@@ -4333,10 +2641,10 @@ class ProjectIndex {
|
|
|
4333
2641
|
const maxCallees = options.all ? Infinity : (options.maxCallees || 10);
|
|
4334
2642
|
const includeMethods = options.includeMethods ?? true;
|
|
4335
2643
|
|
|
4336
|
-
// Find symbol definition(s)
|
|
4337
|
-
const definitions = this.find(name, { exact: true, file: options.file });
|
|
2644
|
+
// Find symbol definition(s) — skip counts since about() computes its own via usages()
|
|
2645
|
+
const definitions = this.find(name, { exact: true, file: options.file, skipCounts: true });
|
|
4338
2646
|
if (definitions.length === 0) {
|
|
4339
|
-
// Try fuzzy match
|
|
2647
|
+
// Try fuzzy match (needs counts for suggestion ranking)
|
|
4340
2648
|
const fuzzy = this.find(name, { file: options.file });
|
|
4341
2649
|
if (fuzzy.length === 0) {
|
|
4342
2650
|
return null;
|
|
@@ -4379,7 +2687,7 @@ class ProjectIndex {
|
|
|
4379
2687
|
let allCallers = null;
|
|
4380
2688
|
let allCallees = null;
|
|
4381
2689
|
if (primary.type === 'function' || primary.params !== undefined) {
|
|
4382
|
-
allCallers = this.findCallers(symbolName, { includeMethods });
|
|
2690
|
+
allCallers = this.findCallers(symbolName, { includeMethods, includeUncertain: options.includeUncertain, targetDefinitions: [primary] });
|
|
4383
2691
|
// Apply exclude filter before slicing
|
|
4384
2692
|
if (options.exclude && options.exclude.length > 0) {
|
|
4385
2693
|
allCallers = allCallers.filter(c => this.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
@@ -4470,7 +2778,7 @@ class ProjectIndex {
|
|
|
4470
2778
|
otherDefinitions: (options.all ? others : others.slice(0, 3)).map(d => ({
|
|
4471
2779
|
file: d.relativePath,
|
|
4472
2780
|
line: d.startLine,
|
|
4473
|
-
usageCount: d.usageCount
|
|
2781
|
+
usageCount: d.usageCount ?? this.countSymbolUsages(d).total
|
|
4474
2782
|
})),
|
|
4475
2783
|
types,
|
|
4476
2784
|
code,
|
|
@@ -4662,7 +2970,8 @@ class ProjectIndex {
|
|
|
4662
2970
|
const functions = [];
|
|
4663
2971
|
for (const [name, symbols] of this.symbols) {
|
|
4664
2972
|
for (const sym of symbols) {
|
|
4665
|
-
if (sym.type === 'function' || sym.
|
|
2973
|
+
if (sym.type === 'function' || sym.type === 'method' || sym.type === 'static' ||
|
|
2974
|
+
sym.type === 'constructor' || sym.type === 'public' || sym.type === 'abstract') {
|
|
4666
2975
|
const lineCount = sym.endLine - sym.startLine + 1;
|
|
4667
2976
|
const relativePath = sym.relativePath || (sym.file ? path.relative(this.root, sym.file) : '');
|
|
4668
2977
|
functions.push({
|
|
@@ -4695,7 +3004,8 @@ class ProjectIndex {
|
|
|
4695
3004
|
|
|
4696
3005
|
for (const [filePath, fileEntry] of this.files) {
|
|
4697
3006
|
let functions = fileEntry.symbols.filter(s =>
|
|
4698
|
-
s.type === 'function' || s.type === 'method' || s.type === 'static' ||
|
|
3007
|
+
s.type === 'function' || s.type === 'method' || s.type === 'static' ||
|
|
3008
|
+
s.type === 'constructor' || s.type === 'public' || s.type === 'abstract'
|
|
4699
3009
|
);
|
|
4700
3010
|
const classes = fileEntry.symbols.filter(s =>
|
|
4701
3011
|
['class', 'interface', 'type', 'enum', 'struct', 'trait', 'impl'].includes(s.type)
|
|
@@ -4802,182 +3112,14 @@ class ProjectIndex {
|
|
|
4802
3112
|
// CACHE METHODS
|
|
4803
3113
|
// ========================================================================
|
|
4804
3114
|
|
|
4805
|
-
/**
|
|
4806
|
-
|
|
4807
|
-
*
|
|
4808
|
-
* @param {string} [cachePath] - Optional custom cache path
|
|
4809
|
-
* @returns {string} - Path to cache file
|
|
4810
|
-
*/
|
|
4811
|
-
saveCache(cachePath) {
|
|
4812
|
-
const cacheDir = cachePath
|
|
4813
|
-
? path.dirname(cachePath)
|
|
4814
|
-
: path.join(this.root, '.ucn-cache');
|
|
4815
|
-
|
|
4816
|
-
if (!fs.existsSync(cacheDir)) {
|
|
4817
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
4818
|
-
}
|
|
4819
|
-
|
|
4820
|
-
const cacheFile = cachePath || path.join(cacheDir, 'index.json');
|
|
4821
|
-
|
|
4822
|
-
// Prepare callsCache for serialization (exclude content to save space)
|
|
4823
|
-
const callsCacheData = [];
|
|
4824
|
-
for (const [filePath, entry] of this.callsCache) {
|
|
4825
|
-
callsCacheData.push([filePath, {
|
|
4826
|
-
mtime: entry.mtime,
|
|
4827
|
-
hash: entry.hash,
|
|
4828
|
-
calls: entry.calls
|
|
4829
|
-
// content is not persisted - will be read on demand
|
|
4830
|
-
}]);
|
|
4831
|
-
}
|
|
4832
|
-
|
|
4833
|
-
const cacheData = {
|
|
4834
|
-
version: 4, // v4: className, memberType, isMethod for all languages
|
|
4835
|
-
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
4836
|
-
root: this.root,
|
|
4837
|
-
buildTime: this.buildTime,
|
|
4838
|
-
timestamp: Date.now(),
|
|
4839
|
-
files: Array.from(this.files.entries()),
|
|
4840
|
-
symbols: Array.from(this.symbols.entries()),
|
|
4841
|
-
importGraph: Array.from(this.importGraph.entries()),
|
|
4842
|
-
exportGraph: Array.from(this.exportGraph.entries()),
|
|
4843
|
-
extendsGraph: Array.from(this.extendsGraph.entries()),
|
|
4844
|
-
extendedByGraph: Array.from(this.extendedByGraph.entries()),
|
|
4845
|
-
callsCache: callsCacheData,
|
|
4846
|
-
failedFiles: this.failedFiles ? Array.from(this.failedFiles) : []
|
|
4847
|
-
};
|
|
4848
|
-
|
|
4849
|
-
fs.writeFileSync(cacheFile, JSON.stringify(cacheData));
|
|
4850
|
-
return cacheFile;
|
|
4851
|
-
}
|
|
4852
|
-
|
|
4853
|
-
/**
|
|
4854
|
-
* Load index from cache file
|
|
4855
|
-
*
|
|
4856
|
-
* @param {string} [cachePath] - Optional custom cache path
|
|
4857
|
-
* @returns {boolean} - True if loaded successfully
|
|
4858
|
-
*/
|
|
4859
|
-
loadCache(cachePath) {
|
|
4860
|
-
const cacheFile = cachePath || path.join(this.root, '.ucn-cache', 'index.json');
|
|
4861
|
-
|
|
4862
|
-
if (!fs.existsSync(cacheFile)) {
|
|
4863
|
-
return false;
|
|
4864
|
-
}
|
|
4865
|
-
|
|
4866
|
-
try {
|
|
4867
|
-
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
4868
|
-
|
|
4869
|
-
// Check version compatibility
|
|
4870
|
-
// v4 adds className, memberType, isMethod for all languages
|
|
4871
|
-
// Only accept exactly version 4 (or future versions handled explicitly)
|
|
4872
|
-
if (cacheData.version !== 4) {
|
|
4873
|
-
return false;
|
|
4874
|
-
}
|
|
4875
|
-
|
|
4876
|
-
// Invalidate cache when UCN version changes (logic may have changed)
|
|
4877
|
-
if (cacheData.ucnVersion !== UCN_VERSION) {
|
|
4878
|
-
return false;
|
|
4879
|
-
}
|
|
4880
|
-
|
|
4881
|
-
// Validate cache structure has required fields
|
|
4882
|
-
if (!Array.isArray(cacheData.files) ||
|
|
4883
|
-
!Array.isArray(cacheData.symbols) ||
|
|
4884
|
-
!Array.isArray(cacheData.importGraph) ||
|
|
4885
|
-
!Array.isArray(cacheData.exportGraph)) {
|
|
4886
|
-
return false;
|
|
4887
|
-
}
|
|
4888
|
-
|
|
4889
|
-
this.files = new Map(cacheData.files);
|
|
4890
|
-
this.symbols = new Map(cacheData.symbols);
|
|
4891
|
-
this.importGraph = new Map(cacheData.importGraph);
|
|
4892
|
-
this.exportGraph = new Map(cacheData.exportGraph);
|
|
4893
|
-
this.buildTime = cacheData.buildTime;
|
|
4894
|
-
|
|
4895
|
-
// Restore optional graphs if present
|
|
4896
|
-
if (Array.isArray(cacheData.extendsGraph)) {
|
|
4897
|
-
this.extendsGraph = new Map(cacheData.extendsGraph);
|
|
4898
|
-
}
|
|
4899
|
-
if (Array.isArray(cacheData.extendedByGraph)) {
|
|
4900
|
-
this.extendedByGraph = new Map(cacheData.extendedByGraph);
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
|
-
// Restore callsCache if present (v2+)
|
|
4904
|
-
if (Array.isArray(cacheData.callsCache)) {
|
|
4905
|
-
this.callsCache = new Map(cacheData.callsCache);
|
|
4906
|
-
}
|
|
3115
|
+
/** Save index to cache file */
|
|
3116
|
+
saveCache(cachePath) { return indexCache.saveCache(this, cachePath); }
|
|
4907
3117
|
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
this.failedFiles = new Set(cacheData.failedFiles);
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
|
-
// Rebuild derived graphs to ensure consistency with current config
|
|
4914
|
-
this.buildImportGraph();
|
|
4915
|
-
this.buildInheritanceGraph();
|
|
3118
|
+
/** Load index from cache file */
|
|
3119
|
+
loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
|
|
4916
3120
|
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
return false;
|
|
4920
|
-
}
|
|
4921
|
-
}
|
|
4922
|
-
|
|
4923
|
-
/**
|
|
4924
|
-
* Check if cache is stale (any files changed or new files added)
|
|
4925
|
-
*
|
|
4926
|
-
* @returns {boolean} - True if cache needs rebuilding
|
|
4927
|
-
*/
|
|
4928
|
-
isCacheStale() {
|
|
4929
|
-
// Check for new files added to project
|
|
4930
|
-
// Use same ignores as build() — .gitignore + .ucn.json exclude
|
|
4931
|
-
const pattern = detectProjectPattern(this.root);
|
|
4932
|
-
const globOpts = { root: this.root };
|
|
4933
|
-
const gitignorePatterns = parseGitignore(this.root);
|
|
4934
|
-
const configExclude = this.config.exclude || [];
|
|
4935
|
-
if (gitignorePatterns.length > 0 || configExclude.length > 0) {
|
|
4936
|
-
globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
|
|
4937
|
-
}
|
|
4938
|
-
const currentFiles = expandGlob(pattern, globOpts);
|
|
4939
|
-
const cachedPaths = new Set(this.files.keys());
|
|
4940
|
-
|
|
4941
|
-
for (const file of currentFiles) {
|
|
4942
|
-
if (!cachedPaths.has(file) && !(this.failedFiles && this.failedFiles.has(file))) {
|
|
4943
|
-
return true; // New file found
|
|
4944
|
-
}
|
|
4945
|
-
}
|
|
4946
|
-
|
|
4947
|
-
// Check existing cached files for modifications/deletions
|
|
4948
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
4949
|
-
// File deleted
|
|
4950
|
-
if (!fs.existsSync(filePath)) {
|
|
4951
|
-
return true;
|
|
4952
|
-
}
|
|
4953
|
-
|
|
4954
|
-
// File modified - check size first, then mtime, then hash
|
|
4955
|
-
try {
|
|
4956
|
-
const stat = fs.statSync(filePath);
|
|
4957
|
-
|
|
4958
|
-
// If size changed, file changed
|
|
4959
|
-
if (fileEntry.size !== undefined && stat.size !== fileEntry.size) {
|
|
4960
|
-
return true;
|
|
4961
|
-
}
|
|
4962
|
-
|
|
4963
|
-
// If mtime matches, file hasn't changed
|
|
4964
|
-
if (fileEntry.mtime && stat.mtimeMs === fileEntry.mtime) {
|
|
4965
|
-
continue;
|
|
4966
|
-
}
|
|
4967
|
-
|
|
4968
|
-
// mtime changed or not stored - verify with hash
|
|
4969
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
4970
|
-
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
4971
|
-
if (hash !== fileEntry.hash) {
|
|
4972
|
-
return true;
|
|
4973
|
-
}
|
|
4974
|
-
} catch (e) {
|
|
4975
|
-
return true;
|
|
4976
|
-
}
|
|
4977
|
-
}
|
|
4978
|
-
|
|
4979
|
-
return false;
|
|
4980
|
-
}
|
|
3121
|
+
/** Check if cache is stale (any files changed or new files added) */
|
|
3122
|
+
isCacheStale() { return indexCache.isCacheStale(this); }
|
|
4981
3123
|
|
|
4982
3124
|
/**
|
|
4983
3125
|
* Find the best usage example of a function.
|
|
@@ -5040,74 +3182,8 @@ class ProjectIndex {
|
|
|
5040
3182
|
} finally { this._endOp(); }
|
|
5041
3183
|
}
|
|
5042
3184
|
|
|
5043
|
-
/**
|
|
5044
|
-
|
|
5045
|
-
* @private
|
|
5046
|
-
*/
|
|
5047
|
-
_analyzeCallSiteAST(filePath, lineNum, funcName) {
|
|
5048
|
-
const result = {
|
|
5049
|
-
isAwait: false, isDestructured: false, isTypedAssignment: false,
|
|
5050
|
-
isInReturn: false, isInCatch: false, isInConditional: false,
|
|
5051
|
-
hasComment: false, isStandalone: false
|
|
5052
|
-
};
|
|
5053
|
-
|
|
5054
|
-
try {
|
|
5055
|
-
const language = detectLanguage(filePath);
|
|
5056
|
-
if (!language) return result;
|
|
5057
|
-
|
|
5058
|
-
const parser = getParser(language);
|
|
5059
|
-
const content = this._readFile(filePath);
|
|
5060
|
-
const tree = safeParse(parser, content);
|
|
5061
|
-
if (!tree) return result;
|
|
5062
|
-
|
|
5063
|
-
const row = lineNum - 1;
|
|
5064
|
-
const node = tree.rootNode.descendantForPosition({ row, column: 0 });
|
|
5065
|
-
if (!node) return result;
|
|
5066
|
-
|
|
5067
|
-
let current = node;
|
|
5068
|
-
let foundCall = false;
|
|
5069
|
-
|
|
5070
|
-
while (current) {
|
|
5071
|
-
const type = current.type;
|
|
5072
|
-
|
|
5073
|
-
if (!foundCall && (type === 'call_expression' || type === 'call')) {
|
|
5074
|
-
const calleeNode = current.childForFieldName('function') || current.namedChild(0);
|
|
5075
|
-
if (calleeNode && calleeNode.text === funcName) {
|
|
5076
|
-
foundCall = true;
|
|
5077
|
-
}
|
|
5078
|
-
}
|
|
5079
|
-
|
|
5080
|
-
if (foundCall) {
|
|
5081
|
-
if (type === 'await_expression') result.isAwait = true;
|
|
5082
|
-
if (type === 'variable_declarator' || type === 'assignment_expression') {
|
|
5083
|
-
const parent = current.parent;
|
|
5084
|
-
if (parent && (parent.type === 'lexical_declaration' || parent.type === 'variable_declaration')) {
|
|
5085
|
-
result.isTypedAssignment = true;
|
|
5086
|
-
}
|
|
5087
|
-
}
|
|
5088
|
-
if (type === 'array_pattern' || type === 'object_pattern') result.isDestructured = true;
|
|
5089
|
-
if (type === 'return_statement') result.isInReturn = true;
|
|
5090
|
-
if (type === 'catch_clause' || type === 'except_clause') result.isInCatch = true;
|
|
5091
|
-
if (type === 'if_statement' || type === 'conditional_expression' || type === 'ternary_expression') result.isInConditional = true;
|
|
5092
|
-
if (type === 'expression_statement') result.isStandalone = true;
|
|
5093
|
-
}
|
|
5094
|
-
|
|
5095
|
-
current = current.parent;
|
|
5096
|
-
}
|
|
5097
|
-
|
|
5098
|
-
const contentLines = content.split('\n');
|
|
5099
|
-
if (lineNum > 1) {
|
|
5100
|
-
const prevLine = contentLines[lineNum - 2].trim();
|
|
5101
|
-
if (prevLine.startsWith('//') || prevLine.startsWith('#') || prevLine.endsWith('*/')) {
|
|
5102
|
-
result.hasComment = true;
|
|
5103
|
-
}
|
|
5104
|
-
}
|
|
5105
|
-
} catch (e) {
|
|
5106
|
-
// Return default result on error
|
|
5107
|
-
}
|
|
5108
|
-
|
|
5109
|
-
return result;
|
|
5110
|
-
}
|
|
3185
|
+
/** Analyze a call site using AST for example scoring */
|
|
3186
|
+
_analyzeCallSiteAST(filePath, lineNum, funcName) { return verifyModule.analyzeCallSiteAST(this, filePath, lineNum, funcName); }
|
|
5111
3187
|
|
|
5112
3188
|
/**
|
|
5113
3189
|
* Diff-based impact analysis: find which functions changed and who calls them
|
|
@@ -5128,7 +3204,7 @@ class ProjectIndex {
|
|
|
5128
3204
|
// Verify git repo
|
|
5129
3205
|
let gitRoot;
|
|
5130
3206
|
try {
|
|
5131
|
-
gitRoot =
|
|
3207
|
+
gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: this.root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
5132
3208
|
} catch (e) {
|
|
5133
3209
|
throw new Error('Not a git repository. diff-impact requires git.');
|
|
5134
3210
|
}
|
|
@@ -5167,7 +3243,24 @@ class ProjectIndex {
|
|
|
5167
3243
|
};
|
|
5168
3244
|
}
|
|
5169
3245
|
|
|
5170
|
-
|
|
3246
|
+
// Diff paths are git-root-relative. Resolve to this.root for file lookup.
|
|
3247
|
+
// Normalize both through realpath to handle macOS /var → /private/var symlinks.
|
|
3248
|
+
let realGitRoot, realProjectRoot;
|
|
3249
|
+
try { realGitRoot = fs.realpathSync(gitRoot); } catch (_) { realGitRoot = gitRoot; }
|
|
3250
|
+
try { realProjectRoot = fs.realpathSync(this.root); } catch (_) { realProjectRoot = this.root; }
|
|
3251
|
+
const projectPrefix = realGitRoot === realProjectRoot
|
|
3252
|
+
? ''
|
|
3253
|
+
: path.relative(realGitRoot, realProjectRoot);
|
|
3254
|
+
|
|
3255
|
+
const rawChanges = parseDiff(diffText, gitRoot);
|
|
3256
|
+
// Filter to files under this.root and remap paths.
|
|
3257
|
+
// Preserve gitRelativePath (repo-relative) for git show commands.
|
|
3258
|
+
const changes = [];
|
|
3259
|
+
for (const c of rawChanges) {
|
|
3260
|
+
if (projectPrefix && !c.relativePath.startsWith(projectPrefix + '/')) continue;
|
|
3261
|
+
const localRel = projectPrefix ? c.relativePath.slice(projectPrefix.length + 1) : c.relativePath;
|
|
3262
|
+
changes.push({ ...c, gitRelativePath: c.relativePath, filePath: path.join(this.root, localRel), relativePath: localRel });
|
|
3263
|
+
}
|
|
5171
3264
|
|
|
5172
3265
|
const functions = [];
|
|
5173
3266
|
const moduleLevelChanges = [];
|
|
@@ -5188,8 +3281,8 @@ class ProjectIndex {
|
|
|
5188
3281
|
const ref = staged ? 'HEAD' : base;
|
|
5189
3282
|
try {
|
|
5190
3283
|
const oldContent = execFileSync(
|
|
5191
|
-
'git', ['show', `${ref}:${change.
|
|
5192
|
-
{ cwd: this.root, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }
|
|
3284
|
+
'git', ['show', `${ref}:${change.gitRelativePath}`],
|
|
3285
|
+
{ cwd: this.root, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
5193
3286
|
);
|
|
5194
3287
|
const oldParsed = parse(oldContent, lang);
|
|
5195
3288
|
for (const oldFn of extractCallableSymbols(oldParsed)) {
|
|
@@ -5292,8 +3385,8 @@ class ProjectIndex {
|
|
|
5292
3385
|
const ref = staged ? 'HEAD' : base;
|
|
5293
3386
|
try {
|
|
5294
3387
|
const oldContent = execFileSync(
|
|
5295
|
-
'git', ['show', `${ref}:${change.
|
|
5296
|
-
{ cwd: this.root, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }
|
|
3388
|
+
'git', ['show', `${ref}:${change.gitRelativePath}`],
|
|
3389
|
+
{ cwd: this.root, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
5297
3390
|
);
|
|
5298
3391
|
const fileLang = detectLanguage(change.filePath);
|
|
5299
3392
|
if (fileLang) {
|