@mishasinitcyn/betterrank 0.2.7 → 0.2.9
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/package.json +1 -1
- package/src/cache.js +14 -5
- package/src/graph.js +208 -80
- package/src/index.js +10 -1
- package/src/parser.js +209 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Structural code index with PageRank-ranked repo maps, symbol search, call-graph queries, and dependency analysis. Built on tree-sitter and graphology.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/cache.js
CHANGED
|
@@ -130,6 +130,7 @@ class CodeIndexCache {
|
|
|
130
130
|
this.graph = null;
|
|
131
131
|
this.mtimes = new Map();
|
|
132
132
|
this.initialized = false;
|
|
133
|
+
this.skipCacheLoadOnce = false;
|
|
133
134
|
this.extensions = opts.extensions || SUPPORTED_EXTENSIONS;
|
|
134
135
|
this.ignorePatterns = [...IGNORE_PATTERNS, ...(opts.ignore || [])];
|
|
135
136
|
}
|
|
@@ -142,11 +143,14 @@ class CodeIndexCache {
|
|
|
142
143
|
if (!this.initialized) {
|
|
143
144
|
await this._loadConfig();
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
if (!this.skipCacheLoadOnce) {
|
|
147
|
+
const cached = await loadGraph(this.cachePath);
|
|
148
|
+
if (cached) {
|
|
149
|
+
this.graph = cached.graph;
|
|
150
|
+
this.mtimes = cached.mtimes;
|
|
151
|
+
}
|
|
149
152
|
}
|
|
153
|
+
this.skipCacheLoadOnce = false;
|
|
150
154
|
this.initialized = true;
|
|
151
155
|
}
|
|
152
156
|
|
|
@@ -179,7 +183,11 @@ class CodeIndexCache {
|
|
|
179
183
|
updateGraphFiles(this.graph, allRemoved, newSymbols);
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
|
|
186
|
+
try {
|
|
187
|
+
await saveGraph(this.graph, this.mtimes, this.cachePath);
|
|
188
|
+
} catch {
|
|
189
|
+
// Cache persistence is best-effort. Keep the in-memory graph usable.
|
|
190
|
+
}
|
|
183
191
|
|
|
184
192
|
if (isColdStart) {
|
|
185
193
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
@@ -218,6 +226,7 @@ class CodeIndexCache {
|
|
|
218
226
|
this.graph = null;
|
|
219
227
|
this.mtimes = new Map();
|
|
220
228
|
this.initialized = false;
|
|
229
|
+
this.skipCacheLoadOnce = true;
|
|
221
230
|
|
|
222
231
|
// Delete the cache file
|
|
223
232
|
try {
|
package/src/graph.js
CHANGED
|
@@ -3,7 +3,7 @@ const { MultiDirectedGraph } = graphology;
|
|
|
3
3
|
import pagerankModule from 'graphology-metrics/centrality/pagerank.js';
|
|
4
4
|
const pagerank = pagerankModule.default || pagerankModule;
|
|
5
5
|
import { writeFile, readFile, mkdir } from 'fs/promises';
|
|
6
|
-
import { dirname } from 'path';
|
|
6
|
+
import { dirname, posix } from 'path';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Build a multi-directed graph from parsed symbol data.
|
|
@@ -20,6 +20,18 @@ import { dirname } from 'path';
|
|
|
20
20
|
// Names with more definitions than this (main, run, get, close, etc.) are
|
|
21
21
|
// too ambiguous to provide useful structural signal.
|
|
22
22
|
const AMBIGUITY_CAP = 5;
|
|
23
|
+
const IMPORT_RESOLVE_EXTENSIONS = [
|
|
24
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
25
|
+
'.py', '.rs', '.go', '.rb', '.java',
|
|
26
|
+
'.c', '.h', '.cpp', '.hpp', '.cc', '.cs', '.php',
|
|
27
|
+
];
|
|
28
|
+
const FOCUS_MAX_HOPS = 2;
|
|
29
|
+
const FOCUS_DISTANCE_WEIGHTS = new Map([
|
|
30
|
+
[0, 250],
|
|
31
|
+
[1, 12],
|
|
32
|
+
[2, 2.5],
|
|
33
|
+
]);
|
|
34
|
+
const FOCUS_DEFAULT_WEIGHT = 0.15;
|
|
23
35
|
|
|
24
36
|
/**
|
|
25
37
|
* Disambiguate which targets a reference should wire to.
|
|
@@ -45,51 +57,97 @@ function disambiguateTargets(targets, sourceFile, graph) {
|
|
|
45
57
|
return targets;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
function
|
|
49
|
-
|
|
60
|
+
function normalizeFilePath(filePath) {
|
|
61
|
+
return filePath.replace(/\\/g, '/');
|
|
62
|
+
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
function buildDefinitionIndex(graph) {
|
|
65
|
+
const defIndex = new Map();
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
graph.forEachNode((node, attrs) => {
|
|
68
|
+
if (attrs.type !== 'symbol') return;
|
|
69
|
+
if (!defIndex.has(attrs.name)) defIndex.set(attrs.name, []);
|
|
70
|
+
defIndex.get(attrs.name).push(node);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return defIndex;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildFileLookup(graph) {
|
|
77
|
+
const fileLookup = new Map();
|
|
78
|
+
|
|
79
|
+
graph.forEachNode((node, attrs) => {
|
|
80
|
+
if (attrs.type !== 'file') return;
|
|
81
|
+
fileLookup.set(normalizeFilePath(node), node);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return fileLookup;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveImportBase(sourceFile, specifier) {
|
|
88
|
+
const normalizedSource = normalizeFilePath(sourceFile);
|
|
89
|
+
const normalizedSpecifier = normalizeFilePath(specifier);
|
|
90
|
+
if (!normalizedSpecifier || normalizedSpecifier.startsWith('node:')) return null;
|
|
91
|
+
|
|
92
|
+
if (normalizedSpecifier.startsWith('.')) {
|
|
93
|
+
return posix.normalize(posix.join(posix.dirname(normalizedSource), normalizedSpecifier));
|
|
70
94
|
}
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
if (normalizedSpecifier.startsWith('~/') || normalizedSpecifier.startsWith('@/')) {
|
|
97
|
+
return posix.normalize(posix.join('src', normalizedSpecifier.slice(2)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (normalizedSpecifier.startsWith('/')) {
|
|
101
|
+
return posix.normalize(normalizedSpecifier.slice(1));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (normalizedSpecifier.startsWith('src/')) {
|
|
105
|
+
return posix.normalize(normalizedSpecifier);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasExplicitImportExtension(filePath) {
|
|
112
|
+
return IMPORT_RESOLVE_EXTENSIONS.some(ext => filePath.endsWith(ext));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveImportTargetFile(sourceFile, specifier, fileLookup) {
|
|
116
|
+
const importBase = resolveImportBase(sourceFile, specifier);
|
|
117
|
+
if (!importBase) return null;
|
|
118
|
+
|
|
119
|
+
const candidates = [importBase];
|
|
120
|
+
if (!hasExplicitImportExtension(importBase)) {
|
|
121
|
+
for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
|
|
122
|
+
candidates.push(`${importBase}${ext}`);
|
|
123
|
+
}
|
|
124
|
+
for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
|
|
125
|
+
candidates.push(posix.join(importBase, `index${ext}`));
|
|
79
126
|
}
|
|
80
127
|
}
|
|
81
128
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
const resolved = fileLookup.get(normalizeFilePath(candidate));
|
|
131
|
+
if (resolved) return resolved;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
85
136
|
|
|
86
|
-
|
|
137
|
+
function wireSymbolReferences(records, graph, defIndex, addedRefs, addedImports) {
|
|
138
|
+
for (const { file, references } of records) {
|
|
87
139
|
for (const ref of references) {
|
|
88
140
|
const targets = defIndex.get(ref.name);
|
|
89
141
|
if (!targets) continue;
|
|
90
142
|
|
|
91
|
-
|
|
92
|
-
|
|
143
|
+
const filteredTargets = ref.kind === 'property'
|
|
144
|
+
? targets.filter(target => {
|
|
145
|
+
try { return graph.getNodeAttribute(target, 'kind') === 'property'; } catch { return false; }
|
|
146
|
+
})
|
|
147
|
+
: targets;
|
|
148
|
+
if (filteredTargets.length === 0) continue;
|
|
149
|
+
|
|
150
|
+
const resolvedTargets = disambiguateTargets(filteredTargets, file, graph);
|
|
93
151
|
|
|
94
152
|
for (const target of resolvedTargets) {
|
|
95
153
|
const targetFile = graph.getNodeAttribute(target, 'file');
|
|
@@ -110,35 +168,25 @@ function buildGraph(allSymbols) {
|
|
|
110
168
|
}
|
|
111
169
|
}
|
|
112
170
|
}
|
|
113
|
-
|
|
114
|
-
return graph;
|
|
115
171
|
}
|
|
116
172
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (const filePath of removedFiles) {
|
|
123
|
-
removeFileNodes(graph, filePath);
|
|
124
|
-
}
|
|
173
|
+
function wireExplicitImportEdges(records, graph, fileLookup, addedImports) {
|
|
174
|
+
for (const { file, imports = [] } of records) {
|
|
175
|
+
for (const entry of imports) {
|
|
176
|
+
const targetFile = resolveImportTargetFile(file, entry.path, fileLookup);
|
|
177
|
+
if (!targetFile || targetFile === file) continue;
|
|
125
178
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const defIndex = new Map();
|
|
179
|
+
const impKey = `${file}\0${targetFile}`;
|
|
180
|
+
if (addedImports.has(impKey)) continue;
|
|
129
181
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (attrs.type === 'symbol') {
|
|
133
|
-
if (!defIndex.has(attrs.name)) defIndex.set(attrs.name, []);
|
|
134
|
-
defIndex.get(attrs.name).push(node);
|
|
182
|
+
addedImports.add(impKey);
|
|
183
|
+
graph.addEdge(file, targetFile, { type: 'IMPORTS' });
|
|
135
184
|
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const addedRefs = new Set();
|
|
139
|
-
const addedImports = new Set();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
140
187
|
|
|
141
|
-
|
|
188
|
+
function mergeDefinitions(graph, records) {
|
|
189
|
+
for (const { file, definitions } of records) {
|
|
142
190
|
graph.mergeNode(file, { type: 'file', symbolCount: definitions.length });
|
|
143
191
|
|
|
144
192
|
for (const def of definitions) {
|
|
@@ -156,36 +204,65 @@ function updateGraphFiles(graph, removedFiles, newSymbols) {
|
|
|
156
204
|
localRefs: def.localRefs || null,
|
|
157
205
|
});
|
|
158
206
|
graph.addEdge(file, symbolKey, { type: 'DEFINES' });
|
|
159
|
-
|
|
160
|
-
if (!defIndex.has(def.name)) defIndex.set(def.name, []);
|
|
161
|
-
defIndex.get(def.name).push(symbolKey);
|
|
162
207
|
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
163
210
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
211
|
+
function buildGraph(allSymbols) {
|
|
212
|
+
const graph = new MultiDirectedGraph({ allowSelfLoops: false });
|
|
213
|
+
mergeDefinitions(graph, allSymbols);
|
|
214
|
+
const defIndex = buildDefinitionIndex(graph);
|
|
167
215
|
|
|
168
|
-
|
|
216
|
+
// Dedup: one REFERENCES edge and one IMPORTS edge per unique (source, target) pair
|
|
217
|
+
const addedRefs = new Set();
|
|
218
|
+
const addedImports = new Set();
|
|
219
|
+
wireSymbolReferences(allSymbols, graph, defIndex, addedRefs, addedImports);
|
|
220
|
+
wireExplicitImportEdges(allSymbols, graph, buildFileLookup(graph), addedImports);
|
|
169
221
|
|
|
170
|
-
|
|
171
|
-
|
|
222
|
+
return graph;
|
|
223
|
+
}
|
|
172
224
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Incrementally update the graph: remove all nodes for the given files,
|
|
227
|
+
* then re-add from fresh parse results.
|
|
228
|
+
*/
|
|
229
|
+
function updateGraphFiles(graph, removedFiles, newSymbols) {
|
|
230
|
+
const changedFiles = new Map(newSymbols.map(record => [record.file, record]));
|
|
178
231
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
232
|
+
for (const filePath of removedFiles) {
|
|
233
|
+
const nextRecord = changedFiles.get(filePath);
|
|
234
|
+
if (!nextRecord) {
|
|
235
|
+
removeFileNodes(graph, filePath);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const outgoingEdges = [];
|
|
240
|
+
graph.forEachOutEdge(filePath, edge => {
|
|
241
|
+
outgoingEdges.push(edge);
|
|
242
|
+
});
|
|
243
|
+
for (const edge of outgoingEdges) {
|
|
244
|
+
graph.dropEdge(edge);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const nextNames = new Set(nextRecord.definitions.map(def => def.name));
|
|
248
|
+
const staleNodes = [];
|
|
249
|
+
graph.forEachNode((node, attrs) => {
|
|
250
|
+
if (attrs.type !== 'symbol' || attrs.file !== filePath) return;
|
|
251
|
+
if (!nextNames.has(attrs.name)) staleNodes.push(node);
|
|
252
|
+
});
|
|
253
|
+
for (const node of staleNodes) {
|
|
254
|
+
graph.dropNode(node);
|
|
187
255
|
}
|
|
188
256
|
}
|
|
257
|
+
|
|
258
|
+
mergeDefinitions(graph, newSymbols);
|
|
259
|
+
|
|
260
|
+
const defIndex = buildDefinitionIndex(graph);
|
|
261
|
+
const fileLookup = buildFileLookup(graph);
|
|
262
|
+
const addedRefs = new Set();
|
|
263
|
+
const addedImports = new Set();
|
|
264
|
+
wireSymbolReferences(newSymbols, graph, defIndex, addedRefs, addedImports);
|
|
265
|
+
wireExplicitImportEdges(newSymbols, graph, fileLookup, addedImports);
|
|
189
266
|
}
|
|
190
267
|
|
|
191
268
|
function removeFileNodes(graph, filePath) {
|
|
@@ -200,6 +277,56 @@ function removeFileNodes(graph, filePath) {
|
|
|
200
277
|
}
|
|
201
278
|
}
|
|
202
279
|
|
|
280
|
+
function buildFocusDistanceMap(graph, focusFiles, maxHops = FOCUS_MAX_HOPS) {
|
|
281
|
+
const distances = new Map();
|
|
282
|
+
const queue = [];
|
|
283
|
+
|
|
284
|
+
for (const file of focusFiles) {
|
|
285
|
+
if (!graph.hasNode(file)) continue;
|
|
286
|
+
const attrs = graph.getNodeAttributes(file);
|
|
287
|
+
if (attrs.type !== 'file') continue;
|
|
288
|
+
distances.set(file, 0);
|
|
289
|
+
queue.push(file);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (let i = 0; i < queue.length; i++) {
|
|
293
|
+
const current = queue[i];
|
|
294
|
+
const currentDistance = distances.get(current);
|
|
295
|
+
if (currentDistance >= maxHops) continue;
|
|
296
|
+
|
|
297
|
+
const visitNeighbor = neighbor => {
|
|
298
|
+
if (!graph.hasNode(neighbor)) return;
|
|
299
|
+
const neighborAttrs = graph.getNodeAttributes(neighbor);
|
|
300
|
+
if (neighborAttrs.type !== 'file') return;
|
|
301
|
+
|
|
302
|
+
const nextDistance = currentDistance + 1;
|
|
303
|
+
const existing = distances.get(neighbor);
|
|
304
|
+
if (existing !== undefined && existing <= nextDistance) return;
|
|
305
|
+
distances.set(neighbor, nextDistance);
|
|
306
|
+
queue.push(neighbor);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
graph.forEachOutEdge(current, (_edge, attrs, _source, target) => {
|
|
310
|
+
if (attrs.type !== 'IMPORTS') return;
|
|
311
|
+
visitNeighbor(target);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
graph.forEachInEdge(current, (_edge, attrs, source) => {
|
|
315
|
+
if (attrs.type !== 'IMPORTS') return;
|
|
316
|
+
visitNeighbor(source);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return distances;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getFocusWeight(filePath, focusDistances) {
|
|
324
|
+
if (!focusDistances || focusDistances.size === 0) return 1.0;
|
|
325
|
+
const distance = focusDistances.get(filePath);
|
|
326
|
+
if (distance === undefined) return FOCUS_DEFAULT_WEIGHT;
|
|
327
|
+
return FOCUS_DISTANCE_WEIGHTS.get(distance) || FOCUS_DEFAULT_WEIGHT;
|
|
328
|
+
}
|
|
329
|
+
|
|
203
330
|
// Path-tier dampening: files outside core source directories get their
|
|
204
331
|
// PageRank scores multiplied by a fraction. This prevents scripts, tests,
|
|
205
332
|
// and temp files from dominating the map output over actual source code.
|
|
@@ -235,6 +362,7 @@ function rankedSymbols(graph, focusFiles = [], pathTiers = DEFAULT_PATH_TIERS) {
|
|
|
235
362
|
if (graph.order === 0) return [];
|
|
236
363
|
|
|
237
364
|
const g = graph.copy();
|
|
365
|
+
const focusDistances = focusFiles.length > 0 ? buildFocusDistanceMap(graph, focusFiles) : null;
|
|
238
366
|
|
|
239
367
|
if (focusFiles.length > 0) {
|
|
240
368
|
g.mergeNode('__focus__', { type: 'virtual' });
|
|
@@ -266,7 +394,7 @@ function rankedSymbols(graph, focusFiles = [], pathTiers = DEFAULT_PATH_TIERS) {
|
|
|
266
394
|
.map(([key, score]) => {
|
|
267
395
|
try {
|
|
268
396
|
const file = graph.getNodeAttribute(key, 'file');
|
|
269
|
-
return [key, score * getPathWeight(file, pathTiers)];
|
|
397
|
+
return [key, score * getPathWeight(file, pathTiers) * getFocusWeight(file, focusDistances)];
|
|
270
398
|
} catch {
|
|
271
399
|
return [key, score];
|
|
272
400
|
}
|
package/src/index.js
CHANGED
|
@@ -578,6 +578,9 @@ class CodeIndex {
|
|
|
578
578
|
// Match call sites: symbol followed by ( — avoids string literals and definitions
|
|
579
579
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
580
580
|
const callPattern = new RegExp(`(?<![a-zA-Z0-9_])${escaped}\\s*\\(`);
|
|
581
|
+
const jsxPattern = new RegExp(`<\\s*${escaped}(?=[\\s>/.]|$)`);
|
|
582
|
+
const memberPattern = new RegExp(`\\.\\s*${escaped}\\b`);
|
|
583
|
+
const bracketPattern = new RegExp(`\\[\\s*['"\`]${escaped}['"\`]\\s*\\]`);
|
|
581
584
|
// Fallback: import/from lines that reference the symbol
|
|
582
585
|
const importPattern = new RegExp(`(?:import|from)\\s.*\\b${escaped}\\b`);
|
|
583
586
|
|
|
@@ -604,7 +607,13 @@ class CodeIndex {
|
|
|
604
607
|
if (inDef) continue;
|
|
605
608
|
|
|
606
609
|
const line = lines[i];
|
|
607
|
-
if (
|
|
610
|
+
if (
|
|
611
|
+
!callPattern.test(line) &&
|
|
612
|
+
!jsxPattern.test(line) &&
|
|
613
|
+
!memberPattern.test(line) &&
|
|
614
|
+
!bracketPattern.test(line) &&
|
|
615
|
+
!importPattern.test(line)
|
|
616
|
+
) continue;
|
|
608
617
|
|
|
609
618
|
const start = Math.max(0, i - context);
|
|
610
619
|
const end = Math.min(lines.length - 1, i + context);
|
package/src/parser.js
CHANGED
|
@@ -82,6 +82,11 @@ const DEF_QUERIES = {
|
|
|
82
82
|
(export_statement declaration: (function_declaration name: (identifier) @name) @definition)
|
|
83
83
|
(export_statement declaration: (class_declaration name: (identifier) @name) @definition)
|
|
84
84
|
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (arrow_function) @_val)) @definition)
|
|
85
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (function_expression) @_val)) @definition)
|
|
86
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (call_expression) @_val)) @definition)
|
|
87
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (new_expression) @_val)) @definition)
|
|
88
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (object) @_val)) @definition)
|
|
89
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (array) @_val)) @definition)
|
|
85
90
|
`,
|
|
86
91
|
|
|
87
92
|
typescript: `
|
|
@@ -95,6 +100,11 @@ const DEF_QUERIES = {
|
|
|
95
100
|
(export_statement declaration: (function_declaration name: (identifier) @name) @definition)
|
|
96
101
|
(export_statement declaration: (class_declaration name: (type_identifier) @name) @definition)
|
|
97
102
|
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (arrow_function) @_val)) @definition)
|
|
103
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (function_expression) @_val)) @definition)
|
|
104
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (call_expression) @_val)) @definition)
|
|
105
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (new_expression) @_val)) @definition)
|
|
106
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (object) @_val)) @definition)
|
|
107
|
+
(export_statement declaration: (lexical_declaration (variable_declarator name: (identifier) @name value: (array) @_val)) @definition)
|
|
98
108
|
(export_statement declaration: (interface_declaration name: (type_identifier) @name) @definition)
|
|
99
109
|
(export_statement declaration: (type_alias_declaration name: (type_identifier) @name) @definition)
|
|
100
110
|
(export_statement declaration: (enum_declaration name: (identifier) @name) @definition)
|
|
@@ -178,15 +188,33 @@ const REF_QUERIES = {
|
|
|
178
188
|
(call_expression function: (identifier) @ref)
|
|
179
189
|
(import_specifier name: (identifier) @ref)
|
|
180
190
|
(import_clause (identifier) @ref)
|
|
191
|
+
(member_expression property: (property_identifier) @prop_ref)
|
|
192
|
+
(jsx_opening_element (identifier) @jsx_ref)
|
|
193
|
+
(jsx_self_closing_element (identifier) @jsx_ref)
|
|
194
|
+
(jsx_opening_element (member_expression (identifier) @jsx_ref))
|
|
195
|
+
(jsx_self_closing_element (member_expression (identifier) @jsx_ref))
|
|
181
196
|
`,
|
|
182
197
|
|
|
183
198
|
typescript: `
|
|
184
199
|
(call_expression function: (identifier) @ref)
|
|
185
200
|
(import_specifier name: (identifier) @ref)
|
|
186
201
|
(import_clause (identifier) @ref)
|
|
202
|
+
(member_expression property: (property_identifier) @prop_ref)
|
|
187
203
|
(type_identifier) @ref
|
|
188
204
|
`,
|
|
189
205
|
|
|
206
|
+
tsx: `
|
|
207
|
+
(call_expression function: (identifier) @ref)
|
|
208
|
+
(import_specifier name: (identifier) @ref)
|
|
209
|
+
(import_clause (identifier) @ref)
|
|
210
|
+
(member_expression property: (property_identifier) @prop_ref)
|
|
211
|
+
(type_identifier) @ref
|
|
212
|
+
(jsx_opening_element (identifier) @jsx_ref)
|
|
213
|
+
(jsx_self_closing_element (identifier) @jsx_ref)
|
|
214
|
+
(jsx_opening_element (member_expression (identifier) @jsx_ref))
|
|
215
|
+
(jsx_self_closing_element (member_expression (identifier) @jsx_ref))
|
|
216
|
+
`,
|
|
217
|
+
|
|
190
218
|
python: `
|
|
191
219
|
(call function: (identifier) @ref)
|
|
192
220
|
(decorator (identifier) @ref)
|
|
@@ -213,7 +241,21 @@ const REF_QUERIES = {
|
|
|
213
241
|
`,
|
|
214
242
|
};
|
|
215
243
|
|
|
216
|
-
|
|
244
|
+
const IMPORT_QUERIES = {
|
|
245
|
+
javascript: `
|
|
246
|
+
(import_statement source: (string) @import_path)
|
|
247
|
+
(export_statement source: (string) @import_path)
|
|
248
|
+
(call_expression function: (identifier) @require_fn arguments: (arguments (string) @import_path))
|
|
249
|
+
`,
|
|
250
|
+
|
|
251
|
+
typescript: `
|
|
252
|
+
(import_statement source: (string) @import_path)
|
|
253
|
+
(export_statement source: (string) @import_path)
|
|
254
|
+
(call_expression function: (identifier) @require_fn arguments: (arguments (string) @import_path))
|
|
255
|
+
`,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
IMPORT_QUERIES.tsx = IMPORT_QUERIES.typescript;
|
|
217
259
|
|
|
218
260
|
const KIND_MAP = {
|
|
219
261
|
function_declaration: 'function',
|
|
@@ -248,8 +290,12 @@ const KIND_MAP = {
|
|
|
248
290
|
};
|
|
249
291
|
|
|
250
292
|
const OUTLINE_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
|
|
293
|
+
const INDEX_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
|
|
251
294
|
const OUTLINE_BODY_TYPES = new Set(['statement_block', 'class_body', 'object', 'array']);
|
|
252
295
|
const OUTLINE_CALL_TYPES = new Set(['call_expression', 'new_expression']);
|
|
296
|
+
const SEMANTIC_CONTAINER_NAME_RE = /(router|routes|handlers|actions|reducers|selectors|queries|mutations|registry|registries|map|maps|config|configs|options|endpoints|procedures)$/i;
|
|
297
|
+
const NON_SEMANTIC_CONTAINER_NAME_RE = /(schema|shape|validator|payload|params?|props?|input|output|response|request|dto)$/i;
|
|
298
|
+
const NON_SEMANTIC_CALLEE_NAME_RE = /^(object|array|enum|union|literal|record|tuple|pick|omit|extend|merge|intersection|partial|strictObject|looseObject)$/i;
|
|
253
299
|
|
|
254
300
|
/**
|
|
255
301
|
* Walk an AST subtree and count node types that reveal structural shape.
|
|
@@ -449,6 +495,111 @@ function hasMultilinePairChildren(node) {
|
|
|
449
495
|
return false;
|
|
450
496
|
}
|
|
451
497
|
|
|
498
|
+
function isTopLevelVariableDeclarator(node) {
|
|
499
|
+
if (!node || node.type !== 'variable_declarator') return false;
|
|
500
|
+
const decl = node.parent;
|
|
501
|
+
if (!decl || decl.type !== 'lexical_declaration') return false;
|
|
502
|
+
const container = decl.parent;
|
|
503
|
+
return !!container && (container.type === 'program' || container.type === 'export_statement');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function extractDeclaratorName(node) {
|
|
507
|
+
const nameNode = node?.childForFieldName('name');
|
|
508
|
+
return nameNode?.type === 'identifier' ? nameNode.text : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function extractCalleeLeafName(node) {
|
|
512
|
+
if (!node) return null;
|
|
513
|
+
|
|
514
|
+
if (['identifier', 'property_identifier', 'private_property_identifier', 'type_identifier'].includes(node.type)) {
|
|
515
|
+
return node.text;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (node.type === 'member_expression') {
|
|
519
|
+
const propertyNode = node.childForFieldName('property');
|
|
520
|
+
if (propertyNode) return extractCalleeLeafName(propertyNode);
|
|
521
|
+
return extractCalleeLeafName(node.namedChild(node.namedChildCount - 1));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function extractCallLikeCalleeName(node) {
|
|
528
|
+
if (!node || (node.type !== 'call_expression' && node.type !== 'new_expression')) return null;
|
|
529
|
+
|
|
530
|
+
const calleeNode = node.childForFieldName('function')
|
|
531
|
+
|| node.childForFieldName('constructor')
|
|
532
|
+
|| node.namedChild(0);
|
|
533
|
+
return extractCalleeLeafName(calleeNode);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function shouldIndexSemanticObjectMembers(node) {
|
|
537
|
+
if (!isTopLevelVariableDeclarator(node)) return false;
|
|
538
|
+
|
|
539
|
+
const variableName = extractDeclaratorName(node);
|
|
540
|
+
const valueNode = node.childForFieldName('value');
|
|
541
|
+
const calleeName = extractCallLikeCalleeName(valueNode);
|
|
542
|
+
|
|
543
|
+
const hasSemanticVariableName = !!variableName
|
|
544
|
+
&& SEMANTIC_CONTAINER_NAME_RE.test(variableName)
|
|
545
|
+
&& !NON_SEMANTIC_CONTAINER_NAME_RE.test(variableName);
|
|
546
|
+
const hasSemanticCalleeName = !!calleeName && !NON_SEMANTIC_CALLEE_NAME_RE.test(calleeName);
|
|
547
|
+
|
|
548
|
+
if (valueNode?.type === 'object') {
|
|
549
|
+
return hasSemanticVariableName;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (valueNode?.type === 'call_expression' || valueNode?.type === 'new_expression') {
|
|
553
|
+
return hasSemanticVariableName || hasSemanticCalleeName;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function collectDirectPairDefinitions(node, filePath, langName) {
|
|
560
|
+
if (!node || node.type !== 'object') return [];
|
|
561
|
+
|
|
562
|
+
const definitions = [];
|
|
563
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
564
|
+
const child = node.namedChild(i);
|
|
565
|
+
if (child.type !== 'pair') continue;
|
|
566
|
+
|
|
567
|
+
const name = extractPairName(child);
|
|
568
|
+
if (!name) continue;
|
|
569
|
+
if (child.endPosition.row <= child.startPosition.row) continue;
|
|
570
|
+
|
|
571
|
+
definitions.push(createDefinition(name, child, filePath, langName, {
|
|
572
|
+
bodyStartLine: child.startPosition.row + 2,
|
|
573
|
+
kind: 'property',
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return definitions;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function collectIndexDefinitions(rootNode, filePath, langName) {
|
|
581
|
+
if (!INDEX_EXTRA_LANGUAGES.has(langName)) return [];
|
|
582
|
+
|
|
583
|
+
const definitions = [];
|
|
584
|
+
|
|
585
|
+
function visit(node) {
|
|
586
|
+
if (node.type === 'variable_declarator' && shouldIndexSemanticObjectMembers(node)) {
|
|
587
|
+
const bodyNode = findBodyNode(node);
|
|
588
|
+
if (bodyNode && bodyNode.type === 'object') {
|
|
589
|
+
definitions.push(...collectDirectPairDefinitions(bodyNode, filePath, langName));
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
595
|
+
visit(node.namedChild(i));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
visit(rootNode);
|
|
600
|
+
return definitions;
|
|
601
|
+
}
|
|
602
|
+
|
|
452
603
|
function collectOutlineDefinitions(rootNode, filePath, langName) {
|
|
453
604
|
if (!OUTLINE_EXTRA_LANGUAGES.has(langName)) return [];
|
|
454
605
|
|
|
@@ -522,8 +673,28 @@ function extractSignature(node, langName) {
|
|
|
522
673
|
return sig.length > 200 ? sig.substring(0, 200) + '...' : sig;
|
|
523
674
|
}
|
|
524
675
|
|
|
676
|
+
function normalizeReferenceCapture(capture) {
|
|
677
|
+
if (!capture) return null;
|
|
678
|
+
const text = capture.node.text;
|
|
679
|
+
if (!text) return null;
|
|
680
|
+
|
|
681
|
+
if (capture.name === 'jsx_ref') {
|
|
682
|
+
return /^[A-Z]/.test(text) ? text : null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return text;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function normalizeImportPathCapture(capture) {
|
|
689
|
+
if (!capture) return null;
|
|
690
|
+
const text = capture.node.text;
|
|
691
|
+
if (!text) return null;
|
|
692
|
+
|
|
693
|
+
return text.replace(/^['"`]|['"`]$/g, '');
|
|
694
|
+
}
|
|
695
|
+
|
|
525
696
|
/**
|
|
526
|
-
* Parse a single source file and extract definitions
|
|
697
|
+
* Parse a single source file and extract definitions, references, and imports.
|
|
527
698
|
* Returns null if the language is unsupported.
|
|
528
699
|
*/
|
|
529
700
|
function parseFile(filePath, source, { includeOutlineDefinitions = false } = {}) {
|
|
@@ -540,6 +711,7 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
540
711
|
|
|
541
712
|
const definitions = [];
|
|
542
713
|
const references = [];
|
|
714
|
+
const imports = [];
|
|
543
715
|
|
|
544
716
|
const defQueryStr = DEF_QUERIES[langName] || null;
|
|
545
717
|
if (defQueryStr) {
|
|
@@ -558,6 +730,8 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
558
730
|
}
|
|
559
731
|
}
|
|
560
732
|
|
|
733
|
+
definitions.push(...collectIndexDefinitions(tree.rootNode, filePath, langName));
|
|
734
|
+
|
|
561
735
|
if (includeOutlineDefinitions) {
|
|
562
736
|
definitions.push(...collectOutlineDefinitions(tree.rootNode, filePath, langName));
|
|
563
737
|
}
|
|
@@ -567,12 +741,39 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
567
741
|
try {
|
|
568
742
|
const refQuery = new Parser.Query(lang, refQueryStr);
|
|
569
743
|
for (const match of refQuery.matches(tree.rootNode)) {
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
name
|
|
744
|
+
for (const capture of match.captures) {
|
|
745
|
+
if (capture.name !== 'ref' && capture.name !== 'jsx_ref' && capture.name !== 'prop_ref') continue;
|
|
746
|
+
const name = normalizeReferenceCapture(capture);
|
|
747
|
+
if (!name) continue;
|
|
748
|
+
references.push({
|
|
749
|
+
name,
|
|
750
|
+
kind: capture.name === 'prop_ref' ? 'property' : 'symbol',
|
|
751
|
+
file: filePath,
|
|
752
|
+
line: capture.node.startPosition.row + 1,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch (e) {
|
|
757
|
+
// Degrade gracefully
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const importQueryStr = IMPORT_QUERIES[langName] || null;
|
|
762
|
+
if (importQueryStr) {
|
|
763
|
+
try {
|
|
764
|
+
const importQuery = new Parser.Query(lang, importQueryStr);
|
|
765
|
+
for (const match of importQuery.matches(tree.rootNode)) {
|
|
766
|
+
const requireCapture = match.captures.find(c => c.name === 'require_fn');
|
|
767
|
+
if (requireCapture && requireCapture.node.text !== 'require') continue;
|
|
768
|
+
|
|
769
|
+
const pathCapture = match.captures.find(c => c.name === 'import_path');
|
|
770
|
+
const path = normalizeImportPathCapture(pathCapture);
|
|
771
|
+
if (!path) continue;
|
|
772
|
+
|
|
773
|
+
imports.push({
|
|
774
|
+
path,
|
|
574
775
|
file: filePath,
|
|
575
|
-
line:
|
|
776
|
+
line: pathCapture.node.startPosition.row + 1,
|
|
576
777
|
});
|
|
577
778
|
}
|
|
578
779
|
} catch (e) {
|
|
@@ -601,7 +802,7 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
601
802
|
|
|
602
803
|
// No tree.delete()/parser.delete() needed — native GC handles cleanup
|
|
603
804
|
|
|
604
|
-
return { file: filePath, definitions, references };
|
|
805
|
+
return { file: filePath, definitions, references, imports };
|
|
605
806
|
}
|
|
606
807
|
|
|
607
808
|
export { parseFile, buildAstProfile, extractParamNames, SUPPORTED_EXTENSIONS, LANG_MAP };
|