pi-hashline-edit-pro 6.1.2 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/index.ts +20 -12
- package/package.json +3 -2
- package/src/auto-read-all.ts +39 -21
- package/src/config-ui.ts +135 -55
- package/src/config.ts +27 -9
- package/src/copy-move.ts +4 -4
- package/src/grep.ts +3 -3
- package/src/insert.ts +3 -3
- package/src/outline/index.ts +45 -0
- package/src/outline/parsers.ts +42 -0
- package/src/outline/render.ts +166 -0
- package/src/outline/vendor/LICENSE +201 -0
- package/src/outline/vendor/README.md +17 -0
- package/src/outline/vendor/parsers/blade.ts +138 -0
- package/src/outline/vendor/parsers/index.ts +75 -0
- package/src/outline/vendor/parsers/java.ts +501 -0
- package/src/outline/vendor/parsers/manager.ts +1721 -0
- package/src/outline/vendor/parsers/php.ts +332 -0
- package/src/outline/vendor/parsers/sass.ts +416 -0
- package/src/outline/vendor/parsers/scss.ts +566 -0
- package/src/outline/vendor/types.ts +82 -0
- package/src/outline/vendor/wasm/LICENSES.md +37 -0
- package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/read.ts +108 -36
- package/src/replace-match.ts +3 -3
- package/src/replace-undo.ts +3 -3
- package/src/replace.ts +3 -3
- package/tool-prompts/read-guidelines.md +3 -0
- package/{prompts → tool-prompts}/read.md +2 -0
- package/prompts/read-guidelines.md +0 -1
- package/src/auto-read-all-state.ts +0 -27
- /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
- /package/{prompts → tool-prompts}/copy.md +0 -0
- /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
- /package/{prompts → tool-prompts}/grep.md +0 -0
- /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
- /package/{prompts → tool-prompts}/insert.md +0 -0
- /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/move-snippet.md +0 -0
- /package/{prompts → tool-prompts}/move.md +0 -0
- /package/{prompts → tool-prompts}/read-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-match.md +0 -0
- /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace.md +0 -0
- /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
|
@@ -0,0 +1,1721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter parser manager.
|
|
3
|
+
*
|
|
4
|
+
* Lazily initializes the WASM runtime and loads language grammars on demand.
|
|
5
|
+
* All parsers share a single Parser instance, re-configured per language.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { Parser, Language, type Node } from "web-tree-sitter";
|
|
10
|
+
import type { SymbolInfo, ImportInfo } from "../types.js";
|
|
11
|
+
import { extractJava, extractJavaImports } from "./java.js";
|
|
12
|
+
import { extractPhp } from "./php.js";
|
|
13
|
+
import { extractBlade } from "./blade.js";
|
|
14
|
+
import { extractScss, extractScssImports } from "./scss.js";
|
|
15
|
+
import { extractSass, extractSassImports } from "./sass.js";
|
|
16
|
+
|
|
17
|
+
// ---- WASM path resolution ----
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/** Known language grammars with their npm package + WASM file name. */
|
|
21
|
+
const GRAMMAR_REGISTRY: Record<
|
|
22
|
+
string,
|
|
23
|
+
{ package: string; wasm: string; label: string }
|
|
24
|
+
> = {
|
|
25
|
+
javascript: {
|
|
26
|
+
package: "tree-sitter-javascript",
|
|
27
|
+
wasm: "tree-sitter-javascript.wasm",
|
|
28
|
+
label: "JavaScript",
|
|
29
|
+
},
|
|
30
|
+
typescript: {
|
|
31
|
+
package: "tree-sitter-typescript",
|
|
32
|
+
wasm: "tree-sitter-typescript.wasm",
|
|
33
|
+
label: "TypeScript",
|
|
34
|
+
},
|
|
35
|
+
tsx: {
|
|
36
|
+
package: "tree-sitter-typescript",
|
|
37
|
+
wasm: "tree-sitter-tsx.wasm",
|
|
38
|
+
label: "TSX",
|
|
39
|
+
},
|
|
40
|
+
python: {
|
|
41
|
+
package: "tree-sitter-python",
|
|
42
|
+
wasm: "tree-sitter-python.wasm",
|
|
43
|
+
label: "Python",
|
|
44
|
+
},
|
|
45
|
+
go: {
|
|
46
|
+
package: "tree-sitter-go",
|
|
47
|
+
wasm: "tree-sitter-go.wasm",
|
|
48
|
+
label: "Go",
|
|
49
|
+
},
|
|
50
|
+
rust: {
|
|
51
|
+
package: "tree-sitter-rust",
|
|
52
|
+
wasm: "tree-sitter-rust.wasm",
|
|
53
|
+
label: "Rust",
|
|
54
|
+
},
|
|
55
|
+
solidity: {
|
|
56
|
+
package: "tree-sitter-solidity",
|
|
57
|
+
wasm: "tree-sitter-solidity.wasm",
|
|
58
|
+
label: "Solidity",
|
|
59
|
+
},
|
|
60
|
+
scss: {
|
|
61
|
+
package: "tree-sitter-scss",
|
|
62
|
+
wasm: "tree-sitter-scss.wasm",
|
|
63
|
+
label: "SCSS",
|
|
64
|
+
},
|
|
65
|
+
sass: {
|
|
66
|
+
package: "tree-sitter-sass",
|
|
67
|
+
wasm: "tree-sitter-sass.wasm",
|
|
68
|
+
label: "Sass",
|
|
69
|
+
},
|
|
70
|
+
java: {
|
|
71
|
+
package: "tree-sitter-java",
|
|
72
|
+
wasm: "tree-sitter-java.wasm",
|
|
73
|
+
label: "Java",
|
|
74
|
+
},
|
|
75
|
+
php: {
|
|
76
|
+
package: "tree-sitter-php",
|
|
77
|
+
wasm: "tree-sitter-php.wasm",
|
|
78
|
+
label: "PHP",
|
|
79
|
+
},
|
|
80
|
+
blade: {
|
|
81
|
+
package: "tree-sitter-blade",
|
|
82
|
+
wasm: "tree-sitter-blade.wasm",
|
|
83
|
+
label: "Blade",
|
|
84
|
+
},
|
|
85
|
+
markdown: {
|
|
86
|
+
package: "markdown",
|
|
87
|
+
wasm: "markdown.wasm",
|
|
88
|
+
label: "Markdown",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Map file extensions to grammar keys. */
|
|
93
|
+
const EXTENSION_MAP: Record<string, string> = {
|
|
94
|
+
".js": "javascript",
|
|
95
|
+
".mjs": "javascript",
|
|
96
|
+
".cjs": "javascript",
|
|
97
|
+
".jsx": "javascript",
|
|
98
|
+
".ts": "typescript",
|
|
99
|
+
".mts": "typescript",
|
|
100
|
+
".cts": "typescript",
|
|
101
|
+
".tsx": "tsx",
|
|
102
|
+
".py": "python",
|
|
103
|
+
".go": "go",
|
|
104
|
+
".rs": "rust",
|
|
105
|
+
".sol": "solidity",
|
|
106
|
+
".java": "java",
|
|
107
|
+
".scss": "scss",
|
|
108
|
+
".sass": "sass",
|
|
109
|
+
".blade.php": "blade",
|
|
110
|
+
".php": "php",
|
|
111
|
+
".md": "markdown",
|
|
112
|
+
".markdown": "markdown",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const BASENAME_MAP: Record<string, string> = {
|
|
116
|
+
artisan: "php",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ---- Singleton state ----
|
|
120
|
+
|
|
121
|
+
let initialized = false;
|
|
122
|
+
const loadedLanguages = new Map<string, Language>();
|
|
123
|
+
let sharedParser: Parser | null = null;
|
|
124
|
+
|
|
125
|
+
/** Initialize the WASM runtime (once). */
|
|
126
|
+
async function ensureInit(): Promise<void> {
|
|
127
|
+
if (initialized) return;
|
|
128
|
+
await Parser.init();
|
|
129
|
+
initialized = true;
|
|
130
|
+
sharedParser = new Parser();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Load a language grammar by key (cached). */
|
|
134
|
+
async function loadLanguage(key: string): Promise<Language> {
|
|
135
|
+
const cached = loadedLanguages.get(key);
|
|
136
|
+
if (cached) return cached;
|
|
137
|
+
|
|
138
|
+
const entry = GRAMMAR_REGISTRY[key];
|
|
139
|
+
if (!entry) throw new Error(`Unknown grammar: ${key}`);
|
|
140
|
+
|
|
141
|
+
const wasmPath = fileURLToPath(
|
|
142
|
+
new URL("../wasm/" + entry.wasm, import.meta.url),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const lang = await Language.load(wasmPath);
|
|
146
|
+
loadedLanguages.set(key, lang);
|
|
147
|
+
return lang;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- Public API ----
|
|
151
|
+
|
|
152
|
+
/** Detect grammar key from a file path's extension. */
|
|
153
|
+
export function detectLanguage(filePath: string): string | null {
|
|
154
|
+
const lower = filePath.toLowerCase();
|
|
155
|
+
const base = lower.split(/[/\\]/).pop() ?? lower;
|
|
156
|
+
const named = BASENAME_MAP[base];
|
|
157
|
+
if (named !== undefined) return named;
|
|
158
|
+
for (const [ext, key] of Object.entries(EXTENSION_MAP)) {
|
|
159
|
+
if (lower.endsWith(ext)) return key;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Get a human-readable label for a grammar key. */
|
|
165
|
+
export function languageLabel(key: string): string {
|
|
166
|
+
return GRAMMAR_REGISTRY[key]?.label ?? key;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Parse source code with a given grammar, returning SymbolInfo[]. */
|
|
170
|
+
export async function parseCode(
|
|
171
|
+
key: string,
|
|
172
|
+
source: string,
|
|
173
|
+
): Promise<SymbolInfo[]> {
|
|
174
|
+
// Markdown uses regex-based parsing, not tree-sitter
|
|
175
|
+
if (key === "markdown") {
|
|
176
|
+
return extractMarkdown(source);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await ensureInit();
|
|
180
|
+
|
|
181
|
+
const lang = await loadLanguage(key);
|
|
182
|
+
if (!sharedParser) throw new Error("Parser not initialized");
|
|
183
|
+
|
|
184
|
+
sharedParser.setLanguage(lang);
|
|
185
|
+
const tree = sharedParser.parse(source);
|
|
186
|
+
if (!tree) return [];
|
|
187
|
+
|
|
188
|
+
return extractSymbols(key, tree.rootNode, source, 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- Symbol extraction dispatch ----
|
|
192
|
+
|
|
193
|
+
function extractSymbols(
|
|
194
|
+
key: string,
|
|
195
|
+
node: Node,
|
|
196
|
+
source: string,
|
|
197
|
+
depth: number,
|
|
198
|
+
): SymbolInfo[] {
|
|
199
|
+
switch (key) {
|
|
200
|
+
case "javascript":
|
|
201
|
+
case "typescript":
|
|
202
|
+
case "tsx":
|
|
203
|
+
return extractJavaScriptLike(node, source, depth);
|
|
204
|
+
case "python":
|
|
205
|
+
return extractPython(node, source, depth);
|
|
206
|
+
case "go":
|
|
207
|
+
return extractGo(node, source, depth);
|
|
208
|
+
case "rust":
|
|
209
|
+
return extractRust(node, source, depth);
|
|
210
|
+
case "solidity":
|
|
211
|
+
return extractSolidity(node, source, depth);
|
|
212
|
+
case "scss":
|
|
213
|
+
return extractScss(node, source, depth);
|
|
214
|
+
case "sass":
|
|
215
|
+
return extractSass(node, source, depth);
|
|
216
|
+
case "java":
|
|
217
|
+
return extractJava(node, source, depth);
|
|
218
|
+
case "php":
|
|
219
|
+
return extractPhp(node, source, depth);
|
|
220
|
+
case "blade":
|
|
221
|
+
return extractBlade(node, source, depth);
|
|
222
|
+
case "markdown":
|
|
223
|
+
return extractMarkdown(source);
|
|
224
|
+
default:
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- Generic helpers ----
|
|
230
|
+
|
|
231
|
+
/** Get the text of the first named child matching a type. */
|
|
232
|
+
function childText(
|
|
233
|
+
node: Node,
|
|
234
|
+
childType: string,
|
|
235
|
+
source: string,
|
|
236
|
+
): string | undefined {
|
|
237
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
238
|
+
const c = node.namedChild(i);
|
|
239
|
+
if (c && c.type === childType) {
|
|
240
|
+
return source.slice(c.startIndex, c.endIndex);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Get the first field child's text (field-name-based access). */
|
|
247
|
+
function fieldChildText(
|
|
248
|
+
node: Node,
|
|
249
|
+
fieldName: string,
|
|
250
|
+
source: string,
|
|
251
|
+
): string | undefined {
|
|
252
|
+
const c = node.childForFieldName(fieldName);
|
|
253
|
+
return c ? source.slice(c.startIndex, c.endIndex) : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---- Node line ranges (0-indexed → 1-indexed) ----
|
|
257
|
+
|
|
258
|
+
function nodeRange(node: Node): [number, number] {
|
|
259
|
+
return [node.startPosition.row + 1, node.endPosition.row + 1];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ========================================================================
|
|
263
|
+
// JavaScript / TypeScript / TSX
|
|
264
|
+
// ========================================================================
|
|
265
|
+
|
|
266
|
+
function extractJavaScriptLike(
|
|
267
|
+
node: Node,
|
|
268
|
+
source: string,
|
|
269
|
+
depth: number,
|
|
270
|
+
): SymbolInfo[] {
|
|
271
|
+
const results: SymbolInfo[] = [];
|
|
272
|
+
const MAX_DEPTH = depth + 10; // track overall depth via recursion param
|
|
273
|
+
|
|
274
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
275
|
+
const child = node.namedChild(i);
|
|
276
|
+
if (!child) continue;
|
|
277
|
+
|
|
278
|
+
const symbol = extractJSChild(child, source);
|
|
279
|
+
if (!symbol) continue;
|
|
280
|
+
|
|
281
|
+
const declaration =
|
|
282
|
+
child.type === "export_statement"
|
|
283
|
+
? findChildByType(child, [
|
|
284
|
+
"class_declaration",
|
|
285
|
+
"class",
|
|
286
|
+
"abstract_class_declaration",
|
|
287
|
+
"interface_declaration",
|
|
288
|
+
"enum_declaration",
|
|
289
|
+
]) ?? child
|
|
290
|
+
: child;
|
|
291
|
+
|
|
292
|
+
// For class/interface/enum, look inside their bodies
|
|
293
|
+
if (
|
|
294
|
+
symbol.type === "class" ||
|
|
295
|
+
symbol.type === "interface" ||
|
|
296
|
+
symbol.type === "enum"
|
|
297
|
+
) {
|
|
298
|
+
const bodyNode = findChildByType(declaration, [
|
|
299
|
+
"class_body",
|
|
300
|
+
"interface_body",
|
|
301
|
+
"enum_body",
|
|
302
|
+
"object_type",
|
|
303
|
+
]);
|
|
304
|
+
if (bodyNode && depth < MAX_DEPTH) {
|
|
305
|
+
symbol.children = extractJavaScriptLike(bodyNode, source, depth + 1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
results.push(symbol);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return results;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const JS_SYMBOL_TYPES: Record<string, string> = {
|
|
316
|
+
function_declaration: "function",
|
|
317
|
+
function_expression: "function",
|
|
318
|
+
arrow_function: "arrow_function",
|
|
319
|
+
method_definition: "method",
|
|
320
|
+
class_declaration: "class",
|
|
321
|
+
class: "class",
|
|
322
|
+
interface_declaration: "interface",
|
|
323
|
+
enum_declaration: "enum",
|
|
324
|
+
type_alias_declaration: "type",
|
|
325
|
+
abstract_class_declaration: "class",
|
|
326
|
+
lexical_declaration: "variable",
|
|
327
|
+
variable_declaration: "variable",
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
function isFunctionNode(node: Node): boolean {
|
|
331
|
+
return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function extractJSChild(
|
|
335
|
+
child: Node,
|
|
336
|
+
source: string,
|
|
337
|
+
): SymbolInfo | null {
|
|
338
|
+
const type = child.type;
|
|
339
|
+
|
|
340
|
+
// Top-level declarations: functions, arrow functions, and variables
|
|
341
|
+
if (
|
|
342
|
+
type === "lexical_declaration" ||
|
|
343
|
+
type === "variable_declaration"
|
|
344
|
+
) {
|
|
345
|
+
const declarators: SymbolInfo[] = [];
|
|
346
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
347
|
+
const d = child.namedChild(j);
|
|
348
|
+
if (!d || d.type !== "variable_declarator") continue;
|
|
349
|
+
const name = fieldChildText(d, "name", source);
|
|
350
|
+
if (!name) continue;
|
|
351
|
+
const value = d.childForFieldName("value");
|
|
352
|
+
if (value && isFunctionNode(value)) {
|
|
353
|
+
const [sl, el] = nodeRange(d);
|
|
354
|
+
declarators.push({
|
|
355
|
+
name,
|
|
356
|
+
type: value.type === "arrow_function" ? "arrow_function" : "function",
|
|
357
|
+
startLine: sl,
|
|
358
|
+
endLine: el,
|
|
359
|
+
detail: extractFnDetail(value, source),
|
|
360
|
+
});
|
|
361
|
+
} else {
|
|
362
|
+
const [sl, el] = nodeRange(d);
|
|
363
|
+
declarators.push({
|
|
364
|
+
name,
|
|
365
|
+
type: "variable",
|
|
366
|
+
startLine: sl,
|
|
367
|
+
endLine: el,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (declarators.length === 1) return declarators[0];
|
|
372
|
+
if (declarators.length > 1) {
|
|
373
|
+
return {
|
|
374
|
+
name: "",
|
|
375
|
+
type: "variable",
|
|
376
|
+
startLine: declarators[0].startLine,
|
|
377
|
+
endLine: declarators[declarators.length - 1].endLine,
|
|
378
|
+
children: declarators,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Export statement — unwrap
|
|
385
|
+
if (type === "export_statement") {
|
|
386
|
+
const isDefault = child.text.startsWith("export default");
|
|
387
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
388
|
+
const inner = child.namedChild(j);
|
|
389
|
+
if (inner) {
|
|
390
|
+
const result = extractJSChild(inner, source);
|
|
391
|
+
if (result) {
|
|
392
|
+
if (isDefault && result.name === "<anonymous>") result.name = "default";
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (type === "expression_statement") return testCallSymbol(child, source);
|
|
401
|
+
|
|
402
|
+
const symbolType = JS_SYMBOL_TYPES[type];
|
|
403
|
+
if (!symbolType) return null;
|
|
404
|
+
|
|
405
|
+
const name =
|
|
406
|
+
fieldChildText(child, "name", source) ||
|
|
407
|
+
childText(child, "property_identifier", source) ||
|
|
408
|
+
childText(child, "type_identifier", source) ||
|
|
409
|
+
"<anonymous>";
|
|
410
|
+
|
|
411
|
+
const [sl, el] = nodeRange(child);
|
|
412
|
+
|
|
413
|
+
// For functions/methods, try to get the parameters as detail
|
|
414
|
+
let detail: string | undefined;
|
|
415
|
+
if (type === "function_declaration" || type === "function_expression" || type === "method_definition" || type === "arrow_function") {
|
|
416
|
+
detail = extractFnDetail(child, source);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return { name, type: symbolType, startLine: sl, endLine: el, detail };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const TEST_CALLEES = new Set(["describe", "test", "it", "suite", "context", "specify"]);
|
|
423
|
+
|
|
424
|
+
function testCallSymbol(child: Node, source: string): SymbolInfo | null {
|
|
425
|
+
const expr = child.namedChildCount > 0 ? child.namedChild(0) : null;
|
|
426
|
+
if (!expr || expr.type !== "call_expression") return null;
|
|
427
|
+
const fn = expr.childForFieldName("function");
|
|
428
|
+
const args = expr.childForFieldName("arguments");
|
|
429
|
+
if (!fn || !args) return null;
|
|
430
|
+
const callee = (source.slice(fn.startIndex, fn.endIndex).split(".")[0] ?? "").trim();
|
|
431
|
+
if (!TEST_CALLEES.has(callee)) return null;
|
|
432
|
+
const first = args.namedChildren[0];
|
|
433
|
+
if (!first || first.type !== "string") return null;
|
|
434
|
+
const name = source.slice(first.startIndex, first.endIndex).replace(/^["'`]|["'`]$/g, "");
|
|
435
|
+
const [sl, el] = nodeRange(expr);
|
|
436
|
+
const symbol: SymbolInfo = { name, type: callee, startLine: sl, endLine: el };
|
|
437
|
+
const children = extractTestCalls(expr, source);
|
|
438
|
+
if (children.length > 0) symbol.children = children;
|
|
439
|
+
return symbol;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function extractTestCalls(call: Node, source: string): SymbolInfo[] {
|
|
443
|
+
const results: SymbolInfo[] = [];
|
|
444
|
+
const args = call.childForFieldName("arguments");
|
|
445
|
+
for (const arg of args?.namedChildren ?? []) {
|
|
446
|
+
if (arg.type !== "arrow_function" && arg.type !== "function_expression" && arg.type !== "function") continue;
|
|
447
|
+
const body = arg.childForFieldName("body");
|
|
448
|
+
if (!body || body.type !== "statement_block") continue;
|
|
449
|
+
for (const stmt of body.namedChildren) {
|
|
450
|
+
if (stmt.type !== "expression_statement") continue;
|
|
451
|
+
const inner = testCallSymbol(stmt, source);
|
|
452
|
+
if (inner) results.push(inner);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return results;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function extractFnDetail(
|
|
459
|
+
node: Node,
|
|
460
|
+
source: string,
|
|
461
|
+
): string | undefined {
|
|
462
|
+
const params = node.childForFieldName("parameters");
|
|
463
|
+
if (params) {
|
|
464
|
+
return source.slice(params.startIndex, params.endIndex);
|
|
465
|
+
}
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function findChildByType(
|
|
470
|
+
node: Node,
|
|
471
|
+
types: string[],
|
|
472
|
+
): Node | null {
|
|
473
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
474
|
+
const c = node.child(i);
|
|
475
|
+
if (c && types.includes(c.type)) return c;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ========================================================================
|
|
481
|
+
// Python
|
|
482
|
+
// ========================================================================
|
|
483
|
+
|
|
484
|
+
function extractPython(
|
|
485
|
+
node: Node,
|
|
486
|
+
source: string,
|
|
487
|
+
depth: number,
|
|
488
|
+
): SymbolInfo[] {
|
|
489
|
+
const results: SymbolInfo[] = [];
|
|
490
|
+
const MAX_DEPTH = depth + 10;
|
|
491
|
+
|
|
492
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
493
|
+
const child = node.namedChild(i);
|
|
494
|
+
if (!child) continue;
|
|
495
|
+
|
|
496
|
+
let symbol: SymbolInfo | null = null;
|
|
497
|
+
|
|
498
|
+
if (child.type === "class_definition") {
|
|
499
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
500
|
+
const [sl, el] = nodeRange(child);
|
|
501
|
+
symbol = { name, type: "class", startLine: sl, endLine: el };
|
|
502
|
+
|
|
503
|
+
// Recurse into body for methods
|
|
504
|
+
const body = child.childForFieldName("body");
|
|
505
|
+
if (body && depth < MAX_DEPTH) {
|
|
506
|
+
symbol.children = extractPython(body, source, depth + 1);
|
|
507
|
+
}
|
|
508
|
+
} else if (child.type === "function_definition") {
|
|
509
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
510
|
+
const [sl, el] = nodeRange(child);
|
|
511
|
+
const params = child.childForFieldName("parameters");
|
|
512
|
+
const detail = params
|
|
513
|
+
? source.slice(params.startIndex, params.endIndex)
|
|
514
|
+
: undefined;
|
|
515
|
+
symbol = { name, type: "function", startLine: sl, endLine: el, detail };
|
|
516
|
+
} else if (child.type === "decorated_definition") {
|
|
517
|
+
// Unwrap to the inner definition
|
|
518
|
+
const def = child.namedChild(child.namedChildCount - 1);
|
|
519
|
+
if (def) {
|
|
520
|
+
const inner = extractPythonSingle(def, source);
|
|
521
|
+
if (inner) {
|
|
522
|
+
inner.type = "decorated_" + inner.type;
|
|
523
|
+
symbol = inner;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (symbol) results.push(symbol);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return results;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function extractPythonSingle(
|
|
535
|
+
child: Node,
|
|
536
|
+
source: string,
|
|
537
|
+
): SymbolInfo | null {
|
|
538
|
+
if (child.type === "class_definition") {
|
|
539
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
540
|
+
const [sl, el] = nodeRange(child);
|
|
541
|
+
return { name, type: "class", startLine: sl, endLine: el };
|
|
542
|
+
}
|
|
543
|
+
if (child.type === "function_definition") {
|
|
544
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
545
|
+
const [sl, el] = nodeRange(child);
|
|
546
|
+
const params = child.childForFieldName("parameters");
|
|
547
|
+
const detail = params
|
|
548
|
+
? source.slice(params.startIndex, params.endIndex)
|
|
549
|
+
: undefined;
|
|
550
|
+
return { name, type: "function", startLine: sl, endLine: el, detail };
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ========================================================================
|
|
556
|
+
// Go
|
|
557
|
+
// ========================================================================
|
|
558
|
+
|
|
559
|
+
function extractGo(
|
|
560
|
+
node: Node,
|
|
561
|
+
source: string,
|
|
562
|
+
depth: number,
|
|
563
|
+
): SymbolInfo[] {
|
|
564
|
+
const results: SymbolInfo[] = [];
|
|
565
|
+
const MAX_DEPTH = depth + 10;
|
|
566
|
+
|
|
567
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
568
|
+
const child = node.namedChild(i);
|
|
569
|
+
if (!child) continue;
|
|
570
|
+
|
|
571
|
+
let symbol: SymbolInfo | null = null;
|
|
572
|
+
|
|
573
|
+
if (child.type === "function_declaration") {
|
|
574
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
575
|
+
const [sl, el] = nodeRange(child);
|
|
576
|
+
symbol = { name, type: "function", startLine: sl, endLine: el };
|
|
577
|
+
} else if (child.type === "method_declaration") {
|
|
578
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
579
|
+
const receiver = child.childForFieldName("receiver");
|
|
580
|
+
const receiverText = receiver
|
|
581
|
+
? source.slice(receiver.startIndex, receiver.endIndex)
|
|
582
|
+
: "";
|
|
583
|
+
const [sl, el] = nodeRange(child);
|
|
584
|
+
const fullName = receiverText ? `${receiverText}).${name}` : name;
|
|
585
|
+
symbol = { name: fullName, type: "method", startLine: sl, endLine: el };
|
|
586
|
+
} else if (child.type === "type_declaration") {
|
|
587
|
+
// type_declaration wraps type_spec nodes
|
|
588
|
+
const specs: SymbolInfo[] = [];
|
|
589
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
590
|
+
const spec = child.namedChild(j);
|
|
591
|
+
if (!spec || spec.type !== "type_spec") continue;
|
|
592
|
+
const name = fieldChildText(spec, "name", source) || "<anonymous>";
|
|
593
|
+
const [sl, el] = nodeRange(spec);
|
|
594
|
+
const typeNode = spec.childForFieldName("type");
|
|
595
|
+
const kind = typeNode?.type === "struct_type"
|
|
596
|
+
? "struct"
|
|
597
|
+
: typeNode?.type === "interface_type"
|
|
598
|
+
? "interface"
|
|
599
|
+
: "type";
|
|
600
|
+
const entry: SymbolInfo = { name, type: kind, startLine: sl, endLine: el };
|
|
601
|
+
|
|
602
|
+
// Recurse into struct/interface body
|
|
603
|
+
if (typeNode && depth < MAX_DEPTH) {
|
|
604
|
+
const body = findChildByType(typeNode, [
|
|
605
|
+
"field_declaration_list",
|
|
606
|
+
]);
|
|
607
|
+
if (body) {
|
|
608
|
+
entry.children = extractGoFields(body, source);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
specs.push(entry);
|
|
613
|
+
}
|
|
614
|
+
if (specs.length === 1) {
|
|
615
|
+
symbol = specs[0];
|
|
616
|
+
} else if (specs.length > 1) {
|
|
617
|
+
symbol = {
|
|
618
|
+
name: "",
|
|
619
|
+
type: "type",
|
|
620
|
+
startLine: specs[0].startLine,
|
|
621
|
+
endLine: specs[specs.length - 1].endLine,
|
|
622
|
+
children: specs,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
if (symbol) results.push(symbol);
|
|
626
|
+
continue;
|
|
627
|
+
} else if (child.type === "const_declaration") {
|
|
628
|
+
const [sl, el] = nodeRange(child);
|
|
629
|
+
const names: string[] = [];
|
|
630
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
631
|
+
const spec = child.namedChild(j);
|
|
632
|
+
if (spec && spec.type === "const_spec") {
|
|
633
|
+
const n = fieldChildText(spec, "name", source);
|
|
634
|
+
if (n) names.push(n);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
symbol = {
|
|
638
|
+
name: names.join(", ") || "const",
|
|
639
|
+
type: "const",
|
|
640
|
+
startLine: sl,
|
|
641
|
+
endLine: el,
|
|
642
|
+
};
|
|
643
|
+
} else if (child.type === "var_declaration") {
|
|
644
|
+
const [sl, el] = nodeRange(child);
|
|
645
|
+
const names: string[] = [];
|
|
646
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
647
|
+
const spec = child.namedChild(j);
|
|
648
|
+
if (spec && spec.type === "var_spec") {
|
|
649
|
+
const n = fieldChildText(spec, "name", source);
|
|
650
|
+
if (n) names.push(n);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
symbol = {
|
|
654
|
+
name: names.join(", ") || "var",
|
|
655
|
+
type: "var",
|
|
656
|
+
startLine: sl,
|
|
657
|
+
endLine: el,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (symbol) results.push(symbol);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return results;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function extractGoFields(
|
|
668
|
+
node: Node,
|
|
669
|
+
source: string,
|
|
670
|
+
): SymbolInfo[] {
|
|
671
|
+
const results: SymbolInfo[] = [];
|
|
672
|
+
|
|
673
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
674
|
+
const child = node.namedChild(i);
|
|
675
|
+
if (!child || child.type !== "field_declaration") continue;
|
|
676
|
+
|
|
677
|
+
const name = fieldChildText(child, "name", source);
|
|
678
|
+
if (!name) continue;
|
|
679
|
+
const [sl, el] = nodeRange(child);
|
|
680
|
+
const typeNode = child.childForFieldName("type");
|
|
681
|
+
const typeText = typeNode
|
|
682
|
+
? source.slice(typeNode.startIndex, typeNode.endIndex)
|
|
683
|
+
: "";
|
|
684
|
+
results.push({
|
|
685
|
+
name,
|
|
686
|
+
type: "field",
|
|
687
|
+
startLine: sl,
|
|
688
|
+
endLine: el,
|
|
689
|
+
detail: typeText,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return results;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ========================================================================
|
|
697
|
+
// Rust
|
|
698
|
+
// ========================================================================
|
|
699
|
+
|
|
700
|
+
function extractRust(
|
|
701
|
+
node: Node,
|
|
702
|
+
source: string,
|
|
703
|
+
depth: number,
|
|
704
|
+
): SymbolInfo[] {
|
|
705
|
+
const results: SymbolInfo[] = [];
|
|
706
|
+
const MAX_DEPTH = depth + 10;
|
|
707
|
+
|
|
708
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
709
|
+
const child = node.namedChild(i);
|
|
710
|
+
if (!child) continue;
|
|
711
|
+
|
|
712
|
+
let symbol: SymbolInfo | null = null;
|
|
713
|
+
|
|
714
|
+
if (child.type === "function_item") {
|
|
715
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
716
|
+
const [sl, el] = nodeRange(child);
|
|
717
|
+
const params = child.childForFieldName("parameters");
|
|
718
|
+
const detail = params
|
|
719
|
+
? source.slice(params.startIndex, params.endIndex)
|
|
720
|
+
: undefined;
|
|
721
|
+
symbol = { name, type: "function", startLine: sl, endLine: el, detail };
|
|
722
|
+
} else if (child.type === "struct_item") {
|
|
723
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
724
|
+
const [sl, el] = nodeRange(child);
|
|
725
|
+
symbol = { name, type: "struct", startLine: sl, endLine: el };
|
|
726
|
+
} else if (child.type === "enum_item") {
|
|
727
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
728
|
+
const [sl, el] = nodeRange(child);
|
|
729
|
+
symbol = { name, type: "enum", startLine: sl, endLine: el };
|
|
730
|
+
} else if (child.type === "trait_item") {
|
|
731
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
732
|
+
const [sl, el] = nodeRange(child);
|
|
733
|
+
symbol = { name, type: "trait", startLine: sl, endLine: el };
|
|
734
|
+
} else if (child.type === "impl_item") {
|
|
735
|
+
const trait = child.childForFieldName("trait");
|
|
736
|
+
const implType = child.childForFieldName("type");
|
|
737
|
+
const traitStr = trait
|
|
738
|
+
? source.slice(trait.startIndex, trait.endIndex) + " for "
|
|
739
|
+
: "";
|
|
740
|
+
const typeStr = implType
|
|
741
|
+
? source.slice(implType.startIndex, implType.endIndex)
|
|
742
|
+
: "";
|
|
743
|
+
const [sl, el] = nodeRange(child);
|
|
744
|
+
const name = `impl ${traitStr}${typeStr}`;
|
|
745
|
+
symbol = { name, type: "impl", startLine: sl, endLine: el };
|
|
746
|
+
|
|
747
|
+
// Recurse into body for associated functions/methods
|
|
748
|
+
const body = child.childForFieldName("body");
|
|
749
|
+
if (body && depth < MAX_DEPTH) {
|
|
750
|
+
symbol.children = extractRustImplBody(body, source);
|
|
751
|
+
}
|
|
752
|
+
} else if (child.type === "type_item") {
|
|
753
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
754
|
+
const [sl, el] = nodeRange(child);
|
|
755
|
+
symbol = { name, type: "type", startLine: sl, endLine: el };
|
|
756
|
+
} else if (child.type === "const_item") {
|
|
757
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
758
|
+
const [sl, el] = nodeRange(child);
|
|
759
|
+
symbol = { name, type: "const", startLine: sl, endLine: el };
|
|
760
|
+
} else if (child.type === "macro_definition") {
|
|
761
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
762
|
+
const [sl, el] = nodeRange(child);
|
|
763
|
+
symbol = { name: `${name}!`, type: "macro", startLine: sl, endLine: el };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (symbol) results.push(symbol);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return results;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function extractRustImplBody(
|
|
773
|
+
node: Node,
|
|
774
|
+
source: string,
|
|
775
|
+
): SymbolInfo[] {
|
|
776
|
+
const results: SymbolInfo[] = [];
|
|
777
|
+
|
|
778
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
779
|
+
const child = node.namedChild(i);
|
|
780
|
+
if (!child) continue;
|
|
781
|
+
|
|
782
|
+
let symbol: SymbolInfo | null = null;
|
|
783
|
+
|
|
784
|
+
if (child.type === "function_item") {
|
|
785
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
786
|
+
const [sl, el] = nodeRange(child);
|
|
787
|
+
const params = child.childForFieldName("parameters");
|
|
788
|
+
const detail = params
|
|
789
|
+
? source.slice(params.startIndex, params.endIndex)
|
|
790
|
+
: undefined;
|
|
791
|
+
symbol = { name, type: "method", startLine: sl, endLine: el, detail };
|
|
792
|
+
} else if (child.type === "const_item") {
|
|
793
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
794
|
+
const [sl, el] = nodeRange(child);
|
|
795
|
+
symbol = { name, type: "const", startLine: sl, endLine: el };
|
|
796
|
+
} else if (child.type === "type_item") {
|
|
797
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
798
|
+
const [sl, el] = nodeRange(child);
|
|
799
|
+
symbol = { name, type: "type", startLine: sl, endLine: el };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (symbol) results.push(symbol);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return results;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ========================================================================
|
|
809
|
+
// Solidity
|
|
810
|
+
// ========================================================================
|
|
811
|
+
|
|
812
|
+
function extractSolidity(
|
|
813
|
+
node: Node,
|
|
814
|
+
source: string,
|
|
815
|
+
depth: number,
|
|
816
|
+
): SymbolInfo[] {
|
|
817
|
+
const results: SymbolInfo[] = [];
|
|
818
|
+
const MAX_DEPTH = depth + 10;
|
|
819
|
+
|
|
820
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
821
|
+
const child = node.namedChild(i);
|
|
822
|
+
if (!child) continue;
|
|
823
|
+
|
|
824
|
+
let symbol: SymbolInfo | null = null;
|
|
825
|
+
|
|
826
|
+
if (child.type === "contract_declaration") {
|
|
827
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
828
|
+
const [sl, el] = nodeRange(child);
|
|
829
|
+
|
|
830
|
+
// Collect inheritance detail
|
|
831
|
+
const inherits: string[] = [];
|
|
832
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
833
|
+
const c = child.namedChild(j);
|
|
834
|
+
if (c && c.type === "inheritance_specifier") {
|
|
835
|
+
const ancestor = c.childForFieldName("ancestor");
|
|
836
|
+
if (ancestor) {
|
|
837
|
+
inherits.push(source.slice(ancestor.startIndex, ancestor.endIndex));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const detail = inherits.length > 0 ? `is ${inherits.join(", ")}` : undefined;
|
|
842
|
+
|
|
843
|
+
symbol = { name, type: "contract", startLine: sl, endLine: el, detail };
|
|
844
|
+
|
|
845
|
+
// Recurse into contract body
|
|
846
|
+
const body = child.childForFieldName("body");
|
|
847
|
+
if (body && depth < MAX_DEPTH) {
|
|
848
|
+
symbol.children = extractSolidityBody(body, source, depth + 1);
|
|
849
|
+
}
|
|
850
|
+
} else if (child.type === "interface_declaration") {
|
|
851
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
852
|
+
const [sl, el] = nodeRange(child);
|
|
853
|
+
|
|
854
|
+
const inherits: string[] = [];
|
|
855
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
856
|
+
const c = child.namedChild(j);
|
|
857
|
+
if (c && c.type === "inheritance_specifier") {
|
|
858
|
+
const ancestor = c.childForFieldName("ancestor");
|
|
859
|
+
if (ancestor) {
|
|
860
|
+
inherits.push(source.slice(ancestor.startIndex, ancestor.endIndex));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const detail = inherits.length > 0 ? `is ${inherits.join(", ")}` : undefined;
|
|
865
|
+
|
|
866
|
+
symbol = { name, type: "interface", startLine: sl, endLine: el, detail };
|
|
867
|
+
|
|
868
|
+
const body = child.childForFieldName("body");
|
|
869
|
+
if (body && depth < MAX_DEPTH) {
|
|
870
|
+
symbol.children = extractSolidityBody(body, source, depth + 1);
|
|
871
|
+
}
|
|
872
|
+
} else if (child.type === "library_declaration") {
|
|
873
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
874
|
+
const [sl, el] = nodeRange(child);
|
|
875
|
+
symbol = { name, type: "library", startLine: sl, endLine: el };
|
|
876
|
+
|
|
877
|
+
const body = child.childForFieldName("body");
|
|
878
|
+
if (body && depth < MAX_DEPTH) {
|
|
879
|
+
symbol.children = extractSolidityBody(body, source, depth + 1);
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
// File-level item
|
|
883
|
+
symbol = extractSolidityItem(child, source);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (symbol) results.push(symbol);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return results;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function extractSolidityBody(
|
|
893
|
+
node: Node,
|
|
894
|
+
source: string,
|
|
895
|
+
depth: number,
|
|
896
|
+
): SymbolInfo[] {
|
|
897
|
+
const results: SymbolInfo[] = [];
|
|
898
|
+
const MAX_DEPTH = depth + 10;
|
|
899
|
+
|
|
900
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
901
|
+
const child = node.namedChild(i);
|
|
902
|
+
if (!child) continue;
|
|
903
|
+
|
|
904
|
+
const symbol = extractSolidityItem(child, source);
|
|
905
|
+
if (!symbol) continue;
|
|
906
|
+
|
|
907
|
+
// Recurse into struct and enum bodies for nested members
|
|
908
|
+
if (symbol.type === "struct" && depth < MAX_DEPTH) {
|
|
909
|
+
const bodyNode = child.childForFieldName("body");
|
|
910
|
+
if (bodyNode) {
|
|
911
|
+
symbol.children = [];
|
|
912
|
+
for (let j = 0; j < bodyNode.namedChildCount; j++) {
|
|
913
|
+
const m = bodyNode.namedChild(j);
|
|
914
|
+
if (!m) continue;
|
|
915
|
+
if (m.type === "struct_member") {
|
|
916
|
+
const memberName = fieldChildText(m, "name", source) || "";
|
|
917
|
+
const memberType = m.childForFieldName("type");
|
|
918
|
+
const typeStr = memberType
|
|
919
|
+
? source.slice(memberType.startIndex, memberType.endIndex)
|
|
920
|
+
: "";
|
|
921
|
+
const [msl, mel] = nodeRange(m);
|
|
922
|
+
symbol.children.push({
|
|
923
|
+
name: memberName,
|
|
924
|
+
type: "field",
|
|
925
|
+
startLine: msl,
|
|
926
|
+
endLine: mel,
|
|
927
|
+
detail: typeStr,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
} else if (symbol.type === "enum" && depth < MAX_DEPTH) {
|
|
933
|
+
const bodyNode = child.childForFieldName("body");
|
|
934
|
+
if (bodyNode) {
|
|
935
|
+
symbol.children = [];
|
|
936
|
+
for (let j = 0; j < bodyNode.namedChildCount; j++) {
|
|
937
|
+
const m = bodyNode.namedChild(j);
|
|
938
|
+
if (!m || m.type !== "enum_value") continue;
|
|
939
|
+
const [msl, mel] = nodeRange(m);
|
|
940
|
+
symbol.children.push({
|
|
941
|
+
name: source.slice(m.startIndex, m.endIndex).trim(),
|
|
942
|
+
type: "enum_value",
|
|
943
|
+
startLine: msl,
|
|
944
|
+
endLine: mel,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
results.push(symbol);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return results;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Collect all unnamed `parameter`/`event_parameter`/`error_parameter` named children and return their source ranges joined. */
|
|
957
|
+
function solidityParamsText(
|
|
958
|
+
node: Node,
|
|
959
|
+
source: string,
|
|
960
|
+
paramType: string = "parameter",
|
|
961
|
+
): string | undefined {
|
|
962
|
+
const parts: string[] = [];
|
|
963
|
+
for (let j = 0; j < node.namedChildCount; j++) {
|
|
964
|
+
const c = node.namedChild(j);
|
|
965
|
+
if (c && c.type === paramType) {
|
|
966
|
+
parts.push(source.slice(c.startIndex, c.endIndex));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return parts.length > 0 ? `(${parts.join(", ")})` : undefined;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function extractSolidityItem(
|
|
973
|
+
child: Node,
|
|
974
|
+
source: string,
|
|
975
|
+
): SymbolInfo | null {
|
|
976
|
+
const type = child.type;
|
|
977
|
+
|
|
978
|
+
if (type === "function_definition") {
|
|
979
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
980
|
+
const [sl, el] = nodeRange(child);
|
|
981
|
+
const params = solidityParamsText(child, source, "parameter");
|
|
982
|
+
|
|
983
|
+
// Check for return type
|
|
984
|
+
let returnType: string | undefined;
|
|
985
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
986
|
+
const c = child.namedChild(j);
|
|
987
|
+
if (c && c.type === "return_type_definition") {
|
|
988
|
+
returnType = source.slice(c.startIndex, c.endIndex);
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Collect qualifiers: visibility, state_mutability, virtual, override, modifiers
|
|
994
|
+
const qualifiers: string[] = [];
|
|
995
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
996
|
+
const c = child.namedChild(j);
|
|
997
|
+
if (!c) continue;
|
|
998
|
+
if (c.type === "visibility" || c.type === "state_mutability") {
|
|
999
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1000
|
+
} else if (c.type === "virtual") {
|
|
1001
|
+
qualifiers.push("virtual");
|
|
1002
|
+
} else if (c.type === "override_specifier") {
|
|
1003
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1004
|
+
} else if (c.type === "modifier_invocation") {
|
|
1005
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const parts: string[] = [];
|
|
1010
|
+
if (params) parts.push(params);
|
|
1011
|
+
if (qualifiers.length > 0) parts.push(qualifiers.join(" "));
|
|
1012
|
+
if (returnType) parts.push(returnType);
|
|
1013
|
+
const detail = parts.length > 0 ? parts.join(" ") : undefined;
|
|
1014
|
+
|
|
1015
|
+
return { name, type: "function", startLine: sl, endLine: el, detail };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (type === "constructor_definition") {
|
|
1019
|
+
const [sl, el] = nodeRange(child);
|
|
1020
|
+
const params = solidityParamsText(child, source, "parameter");
|
|
1021
|
+
|
|
1022
|
+
const qualifiers: string[] = [];
|
|
1023
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1024
|
+
const c = child.namedChild(j);
|
|
1025
|
+
if (!c) continue;
|
|
1026
|
+
if (c.type === "visibility" || c.type === "state_mutability") {
|
|
1027
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1028
|
+
} else if (c.type === "modifier_invocation") {
|
|
1029
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const parts: string[] = [];
|
|
1034
|
+
if (params) parts.push(params);
|
|
1035
|
+
if (qualifiers.length > 0) parts.push(qualifiers.join(" "));
|
|
1036
|
+
const detail = parts.length > 0 ? parts.join(" ") : undefined;
|
|
1037
|
+
|
|
1038
|
+
return { name: "constructor", type: "constructor", startLine: sl, endLine: el, detail };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (type === "modifier_definition") {
|
|
1042
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
1043
|
+
const [sl, el] = nodeRange(child);
|
|
1044
|
+
const detail = solidityParamsText(child, source, "parameter");
|
|
1045
|
+
return { name, type: "modifier", startLine: sl, endLine: el, detail };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (type === "event_definition") {
|
|
1049
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
1050
|
+
const [sl, el] = nodeRange(child);
|
|
1051
|
+
return { name, type: "event", startLine: sl, endLine: el };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (type === "error_declaration") {
|
|
1055
|
+
const name = childText(child, "identifier", source) || "<anonymous>";
|
|
1056
|
+
const [sl, el] = nodeRange(child);
|
|
1057
|
+
// Collect error parameters for detail
|
|
1058
|
+
const params: string[] = [];
|
|
1059
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1060
|
+
const c = child.namedChild(j);
|
|
1061
|
+
if (c && c.type === "error_parameter") {
|
|
1062
|
+
const typeName = c.childForFieldName("type");
|
|
1063
|
+
if (typeName) {
|
|
1064
|
+
const typeText = source.slice(typeName.startIndex, typeName.endIndex);
|
|
1065
|
+
const paramName = fieldChildText(c, "name", source);
|
|
1066
|
+
params.push(paramName ? `${typeText} ${paramName}` : typeText);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const detail = params.length > 0 ? `(${params.join(", ")})` : undefined;
|
|
1071
|
+
return { name, type: "error", startLine: sl, endLine: el, detail };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (type === "struct_declaration") {
|
|
1075
|
+
const name = childText(child, "identifier", source) || fieldChildText(child, "name", source) || "<anonymous>";
|
|
1076
|
+
const [sl, el] = nodeRange(child);
|
|
1077
|
+
return { name, type: "struct", startLine: sl, endLine: el };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (type === "enum_declaration") {
|
|
1081
|
+
const name = fieldChildText(child, "name", source) || childText(child, "identifier", source) || "<anonymous>";
|
|
1082
|
+
const [sl, el] = nodeRange(child);
|
|
1083
|
+
return { name, type: "enum", startLine: sl, endLine: el };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (type === "fallback_receive_definition") {
|
|
1087
|
+
const [sl, el] = nodeRange(child);
|
|
1088
|
+
|
|
1089
|
+
// Distinguish fallback vs receive by checking first keyword child
|
|
1090
|
+
let symbolType = "fallback";
|
|
1091
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
1092
|
+
const c = child.child(j);
|
|
1093
|
+
if (c && c.type === "receive") {
|
|
1094
|
+
symbolType = "receive";
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const qualifiers: string[] = [];
|
|
1100
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1101
|
+
const c = child.namedChild(j);
|
|
1102
|
+
if (!c) continue;
|
|
1103
|
+
if (c.type === "visibility" || c.type === "state_mutability") {
|
|
1104
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1105
|
+
} else if (c.type === "virtual") {
|
|
1106
|
+
qualifiers.push("virtual");
|
|
1107
|
+
} else if (c.type === "override_specifier") {
|
|
1108
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1109
|
+
} else if (c.type === "modifier_invocation") {
|
|
1110
|
+
qualifiers.push(source.slice(c.startIndex, c.endIndex));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return {
|
|
1115
|
+
name: symbolType,
|
|
1116
|
+
type: symbolType,
|
|
1117
|
+
startLine: sl,
|
|
1118
|
+
endLine: el,
|
|
1119
|
+
detail: qualifiers.length > 0 ? qualifiers.join(" ") : undefined,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (type === "state_variable_declaration") {
|
|
1124
|
+
const name = fieldChildText(child, "name", source) || "<anonymous>";
|
|
1125
|
+
const [sl, el] = nodeRange(child);
|
|
1126
|
+
const varType = child.childForFieldName("type");
|
|
1127
|
+
const detail = varType ? source.slice(varType.startIndex, varType.endIndex) : undefined;
|
|
1128
|
+
return { name, type: "variable", startLine: sl, endLine: el, detail };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ========================================================================
|
|
1135
|
+
// Import extraction
|
|
1136
|
+
// ========================================================================
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Extract import statements from parsed source code.
|
|
1140
|
+
* Returns a list of ImportInfo for each import in the file.
|
|
1141
|
+
*/
|
|
1142
|
+
export async function parseFileImports(
|
|
1143
|
+
key: string,
|
|
1144
|
+
source: string,
|
|
1145
|
+
): Promise<ImportInfo[]> {
|
|
1146
|
+
// Markdown uses regex-based parsing, not tree-sitter
|
|
1147
|
+
if (key === "markdown") {
|
|
1148
|
+
return extractMarkdownLinks(source);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
await ensureInit();
|
|
1152
|
+
|
|
1153
|
+
const lang = await loadLanguage(key);
|
|
1154
|
+
if (!sharedParser) throw new Error("Parser not initialized");
|
|
1155
|
+
|
|
1156
|
+
sharedParser.setLanguage(lang);
|
|
1157
|
+
const tree = sharedParser.parse(source);
|
|
1158
|
+
if (!tree) return [];
|
|
1159
|
+
|
|
1160
|
+
return extractImports(key, tree.rootNode, source);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function extractImports(
|
|
1164
|
+
key: string,
|
|
1165
|
+
node: Node,
|
|
1166
|
+
source: string,
|
|
1167
|
+
): ImportInfo[] {
|
|
1168
|
+
switch (key) {
|
|
1169
|
+
case "javascript":
|
|
1170
|
+
case "typescript":
|
|
1171
|
+
case "tsx":
|
|
1172
|
+
return extractJSImports(node, source);
|
|
1173
|
+
case "python":
|
|
1174
|
+
return extractPythonImports(node, source);
|
|
1175
|
+
case "go":
|
|
1176
|
+
return extractGoImports(node, source);
|
|
1177
|
+
case "rust":
|
|
1178
|
+
return extractRustImports(node, source);
|
|
1179
|
+
case "solidity":
|
|
1180
|
+
return extractSolidityImports(node, source);
|
|
1181
|
+
case "scss":
|
|
1182
|
+
return extractScssImports(node, source);
|
|
1183
|
+
case "sass":
|
|
1184
|
+
return extractSassImports(node, source);
|
|
1185
|
+
case "java":
|
|
1186
|
+
return extractJavaImports(node, source);
|
|
1187
|
+
case "markdown":
|
|
1188
|
+
return extractMarkdownLinks(source);
|
|
1189
|
+
default:
|
|
1190
|
+
return [];
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// ---- JavaScript / TypeScript / TSX imports ----
|
|
1195
|
+
|
|
1196
|
+
function extractJSImports(node: Node, source: string): ImportInfo[] {
|
|
1197
|
+
const results: ImportInfo[] = [];
|
|
1198
|
+
|
|
1199
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1200
|
+
const child = node.namedChild(i);
|
|
1201
|
+
if (!child) continue;
|
|
1202
|
+
|
|
1203
|
+
// import { foo } from 'bar'; or import foo from 'bar';
|
|
1204
|
+
if (child.type === "import_statement") {
|
|
1205
|
+
const sourceNode = child.childForFieldName("source");
|
|
1206
|
+
if (!sourceNode) continue;
|
|
1207
|
+
const sourceText = extractStringLiteral(sourceNode, source);
|
|
1208
|
+
if (!sourceText) continue;
|
|
1209
|
+
|
|
1210
|
+
const names = extractImportNames(child, source);
|
|
1211
|
+
results.push({
|
|
1212
|
+
source: sourceText,
|
|
1213
|
+
names,
|
|
1214
|
+
lineNumber: child.startPosition.row + 1,
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// export { foo } from 'bar';
|
|
1219
|
+
if (child.type === "export_statement") {
|
|
1220
|
+
const sourceNode = child.childForFieldName("source");
|
|
1221
|
+
if (!sourceNode) continue;
|
|
1222
|
+
const sourceText = extractStringLiteral(sourceNode, source);
|
|
1223
|
+
if (!sourceText) continue;
|
|
1224
|
+
|
|
1225
|
+
const names = extractImportNames(child, source);
|
|
1226
|
+
|
|
1227
|
+
results.push({
|
|
1228
|
+
source: sourceText,
|
|
1229
|
+
names,
|
|
1230
|
+
lineNumber: child.startPosition.row + 1,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// const x = require('bar');
|
|
1235
|
+
if (
|
|
1236
|
+
child.type === "lexical_declaration" ||
|
|
1237
|
+
child.type === "variable_declaration"
|
|
1238
|
+
) {
|
|
1239
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1240
|
+
const decl = child.namedChild(j);
|
|
1241
|
+
if (!decl || decl.type !== "variable_declarator") continue;
|
|
1242
|
+
const value = decl.childForFieldName("value");
|
|
1243
|
+
if (!value || value.type !== "call_expression") continue;
|
|
1244
|
+
const func = value.childForFieldName("function");
|
|
1245
|
+
if (!func) continue;
|
|
1246
|
+
const funcText = source.slice(func.startIndex, func.endIndex);
|
|
1247
|
+
if (funcText !== "require") continue;
|
|
1248
|
+
|
|
1249
|
+
const args = value.childForFieldName("arguments");
|
|
1250
|
+
if (!args || args.namedChildCount === 0) continue;
|
|
1251
|
+
const firstArg = args.namedChild(0);
|
|
1252
|
+
if (!firstArg) continue;
|
|
1253
|
+
const sourceText = extractStringLiteral(firstArg, source);
|
|
1254
|
+
if (!sourceText) continue;
|
|
1255
|
+
|
|
1256
|
+
const name = fieldChildText(decl, "name", source);
|
|
1257
|
+
results.push({
|
|
1258
|
+
source: sourceText,
|
|
1259
|
+
names: name ? [name] : [],
|
|
1260
|
+
lineNumber: child.startPosition.row + 1,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return results;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function extractImportNames(node: Node, source: string): string[] {
|
|
1270
|
+
const names: string[] = [];
|
|
1271
|
+
|
|
1272
|
+
// import_clause is a named child, NOT a field in tree-sitter grammar
|
|
1273
|
+
for (let j = 0; j < node.namedChildCount; j++) {
|
|
1274
|
+
const c = node.namedChild(j);
|
|
1275
|
+
if (!c) continue;
|
|
1276
|
+
|
|
1277
|
+
if (c.type === "import_clause") {
|
|
1278
|
+
extractNamesFromClause(c, source, names);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// export_statement may have export_clause with export_specifiers
|
|
1282
|
+
if (c.type === "export_clause") {
|
|
1283
|
+
for (let k = 0; k < c.namedChildCount; k++) {
|
|
1284
|
+
const spec = c.namedChild(k);
|
|
1285
|
+
if (spec && spec.type === "export_specifier") {
|
|
1286
|
+
const name = fieldChildText(spec, "name", source);
|
|
1287
|
+
if (name) names.push(name);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return names;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** Extract imported symbol names from an import_clause node. */
|
|
1297
|
+
function extractNamesFromClause(clause: Node, source: string, names: string[]): void {
|
|
1298
|
+
for (let j = 0; j < clause.namedChildCount; j++) {
|
|
1299
|
+
const c = clause.namedChild(j);
|
|
1300
|
+
if (!c) continue;
|
|
1301
|
+
|
|
1302
|
+
if (c.type === "namespace_import") {
|
|
1303
|
+
const alias = c.childForFieldName("alias");
|
|
1304
|
+
if (alias) names.push("* as " + source.slice(alias.startIndex, alias.endIndex));
|
|
1305
|
+
} else if (c.type === "named_imports") {
|
|
1306
|
+
for (let k = 0; k < c.namedChildCount; k++) {
|
|
1307
|
+
const spec = c.namedChild(k);
|
|
1308
|
+
if (spec && spec.type === "import_specifier") {
|
|
1309
|
+
const name = fieldChildText(spec, "name", source);
|
|
1310
|
+
if (name) names.push(name);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
} else {
|
|
1314
|
+
// default import (identifier)
|
|
1315
|
+
names.push(source.slice(c.startIndex, c.endIndex));
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// ---- Python imports ----
|
|
1321
|
+
|
|
1322
|
+
function extractPythonImports(node: Node, source: string): ImportInfo[] {
|
|
1323
|
+
const results: ImportInfo[] = [];
|
|
1324
|
+
|
|
1325
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1326
|
+
const child = node.namedChild(i);
|
|
1327
|
+
if (!child) continue;
|
|
1328
|
+
|
|
1329
|
+
// import foo
|
|
1330
|
+
if (child.type === "import_statement") {
|
|
1331
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1332
|
+
const c = child.namedChild(j);
|
|
1333
|
+
if (!c) continue;
|
|
1334
|
+
if (c.type === "dotted_name") {
|
|
1335
|
+
const mod = source.slice(c.startIndex, c.endIndex);
|
|
1336
|
+
results.push({
|
|
1337
|
+
source: mod,
|
|
1338
|
+
names: [],
|
|
1339
|
+
lineNumber: child.startPosition.row + 1,
|
|
1340
|
+
});
|
|
1341
|
+
} else if (c.type === "aliased_import") {
|
|
1342
|
+
const name = fieldChildText(c, "name", source);
|
|
1343
|
+
const alias = fieldChildText(c, "alias", source);
|
|
1344
|
+
if (name) {
|
|
1345
|
+
results.push({
|
|
1346
|
+
source: name,
|
|
1347
|
+
names: alias ? [alias] : [],
|
|
1348
|
+
lineNumber: child.startPosition.row + 1,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// from foo import bar
|
|
1356
|
+
if (child.type === "import_from_statement") {
|
|
1357
|
+
const moduleNode = child.childForFieldName("module_name");
|
|
1358
|
+
const sourceText = moduleNode
|
|
1359
|
+
? source.slice(moduleNode.startIndex, moduleNode.endIndex)
|
|
1360
|
+
: "";
|
|
1361
|
+
if (!sourceText) continue;
|
|
1362
|
+
|
|
1363
|
+
const names: string[] = [];
|
|
1364
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1365
|
+
const c = child.namedChild(j);
|
|
1366
|
+
if (!c) continue;
|
|
1367
|
+
if (c.type === "dotted_name") {
|
|
1368
|
+
names.push(source.slice(c.startIndex, c.endIndex));
|
|
1369
|
+
} else if (c.type === "aliased_import") {
|
|
1370
|
+
const name = fieldChildText(c, "name", source);
|
|
1371
|
+
if (name) names.push(name);
|
|
1372
|
+
} else if (c.type === "wildcard_import") {
|
|
1373
|
+
names.push("*");
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
results.push({
|
|
1378
|
+
source: sourceText,
|
|
1379
|
+
names,
|
|
1380
|
+
lineNumber: child.startPosition.row + 1,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
return results;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// ---- Go imports ----
|
|
1389
|
+
|
|
1390
|
+
function extractGoImports(node: Node, source: string): ImportInfo[] {
|
|
1391
|
+
const results: ImportInfo[] = [];
|
|
1392
|
+
|
|
1393
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1394
|
+
const child = node.namedChild(i);
|
|
1395
|
+
if (!child || child.type !== "import_declaration") continue;
|
|
1396
|
+
|
|
1397
|
+
// Go's import_declaration wraps: single import_spec, or import_spec_list (grouped)
|
|
1398
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1399
|
+
const spec = child.namedChild(j);
|
|
1400
|
+
if (!spec) continue;
|
|
1401
|
+
|
|
1402
|
+
// Grouped imports: import_spec_list contains import_spec children
|
|
1403
|
+
if (spec.type === "import_spec_list") {
|
|
1404
|
+
for (let k = 0; k < spec.namedChildCount; k++) {
|
|
1405
|
+
const innerSpec = spec.namedChild(k);
|
|
1406
|
+
if (!innerSpec || innerSpec.type !== "import_spec") continue;
|
|
1407
|
+
const result = extractGoImportSpec(innerSpec, source);
|
|
1408
|
+
if (result) results.push(result);
|
|
1409
|
+
}
|
|
1410
|
+
} else if (spec.type === "import_spec") {
|
|
1411
|
+
const result = extractGoImportSpec(spec, source);
|
|
1412
|
+
if (result) results.push(result);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
return results;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function extractGoImportSpec(spec: Node, source: string): ImportInfo | null {
|
|
1421
|
+
const pathNode = spec.childForFieldName("path");
|
|
1422
|
+
if (!pathNode) return null;
|
|
1423
|
+
const sourceText = extractStringLiteral(pathNode, source);
|
|
1424
|
+
if (!sourceText) return null;
|
|
1425
|
+
|
|
1426
|
+
const name = fieldChildText(spec, "name", source);
|
|
1427
|
+
return {
|
|
1428
|
+
source: sourceText,
|
|
1429
|
+
names: name ? [name] : [],
|
|
1430
|
+
lineNumber: spec.startPosition.row + 1,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// ---- Rust imports ----
|
|
1435
|
+
|
|
1436
|
+
function extractRustImports(node: Node, source: string): ImportInfo[] {
|
|
1437
|
+
const results: ImportInfo[] = [];
|
|
1438
|
+
|
|
1439
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1440
|
+
const child = node.namedChild(i);
|
|
1441
|
+
if (!child) continue;
|
|
1442
|
+
|
|
1443
|
+
// use foo::bar;
|
|
1444
|
+
if (child.type === "use_declaration") {
|
|
1445
|
+
const arg = child.childForFieldName("argument");
|
|
1446
|
+
if (!arg) continue;
|
|
1447
|
+
|
|
1448
|
+
const sourceText = source
|
|
1449
|
+
.slice(arg.startIndex, arg.endIndex)
|
|
1450
|
+
.replace(/\s+/g, " ");
|
|
1451
|
+
results.push({
|
|
1452
|
+
source: sourceText,
|
|
1453
|
+
names: [],
|
|
1454
|
+
lineNumber: child.startPosition.row + 1,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// extern crate foo;
|
|
1459
|
+
if (child.type === "extern_crate_declaration") {
|
|
1460
|
+
const name = fieldChildText(child, "name", source);
|
|
1461
|
+
if (name) {
|
|
1462
|
+
results.push({
|
|
1463
|
+
source: name,
|
|
1464
|
+
names: [],
|
|
1465
|
+
lineNumber: child.startPosition.row + 1,
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return results;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// ---- Solidity imports ----
|
|
1475
|
+
|
|
1476
|
+
function extractSolidityImports(node: Node, source: string): ImportInfo[] {
|
|
1477
|
+
const results: ImportInfo[] = [];
|
|
1478
|
+
|
|
1479
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1480
|
+
const child = node.namedChild(i);
|
|
1481
|
+
if (!child || child.type !== "import_directive") continue;
|
|
1482
|
+
|
|
1483
|
+
const sourceNode = child.childForFieldName("source");
|
|
1484
|
+
if (!sourceNode) continue;
|
|
1485
|
+
const sourceText = extractStringLiteral(sourceNode, source);
|
|
1486
|
+
if (!sourceText) continue;
|
|
1487
|
+
|
|
1488
|
+
const names: string[] = [];
|
|
1489
|
+
// Check for named imports: import {A, B} from "foo.sol";
|
|
1490
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1491
|
+
const c = child.namedChild(j);
|
|
1492
|
+
if (!c) continue;
|
|
1493
|
+
if (c.type === "import_clause") {
|
|
1494
|
+
for (let k = 0; k < c.namedChildCount; k++) {
|
|
1495
|
+
const sym = c.namedChild(k);
|
|
1496
|
+
if (sym && sym.type === "symbol_alias") {
|
|
1497
|
+
const name = fieldChildText(sym, "name", source);
|
|
1498
|
+
if (name) names.push(name);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
results.push({
|
|
1505
|
+
source: sourceText,
|
|
1506
|
+
names,
|
|
1507
|
+
lineNumber: child.startPosition.row + 1,
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return results;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// ---- Markdown extraction ----
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Extract structural symbols from Markdown content.
|
|
1518
|
+
* Since Markdown doesn't use tree-sitter, we use regex-based parsing.
|
|
1519
|
+
* Extracts headings as symbols, with nested structure based on heading levels.
|
|
1520
|
+
* Code blocks are nested under their parent heading.
|
|
1521
|
+
*/
|
|
1522
|
+
function extractMarkdown(source: string): SymbolInfo[] {
|
|
1523
|
+
const lines = source.split("\n");
|
|
1524
|
+
const results: SymbolInfo[] = [];
|
|
1525
|
+
|
|
1526
|
+
// Track heading hierarchy for nesting
|
|
1527
|
+
const stack: SymbolInfo[] = [];
|
|
1528
|
+
|
|
1529
|
+
// First pass: extract headings and build hierarchy
|
|
1530
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1531
|
+
const line = lines[i];
|
|
1532
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1533
|
+
|
|
1534
|
+
if (headingMatch) {
|
|
1535
|
+
const level = headingMatch[1].length;
|
|
1536
|
+
const title = headingMatch[2].trim();
|
|
1537
|
+
const startLine = i + 1; // 1-indexed
|
|
1538
|
+
|
|
1539
|
+
const symbol: SymbolInfo = {
|
|
1540
|
+
name: title,
|
|
1541
|
+
type: `heading${level}`,
|
|
1542
|
+
startLine,
|
|
1543
|
+
endLine: lines.length, // Will be updated later
|
|
1544
|
+
children: [],
|
|
1545
|
+
detail: `#`.repeat(level),
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
// Find parent: pop stack until we find a heading with lower level
|
|
1549
|
+
while (stack.length > 0) {
|
|
1550
|
+
const parentLevel = parseInt(stack[stack.length - 1].type?.replace('heading', '') || '0', 10);
|
|
1551
|
+
if (parentLevel >= level) {
|
|
1552
|
+
stack.pop();
|
|
1553
|
+
} else {
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Add to parent or root
|
|
1559
|
+
if (stack.length > 0) {
|
|
1560
|
+
stack[stack.length - 1].children!.push(symbol);
|
|
1561
|
+
} else {
|
|
1562
|
+
results.push(symbol);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
stack.push(symbol);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Update endLine for each heading: it ends just before the next sibling or parent's next sibling
|
|
1570
|
+
updateEndLines(results, lines.length);
|
|
1571
|
+
|
|
1572
|
+
// Second pass: extract code blocks and nest them under appropriate headings
|
|
1573
|
+
const codeBlockRegex = /^```(\w+)\s*$/gm; // Only match opening ``` with a language
|
|
1574
|
+
let match;
|
|
1575
|
+
while ((match = codeBlockRegex.exec(source)) !== null) {
|
|
1576
|
+
const language = match[1];
|
|
1577
|
+
const startLine = source.slice(0, match.index).split("\n").length;
|
|
1578
|
+
|
|
1579
|
+
// Find closing ```
|
|
1580
|
+
const afterMatch = source.slice(match.index + match[0].length);
|
|
1581
|
+
const closingIdx = afterMatch.search(/^```\s*$/m);
|
|
1582
|
+
let endLine = startLine + 1;
|
|
1583
|
+
if (closingIdx !== -1) {
|
|
1584
|
+
endLine = startLine + afterMatch.slice(0, closingIdx).split("\n").length + 1;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const codeBlock: SymbolInfo = {
|
|
1588
|
+
name: `code_block_${language}`,
|
|
1589
|
+
type: "code_block",
|
|
1590
|
+
startLine,
|
|
1591
|
+
endLine,
|
|
1592
|
+
detail: language,
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// Find the heading that contains this code block
|
|
1596
|
+
const parent = findParentHeading(results, startLine);
|
|
1597
|
+
if (parent) {
|
|
1598
|
+
parent.children!.push(codeBlock);
|
|
1599
|
+
} else {
|
|
1600
|
+
results.push(codeBlock);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
return results;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/** Update endLine for each heading based on next sibling or parent's next sibling. */
|
|
1608
|
+
function updateEndLines(symbols: SymbolInfo[], totalLines: number): void {
|
|
1609
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1610
|
+
const symbol = symbols[i];
|
|
1611
|
+
|
|
1612
|
+
// Default: ends at file end
|
|
1613
|
+
symbol.endLine = totalLines;
|
|
1614
|
+
|
|
1615
|
+
// If there's a next sibling, this heading ends just before it
|
|
1616
|
+
if (i + 1 < symbols.length) {
|
|
1617
|
+
symbol.endLine = symbols[i + 1].startLine - 1;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Recurse into children
|
|
1621
|
+
if (symbol.children && symbol.children.length > 0) {
|
|
1622
|
+
updateEndLines(symbol.children, symbol.endLine);
|
|
1623
|
+
// The last child's endLine might extend to parent's endLine
|
|
1624
|
+
const lastChild = symbol.children[symbol.children.length - 1];
|
|
1625
|
+
if (lastChild.endLine < symbol.endLine) {
|
|
1626
|
+
lastChild.endLine = symbol.endLine;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/** Find the heading that contains the given line number. */
|
|
1633
|
+
function findParentHeading(symbols: SymbolInfo[], line: number): SymbolInfo | null {
|
|
1634
|
+
for (const symbol of symbols) {
|
|
1635
|
+
if (symbol.type?.startsWith("heading")) {
|
|
1636
|
+
// Check if line is within this heading's range
|
|
1637
|
+
if (line >= symbol.startLine && line <= symbol.endLine) {
|
|
1638
|
+
// Try to find a more specific child
|
|
1639
|
+
if (symbol.children && symbol.children.length > 0) {
|
|
1640
|
+
const child = findParentHeading(symbol.children, line);
|
|
1641
|
+
if (child) return child;
|
|
1642
|
+
}
|
|
1643
|
+
return symbol;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Extract links from Markdown content as "imports".
|
|
1652
|
+
* Links to local files are considered imports.
|
|
1653
|
+
*/
|
|
1654
|
+
function extractMarkdownLinks(source: string): ImportInfo[] {
|
|
1655
|
+
const results: ImportInfo[] = [];
|
|
1656
|
+
const lines = source.split("\n");
|
|
1657
|
+
|
|
1658
|
+
// Match markdown links: [text](url)
|
|
1659
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
1660
|
+
|
|
1661
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1662
|
+
const line = lines[i];
|
|
1663
|
+
let match;
|
|
1664
|
+
|
|
1665
|
+
while ((match = linkRegex.exec(line)) !== null) {
|
|
1666
|
+
const linkText = match[1];
|
|
1667
|
+
const url = match[2];
|
|
1668
|
+
|
|
1669
|
+
// Skip external URLs
|
|
1670
|
+
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("mailto:")) {
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
results.push({
|
|
1675
|
+
source: url,
|
|
1676
|
+
names: [linkText],
|
|
1677
|
+
lineNumber: i + 1,
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Also extract import-like references: 
|
|
1683
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
1684
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1685
|
+
const line = lines[i];
|
|
1686
|
+
let match;
|
|
1687
|
+
|
|
1688
|
+
while ((match = imageRegex.exec(line)) !== null) {
|
|
1689
|
+
const altText = match[1] || "image";
|
|
1690
|
+
const url = match[2];
|
|
1691
|
+
|
|
1692
|
+
// Skip external URLs
|
|
1693
|
+
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("mailto:")) {
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
results.push({
|
|
1698
|
+
source: url,
|
|
1699
|
+
names: [altText],
|
|
1700
|
+
lineNumber: i + 1,
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
return results;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// ---- Shared helpers ----
|
|
1709
|
+
|
|
1710
|
+
/** Extract a string literal's value (strip surrounding quotes). */
|
|
1711
|
+
function extractStringLiteral(node: Node, source: string): string | null {
|
|
1712
|
+
const text = source.slice(node.startIndex, node.endIndex);
|
|
1713
|
+
// Handle single, double, and backtick quotes
|
|
1714
|
+
const match = text.match(/^['"`](.*)['"`]$/s);
|
|
1715
|
+
return match ? match[1] : text;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// ---- Re-export ImportInfo for backward compatibility ----
|
|
1719
|
+
|
|
1720
|
+
export type { ImportInfo };
|
|
1721
|
+
|