@mishasinitcyn/betterrank 0.2.7 → 0.2.8
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 +141 -78
- package/src/index.js +2 -1
- package/src/parser.js +94 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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,11 @@ 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
|
+
];
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Disambiguate which targets a reference should wire to.
|
|
@@ -45,50 +50,89 @@ function disambiguateTargets(targets, sourceFile, graph) {
|
|
|
45
50
|
return targets;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
function
|
|
49
|
-
|
|
53
|
+
function normalizeFilePath(filePath) {
|
|
54
|
+
return filePath.replace(/\\/g, '/');
|
|
55
|
+
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
function buildDefinitionIndex(graph) {
|
|
58
|
+
const defIndex = new Map();
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
graph.forEachNode((node, attrs) => {
|
|
61
|
+
if (attrs.type !== 'symbol') return;
|
|
62
|
+
if (!defIndex.has(attrs.name)) defIndex.set(attrs.name, []);
|
|
63
|
+
defIndex.get(attrs.name).push(node);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return defIndex;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildFileLookup(graph) {
|
|
70
|
+
const fileLookup = new Map();
|
|
71
|
+
|
|
72
|
+
graph.forEachNode((node, attrs) => {
|
|
73
|
+
if (attrs.type !== 'file') return;
|
|
74
|
+
fileLookup.set(normalizeFilePath(node), node);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return fileLookup;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveImportBase(sourceFile, specifier) {
|
|
81
|
+
const normalizedSource = normalizeFilePath(sourceFile);
|
|
82
|
+
const normalizedSpecifier = normalizeFilePath(specifier);
|
|
83
|
+
if (!normalizedSpecifier || normalizedSpecifier.startsWith('node:')) return null;
|
|
84
|
+
|
|
85
|
+
if (normalizedSpecifier.startsWith('.')) {
|
|
86
|
+
return posix.normalize(posix.join(posix.dirname(normalizedSource), normalizedSpecifier));
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
if (normalizedSpecifier.startsWith('~/') || normalizedSpecifier.startsWith('@/')) {
|
|
90
|
+
return posix.normalize(posix.join('src', normalizedSpecifier.slice(2)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (normalizedSpecifier.startsWith('/')) {
|
|
94
|
+
return posix.normalize(normalizedSpecifier.slice(1));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (normalizedSpecifier.startsWith('src/')) {
|
|
98
|
+
return posix.normalize(normalizedSpecifier);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasExplicitImportExtension(filePath) {
|
|
105
|
+
return IMPORT_RESOLVE_EXTENSIONS.some(ext => filePath.endsWith(ext));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveImportTargetFile(sourceFile, specifier, fileLookup) {
|
|
109
|
+
const importBase = resolveImportBase(sourceFile, specifier);
|
|
110
|
+
if (!importBase) return null;
|
|
111
|
+
|
|
112
|
+
const candidates = [importBase];
|
|
113
|
+
if (!hasExplicitImportExtension(importBase)) {
|
|
114
|
+
for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
|
|
115
|
+
candidates.push(`${importBase}${ext}`);
|
|
116
|
+
}
|
|
117
|
+
for (const ext of IMPORT_RESOLVE_EXTENSIONS) {
|
|
118
|
+
candidates.push(posix.join(importBase, `index${ext}`));
|
|
79
119
|
}
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
122
|
+
for (const candidate of candidates) {
|
|
123
|
+
const resolved = fileLookup.get(normalizeFilePath(candidate));
|
|
124
|
+
if (resolved) return resolved;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
85
129
|
|
|
86
|
-
|
|
130
|
+
function wireSymbolReferences(records, graph, defIndex, addedRefs, addedImports) {
|
|
131
|
+
for (const { file, references } of records) {
|
|
87
132
|
for (const ref of references) {
|
|
88
133
|
const targets = defIndex.get(ref.name);
|
|
89
134
|
if (!targets) continue;
|
|
90
135
|
|
|
91
|
-
// Disambiguate: resolve which targets to actually wire
|
|
92
136
|
const resolvedTargets = disambiguateTargets(targets, file, graph);
|
|
93
137
|
|
|
94
138
|
for (const target of resolvedTargets) {
|
|
@@ -110,35 +154,25 @@ function buildGraph(allSymbols) {
|
|
|
110
154
|
}
|
|
111
155
|
}
|
|
112
156
|
}
|
|
113
|
-
|
|
114
|
-
return graph;
|
|
115
157
|
}
|
|
116
158
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (const filePath of removedFiles) {
|
|
123
|
-
removeFileNodes(graph, filePath);
|
|
124
|
-
}
|
|
159
|
+
function wireExplicitImportEdges(records, graph, fileLookup, addedImports) {
|
|
160
|
+
for (const { file, imports = [] } of records) {
|
|
161
|
+
for (const entry of imports) {
|
|
162
|
+
const targetFile = resolveImportTargetFile(file, entry.path, fileLookup);
|
|
163
|
+
if (!targetFile || targetFile === file) continue;
|
|
125
164
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const defIndex = new Map();
|
|
165
|
+
const impKey = `${file}\0${targetFile}`;
|
|
166
|
+
if (addedImports.has(impKey)) continue;
|
|
129
167
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (attrs.type === 'symbol') {
|
|
133
|
-
if (!defIndex.has(attrs.name)) defIndex.set(attrs.name, []);
|
|
134
|
-
defIndex.get(attrs.name).push(node);
|
|
168
|
+
addedImports.add(impKey);
|
|
169
|
+
graph.addEdge(file, targetFile, { type: 'IMPORTS' });
|
|
135
170
|
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const addedRefs = new Set();
|
|
139
|
-
const addedImports = new Set();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
140
173
|
|
|
141
|
-
|
|
174
|
+
function mergeDefinitions(graph, records) {
|
|
175
|
+
for (const { file, definitions } of records) {
|
|
142
176
|
graph.mergeNode(file, { type: 'file', symbolCount: definitions.length });
|
|
143
177
|
|
|
144
178
|
for (const def of definitions) {
|
|
@@ -156,36 +190,65 @@ function updateGraphFiles(graph, removedFiles, newSymbols) {
|
|
|
156
190
|
localRefs: def.localRefs || null,
|
|
157
191
|
});
|
|
158
192
|
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
193
|
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
163
196
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
function buildGraph(allSymbols) {
|
|
198
|
+
const graph = new MultiDirectedGraph({ allowSelfLoops: false });
|
|
199
|
+
mergeDefinitions(graph, allSymbols);
|
|
200
|
+
const defIndex = buildDefinitionIndex(graph);
|
|
167
201
|
|
|
168
|
-
|
|
202
|
+
// Dedup: one REFERENCES edge and one IMPORTS edge per unique (source, target) pair
|
|
203
|
+
const addedRefs = new Set();
|
|
204
|
+
const addedImports = new Set();
|
|
205
|
+
wireSymbolReferences(allSymbols, graph, defIndex, addedRefs, addedImports);
|
|
206
|
+
wireExplicitImportEdges(allSymbols, graph, buildFileLookup(graph), addedImports);
|
|
169
207
|
|
|
170
|
-
|
|
171
|
-
|
|
208
|
+
return graph;
|
|
209
|
+
}
|
|
172
210
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Incrementally update the graph: remove all nodes for the given files,
|
|
213
|
+
* then re-add from fresh parse results.
|
|
214
|
+
*/
|
|
215
|
+
function updateGraphFiles(graph, removedFiles, newSymbols) {
|
|
216
|
+
const changedFiles = new Map(newSymbols.map(record => [record.file, record]));
|
|
178
217
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
218
|
+
for (const filePath of removedFiles) {
|
|
219
|
+
const nextRecord = changedFiles.get(filePath);
|
|
220
|
+
if (!nextRecord) {
|
|
221
|
+
removeFileNodes(graph, filePath);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const outgoingEdges = [];
|
|
226
|
+
graph.forEachOutEdge(filePath, edge => {
|
|
227
|
+
outgoingEdges.push(edge);
|
|
228
|
+
});
|
|
229
|
+
for (const edge of outgoingEdges) {
|
|
230
|
+
graph.dropEdge(edge);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const nextNames = new Set(nextRecord.definitions.map(def => def.name));
|
|
234
|
+
const staleNodes = [];
|
|
235
|
+
graph.forEachNode((node, attrs) => {
|
|
236
|
+
if (attrs.type !== 'symbol' || attrs.file !== filePath) return;
|
|
237
|
+
if (!nextNames.has(attrs.name)) staleNodes.push(node);
|
|
238
|
+
});
|
|
239
|
+
for (const node of staleNodes) {
|
|
240
|
+
graph.dropNode(node);
|
|
187
241
|
}
|
|
188
242
|
}
|
|
243
|
+
|
|
244
|
+
mergeDefinitions(graph, newSymbols);
|
|
245
|
+
|
|
246
|
+
const defIndex = buildDefinitionIndex(graph);
|
|
247
|
+
const fileLookup = buildFileLookup(graph);
|
|
248
|
+
const addedRefs = new Set();
|
|
249
|
+
const addedImports = new Set();
|
|
250
|
+
wireSymbolReferences(newSymbols, graph, defIndex, addedRefs, addedImports);
|
|
251
|
+
wireExplicitImportEdges(newSymbols, graph, fileLookup, addedImports);
|
|
189
252
|
}
|
|
190
253
|
|
|
191
254
|
function removeFileNodes(graph, filePath) {
|
package/src/index.js
CHANGED
|
@@ -578,6 +578,7 @@ 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>/.]|$)`);
|
|
581
582
|
// Fallback: import/from lines that reference the symbol
|
|
582
583
|
const importPattern = new RegExp(`(?:import|from)\\s.*\\b${escaped}\\b`);
|
|
583
584
|
|
|
@@ -604,7 +605,7 @@ class CodeIndex {
|
|
|
604
605
|
if (inDef) continue;
|
|
605
606
|
|
|
606
607
|
const line = lines[i];
|
|
607
|
-
if (!callPattern.test(line) && !importPattern.test(line)) continue;
|
|
608
|
+
if (!callPattern.test(line) && !jsxPattern.test(line) && !importPattern.test(line)) continue;
|
|
608
609
|
|
|
609
610
|
const start = Math.max(0, i - context);
|
|
610
611
|
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,6 +188,10 @@ const REF_QUERIES = {
|
|
|
178
188
|
(call_expression function: (identifier) @ref)
|
|
179
189
|
(import_specifier name: (identifier) @ref)
|
|
180
190
|
(import_clause (identifier) @ref)
|
|
191
|
+
(jsx_opening_element (identifier) @jsx_ref)
|
|
192
|
+
(jsx_self_closing_element (identifier) @jsx_ref)
|
|
193
|
+
(jsx_opening_element (member_expression (identifier) @jsx_ref))
|
|
194
|
+
(jsx_self_closing_element (member_expression (identifier) @jsx_ref))
|
|
181
195
|
`,
|
|
182
196
|
|
|
183
197
|
typescript: `
|
|
@@ -187,6 +201,17 @@ const REF_QUERIES = {
|
|
|
187
201
|
(type_identifier) @ref
|
|
188
202
|
`,
|
|
189
203
|
|
|
204
|
+
tsx: `
|
|
205
|
+
(call_expression function: (identifier) @ref)
|
|
206
|
+
(import_specifier name: (identifier) @ref)
|
|
207
|
+
(import_clause (identifier) @ref)
|
|
208
|
+
(type_identifier) @ref
|
|
209
|
+
(jsx_opening_element (identifier) @jsx_ref)
|
|
210
|
+
(jsx_self_closing_element (identifier) @jsx_ref)
|
|
211
|
+
(jsx_opening_element (member_expression (identifier) @jsx_ref))
|
|
212
|
+
(jsx_self_closing_element (member_expression (identifier) @jsx_ref))
|
|
213
|
+
`,
|
|
214
|
+
|
|
190
215
|
python: `
|
|
191
216
|
(call function: (identifier) @ref)
|
|
192
217
|
(decorator (identifier) @ref)
|
|
@@ -213,7 +238,21 @@ const REF_QUERIES = {
|
|
|
213
238
|
`,
|
|
214
239
|
};
|
|
215
240
|
|
|
216
|
-
|
|
241
|
+
const IMPORT_QUERIES = {
|
|
242
|
+
javascript: `
|
|
243
|
+
(import_statement source: (string) @import_path)
|
|
244
|
+
(export_statement source: (string) @import_path)
|
|
245
|
+
(call_expression function: (identifier) @require_fn arguments: (arguments (string) @import_path))
|
|
246
|
+
`,
|
|
247
|
+
|
|
248
|
+
typescript: `
|
|
249
|
+
(import_statement source: (string) @import_path)
|
|
250
|
+
(export_statement source: (string) @import_path)
|
|
251
|
+
(call_expression function: (identifier) @require_fn arguments: (arguments (string) @import_path))
|
|
252
|
+
`,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
IMPORT_QUERIES.tsx = IMPORT_QUERIES.typescript;
|
|
217
256
|
|
|
218
257
|
const KIND_MAP = {
|
|
219
258
|
function_declaration: 'function',
|
|
@@ -522,8 +561,28 @@ function extractSignature(node, langName) {
|
|
|
522
561
|
return sig.length > 200 ? sig.substring(0, 200) + '...' : sig;
|
|
523
562
|
}
|
|
524
563
|
|
|
564
|
+
function normalizeReferenceCapture(capture) {
|
|
565
|
+
if (!capture) return null;
|
|
566
|
+
const text = capture.node.text;
|
|
567
|
+
if (!text) return null;
|
|
568
|
+
|
|
569
|
+
if (capture.name === 'jsx_ref') {
|
|
570
|
+
return /^[A-Z]/.test(text) ? text : null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return text;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function normalizeImportPathCapture(capture) {
|
|
577
|
+
if (!capture) return null;
|
|
578
|
+
const text = capture.node.text;
|
|
579
|
+
if (!text) return null;
|
|
580
|
+
|
|
581
|
+
return text.replace(/^['"`]|['"`]$/g, '');
|
|
582
|
+
}
|
|
583
|
+
|
|
525
584
|
/**
|
|
526
|
-
* Parse a single source file and extract definitions
|
|
585
|
+
* Parse a single source file and extract definitions, references, and imports.
|
|
527
586
|
* Returns null if the language is unsupported.
|
|
528
587
|
*/
|
|
529
588
|
function parseFile(filePath, source, { includeOutlineDefinitions = false } = {}) {
|
|
@@ -540,6 +599,7 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
540
599
|
|
|
541
600
|
const definitions = [];
|
|
542
601
|
const references = [];
|
|
602
|
+
const imports = [];
|
|
543
603
|
|
|
544
604
|
const defQueryStr = DEF_QUERIES[langName] || null;
|
|
545
605
|
if (defQueryStr) {
|
|
@@ -567,12 +627,38 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
567
627
|
try {
|
|
568
628
|
const refQuery = new Parser.Query(lang, refQueryStr);
|
|
569
629
|
for (const match of refQuery.matches(tree.rootNode)) {
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
name
|
|
630
|
+
for (const capture of match.captures) {
|
|
631
|
+
if (capture.name !== 'ref' && capture.name !== 'jsx_ref') continue;
|
|
632
|
+
const name = normalizeReferenceCapture(capture);
|
|
633
|
+
if (!name) continue;
|
|
634
|
+
references.push({
|
|
635
|
+
name,
|
|
636
|
+
file: filePath,
|
|
637
|
+
line: capture.node.startPosition.row + 1,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} catch (e) {
|
|
642
|
+
// Degrade gracefully
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const importQueryStr = IMPORT_QUERIES[langName] || null;
|
|
647
|
+
if (importQueryStr) {
|
|
648
|
+
try {
|
|
649
|
+
const importQuery = new Parser.Query(lang, importQueryStr);
|
|
650
|
+
for (const match of importQuery.matches(tree.rootNode)) {
|
|
651
|
+
const requireCapture = match.captures.find(c => c.name === 'require_fn');
|
|
652
|
+
if (requireCapture && requireCapture.node.text !== 'require') continue;
|
|
653
|
+
|
|
654
|
+
const pathCapture = match.captures.find(c => c.name === 'import_path');
|
|
655
|
+
const path = normalizeImportPathCapture(pathCapture);
|
|
656
|
+
if (!path) continue;
|
|
657
|
+
|
|
658
|
+
imports.push({
|
|
659
|
+
path,
|
|
574
660
|
file: filePath,
|
|
575
|
-
line:
|
|
661
|
+
line: pathCapture.node.startPosition.row + 1,
|
|
576
662
|
});
|
|
577
663
|
}
|
|
578
664
|
} catch (e) {
|
|
@@ -601,7 +687,7 @@ function parseFile(filePath, source, { includeOutlineDefinitions = false } = {})
|
|
|
601
687
|
|
|
602
688
|
// No tree.delete()/parser.delete() needed — native GC handles cleanup
|
|
603
689
|
|
|
604
|
-
return { file: filePath, definitions, references };
|
|
690
|
+
return { file: filePath, definitions, references, imports };
|
|
605
691
|
}
|
|
606
692
|
|
|
607
693
|
export { parseFile, buildAstProfile, extractParamNames, SUPPORTED_EXTENSIONS, LANG_MAP };
|