@mishasinitcyn/betterrank 0.2.6 → 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/outline.js +1 -1
- package/src/parser.js +237 -44
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/outline.js
CHANGED
|
@@ -25,7 +25,7 @@ export function buildOutline(source, filePath, expandSymbols = [], { callerCount
|
|
|
25
25
|
return rawView(lines, pad);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const parsed = parseFile(filePath, source);
|
|
28
|
+
const parsed = parseFile(filePath, source, { includeOutlineDefinitions: true });
|
|
29
29
|
if (!parsed || parsed.definitions.length === 0) {
|
|
30
30
|
return rawView(lines, pad);
|
|
31
31
|
}
|
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',
|
|
@@ -244,8 +283,13 @@ const KIND_MAP = {
|
|
|
244
283
|
variable_declarator: 'variable',
|
|
245
284
|
namespace_definition: 'namespace',
|
|
246
285
|
decorated_definition: 'function',
|
|
286
|
+
pair: 'property',
|
|
247
287
|
};
|
|
248
288
|
|
|
289
|
+
const OUTLINE_EXTRA_LANGUAGES = new Set(['javascript', 'typescript', 'tsx']);
|
|
290
|
+
const OUTLINE_BODY_TYPES = new Set(['statement_block', 'class_body', 'object', 'array']);
|
|
291
|
+
const OUTLINE_CALL_TYPES = new Set(['call_expression', 'new_expression']);
|
|
292
|
+
|
|
249
293
|
/**
|
|
250
294
|
* Walk an AST subtree and count node types that reveal structural shape.
|
|
251
295
|
* Returns a flat object like { if_statement: 3, for_statement: 1, call_expression: 7, ... }
|
|
@@ -343,22 +387,146 @@ function extractParamNames(node) {
|
|
|
343
387
|
* Find the body/block node of a definition, drilling into wrappers like
|
|
344
388
|
* lexical_declaration → variable_declarator → arrow_function → body.
|
|
345
389
|
*/
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
|
|
390
|
+
function findCallArgumentBody(node, depth) {
|
|
391
|
+
if (!node) return null;
|
|
392
|
+
let fallback = null;
|
|
349
393
|
|
|
350
394
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
351
395
|
const child = node.namedChild(i);
|
|
352
|
-
body = child
|
|
353
|
-
if (body)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (body) return body;
|
|
396
|
+
const body = findBodyNode(child, depth + 1);
|
|
397
|
+
if (!body) continue;
|
|
398
|
+
if (body.type === 'statement_block' || body.type === 'class_body') {
|
|
399
|
+
return body;
|
|
357
400
|
}
|
|
401
|
+
if (!fallback) fallback = body;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return fallback;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function findBodyNode(node, depth = 0) {
|
|
408
|
+
if (!node || depth > 12) return null;
|
|
409
|
+
if (OUTLINE_BODY_TYPES.has(node.type)) return node;
|
|
410
|
+
|
|
411
|
+
const body = node.childForFieldName('body');
|
|
412
|
+
if (body) {
|
|
413
|
+
const nestedBody = findBodyNode(body, depth + 1);
|
|
414
|
+
return nestedBody || body;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (OUTLINE_CALL_TYPES.has(node.type)) {
|
|
418
|
+
const argBody = findCallArgumentBody(node.childForFieldName('arguments'), depth);
|
|
419
|
+
if (argBody) return argBody;
|
|
358
420
|
}
|
|
421
|
+
|
|
422
|
+
for (const fieldName of ['value', 'expression', 'argument']) {
|
|
423
|
+
const child = node.childForFieldName(fieldName);
|
|
424
|
+
if (!child) continue;
|
|
425
|
+
const nestedBody = findBodyNode(child, depth + 1);
|
|
426
|
+
if (nestedBody) return nestedBody;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
430
|
+
const nestedBody = findBodyNode(node.namedChild(i), depth + 1);
|
|
431
|
+
if (nestedBody) return nestedBody;
|
|
432
|
+
}
|
|
433
|
+
|
|
359
434
|
return null;
|
|
360
435
|
}
|
|
361
436
|
|
|
437
|
+
function computeBodyStartLine(node, bodyNode) {
|
|
438
|
+
if (!bodyNode) return null;
|
|
439
|
+
|
|
440
|
+
const bodyRow = bodyNode.startPosition.row;
|
|
441
|
+
const defRow = node.startPosition.row;
|
|
442
|
+
return bodyRow === defRow ? bodyRow + 2 : bodyRow + 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function createDefinition(name, node, filePath, langName, { bodyStartLine = null, kind } = {}) {
|
|
446
|
+
const bodyNode = bodyStartLine == null ? findBodyNode(node) : null;
|
|
447
|
+
const resolvedBodyStartLine = bodyStartLine ?? computeBodyStartLine(node, bodyNode);
|
|
448
|
+
const profileNode = bodyNode || node;
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
name,
|
|
452
|
+
kind: kind || nodeKind(node.type),
|
|
453
|
+
file: filePath,
|
|
454
|
+
lineStart: node.startPosition.row + 1,
|
|
455
|
+
lineEnd: node.endPosition.row + 1,
|
|
456
|
+
signature: extractSignature(node, langName),
|
|
457
|
+
bodyStartLine: resolvedBodyStartLine,
|
|
458
|
+
astProfile: buildAstProfile(profileNode),
|
|
459
|
+
paramNames: extractParamNames(node),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function extractPairName(node) {
|
|
464
|
+
const keyNode = node.childForFieldName('key') || node.namedChild(0);
|
|
465
|
+
if (!keyNode) return null;
|
|
466
|
+
|
|
467
|
+
if (['property_identifier', 'identifier', 'private_property_identifier', 'number'].includes(keyNode.type)) {
|
|
468
|
+
return keyNode.text;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (keyNode.type === 'string') {
|
|
472
|
+
return keyNode.text.replace(/^['"`]|['"`]$/g, '');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function hasMultilinePairChildren(node) {
|
|
479
|
+
if (!node || node.type !== 'object') return false;
|
|
480
|
+
|
|
481
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
482
|
+
const child = node.namedChild(i);
|
|
483
|
+
if (child.type !== 'pair') continue;
|
|
484
|
+
if (!extractPairName(child)) continue;
|
|
485
|
+
if (child.endPosition.row > child.startPosition.row) return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function collectOutlineDefinitions(rootNode, filePath, langName) {
|
|
492
|
+
if (!OUTLINE_EXTRA_LANGUAGES.has(langName)) return [];
|
|
493
|
+
|
|
494
|
+
const definitions = [];
|
|
495
|
+
|
|
496
|
+
function visit(node) {
|
|
497
|
+
if (node.type === 'pair') {
|
|
498
|
+
const name = extractPairName(node);
|
|
499
|
+
if (name && node.endPosition.row > node.startPosition.row) {
|
|
500
|
+
definitions.push(createDefinition(name, node, filePath, langName, {
|
|
501
|
+
bodyStartLine: node.startPosition.row + 2,
|
|
502
|
+
kind: 'property',
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
} else if (node.type === 'variable_declarator') {
|
|
506
|
+
const nameNode = node.childForFieldName('name');
|
|
507
|
+
const bodyNode = findBodyNode(node);
|
|
508
|
+
if (
|
|
509
|
+
nameNode &&
|
|
510
|
+
nameNode.type === 'identifier' &&
|
|
511
|
+
node.endPosition.row > node.startPosition.row &&
|
|
512
|
+
!(bodyNode && bodyNode.type === 'object' && hasMultilinePairChildren(bodyNode))
|
|
513
|
+
) {
|
|
514
|
+
definitions.push(createDefinition(nameNode.text, node, filePath, langName, {
|
|
515
|
+
bodyStartLine: node.startPosition.row + 2,
|
|
516
|
+
kind: 'variable',
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
522
|
+
visit(node.namedChild(i));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
visit(rootNode);
|
|
527
|
+
return definitions;
|
|
528
|
+
}
|
|
529
|
+
|
|
362
530
|
function nodeKind(nodeType) {
|
|
363
531
|
return KIND_MAP[nodeType] || 'other';
|
|
364
532
|
}
|
|
@@ -393,11 +561,31 @@ function extractSignature(node, langName) {
|
|
|
393
561
|
return sig.length > 200 ? sig.substring(0, 200) + '...' : sig;
|
|
394
562
|
}
|
|
395
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
|
+
|
|
396
584
|
/**
|
|
397
|
-
* Parse a single source file and extract definitions
|
|
585
|
+
* Parse a single source file and extract definitions, references, and imports.
|
|
398
586
|
* Returns null if the language is unsupported.
|
|
399
587
|
*/
|
|
400
|
-
function parseFile(filePath, source) {
|
|
588
|
+
function parseFile(filePath, source, { includeOutlineDefinitions = false } = {}) {
|
|
401
589
|
const dotIdx = filePath.lastIndexOf('.');
|
|
402
590
|
if (dotIdx === -1) return null;
|
|
403
591
|
const ext = filePath.substring(dotIdx);
|
|
@@ -411,6 +599,7 @@ function parseFile(filePath, source) {
|
|
|
411
599
|
|
|
412
600
|
const definitions = [];
|
|
413
601
|
const references = [];
|
|
602
|
+
const imports = [];
|
|
414
603
|
|
|
415
604
|
const defQueryStr = DEF_QUERIES[langName] || null;
|
|
416
605
|
if (defQueryStr) {
|
|
@@ -422,50 +611,54 @@ function parseFile(filePath, source) {
|
|
|
422
611
|
if (!nameCapture) continue;
|
|
423
612
|
const defNode = defCapture || nameCapture;
|
|
424
613
|
|
|
425
|
-
|
|
426
|
-
const bodyNode = findBodyNode(defNode.node);
|
|
427
|
-
let bodyStartLine = null;
|
|
428
|
-
if (bodyNode) {
|
|
429
|
-
const bodyRow = bodyNode.startPosition.row; // 0-indexed
|
|
430
|
-
const defRow = defNode.node.startPosition.row; // 0-indexed
|
|
431
|
-
// If body opens on same line as def (JS: `function foo() {`),
|
|
432
|
-
// content starts on next line. Otherwise body IS the content.
|
|
433
|
-
bodyStartLine = bodyRow === defRow ? bodyRow + 2 : bodyRow + 1; // 1-indexed
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Build AST profile from function body (or whole node if no body)
|
|
437
|
-
const profileNode = bodyNode || defNode.node;
|
|
438
|
-
const astProfile = buildAstProfile(profileNode);
|
|
439
|
-
const paramNames = extractParamNames(defNode.node);
|
|
440
|
-
|
|
441
|
-
definitions.push({
|
|
442
|
-
name: nameCapture.node.text,
|
|
443
|
-
kind: nodeKind(defNode.node.type),
|
|
444
|
-
file: filePath,
|
|
445
|
-
lineStart: defNode.node.startPosition.row + 1,
|
|
446
|
-
lineEnd: defNode.node.endPosition.row + 1,
|
|
447
|
-
signature: extractSignature(defNode.node, langName),
|
|
448
|
-
bodyStartLine,
|
|
449
|
-
astProfile,
|
|
450
|
-
paramNames,
|
|
451
|
-
});
|
|
614
|
+
definitions.push(createDefinition(nameCapture.node.text, defNode.node, filePath, langName));
|
|
452
615
|
}
|
|
453
616
|
} catch (e) {
|
|
454
617
|
// Query may fail on some grammar versions; degrade gracefully
|
|
455
618
|
}
|
|
456
619
|
}
|
|
457
620
|
|
|
621
|
+
if (includeOutlineDefinitions) {
|
|
622
|
+
definitions.push(...collectOutlineDefinitions(tree.rootNode, filePath, langName));
|
|
623
|
+
}
|
|
624
|
+
|
|
458
625
|
const refQueryStr = REF_QUERIES[langName] || REF_QUERIES.default;
|
|
459
626
|
if (refQueryStr) {
|
|
460
627
|
try {
|
|
461
628
|
const refQuery = new Parser.Query(lang, refQueryStr);
|
|
462
629
|
for (const match of refQuery.matches(tree.rootNode)) {
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
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,
|
|
467
660
|
file: filePath,
|
|
468
|
-
line:
|
|
661
|
+
line: pathCapture.node.startPosition.row + 1,
|
|
469
662
|
});
|
|
470
663
|
}
|
|
471
664
|
} catch (e) {
|
|
@@ -494,7 +687,7 @@ function parseFile(filePath, source) {
|
|
|
494
687
|
|
|
495
688
|
// No tree.delete()/parser.delete() needed — native GC handles cleanup
|
|
496
689
|
|
|
497
|
-
return { file: filePath, definitions, references };
|
|
690
|
+
return { file: filePath, definitions, references, imports };
|
|
498
691
|
}
|
|
499
692
|
|
|
500
693
|
export { parseFile, buildAstProfile, extractParamNames, SUPPORTED_EXTENSIONS, LANG_MAP };
|