Navegador

navegador / navegador / ingestion / typescript.py
Blame History Raw 370 lines
1
"""
2
TypeScript/JavaScript AST parser — extracts classes, interfaces, functions
3
(including const arrow functions), methods, imports, and call edges.
4
5
Supports:
6
- .ts / .tsx → tree-sitter-typescript (TypeScript grammar)
7
- .js / .jsx → tree-sitter-javascript (JavaScript grammar)
8
"""
9
10
import logging
11
from pathlib import Path
12
13
from navegador.graph.schema import EdgeType, NodeLabel
14
from navegador.graph.store import GraphStore
15
from navegador.ingestion.parser import LanguageParser
16
17
logger = logging.getLogger(__name__)
18
19
20
def _get_ts_language(language: str):
21
try:
22
if language == "typescript":
23
import tree_sitter_typescript as tsts # type: ignore[import]
24
from tree_sitter import Language
25
26
return Language(tsts.language_typescript())
27
else:
28
import tree_sitter_javascript as tsjs # type: ignore[import]
29
from tree_sitter import Language
30
31
return Language(tsjs.language())
32
except ImportError as e:
33
raise ImportError(
34
f"Install tree-sitter-{language}: pip install tree-sitter-{language}"
35
) from e
36
37
38
def _node_text(node, source: bytes) -> str:
39
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
40
41
42
def _jsdoc(node, source: bytes) -> str:
43
"""Return the JSDoc comment (/** ... */) preceding a node, if any."""
44
parent = node.parent
45
if not parent:
46
return ""
47
siblings = list(parent.children)
48
idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
49
if idx <= 0:
50
return ""
51
prev = siblings[idx - 1]
52
if prev.type == "comment":
53
raw = _node_text(prev, source).strip()
54
if raw.startswith("/**"):
55
return raw.strip("/**").strip("*/").strip()
56
return ""
57
58
59
class TypeScriptParser(LanguageParser):
60
"""Parses TypeScript/JavaScript source files into the navegador graph."""
61
62
def __init__(self, language: str = "typescript") -> None:
63
from tree_sitter import Parser # type: ignore[import]
64
65
self._parser = Parser(_get_ts_language(language))
66
self._language = language
67
68
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
69
source = path.read_bytes()
70
tree = self._parser.parse(source)
71
rel_path = str(path.relative_to(repo_root))
72
73
store.create_node(
74
NodeLabel.File,
75
{
76
"name": path.name,
77
"path": rel_path,
78
"language": self._language,
79
"line_count": source.count(b"\n"),
80
},
81
)
82
83
stats = {"functions": 0, "classes": 0, "edges": 0}
84
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
85
return stats
86
87
# ── AST walker ────────────────────────────────────────────────────────────
88
89
def _walk(
90
self,
91
node,
92
source: bytes,
93
file_path: str,
94
store: GraphStore,
95
stats: dict,
96
class_name: str | None,
97
) -> None:
98
if node.type in ("class_declaration", "abstract_class_declaration"):
99
self._handle_class(node, source, file_path, store, stats)
100
return
101
if node.type in ("interface_declaration", "type_alias_declaration"):
102
self._handle_interface(node, source, file_path, store, stats)
103
return
104
if node.type == "function_declaration":
105
self._handle_function(node, source, file_path, store, stats, class_name)
106
return
107
if node.type == "method_definition":
108
self._handle_function(node, source, file_path, store, stats, class_name)
109
return
110
if node.type in ("lexical_declaration", "variable_declaration"):
111
self._handle_lexical(node, source, file_path, store, stats)
112
return
113
if node.type in ("import_statement", "import_declaration"):
114
self._handle_import(node, source, file_path, store, stats)
115
return
116
if node.type == "export_statement":
117
# Recurse into exported declarations
118
for child in node.children:
119
if child.type not in ("export", "default", "from"):
120
self._walk(child, source, file_path, store, stats, class_name)
121
return
122
123
for child in node.children:
124
self._walk(child, source, file_path, store, stats, class_name)
125
126
# ── Handlers ──────────────────────────────────────────────────────────────
127
128
def _handle_class(
129
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
130
) -> None:
131
name_node = next((c for c in node.children if c.type == "type_identifier"), None)
132
if not name_node:
133
return
134
name = _node_text(name_node, source)
135
docstring = _jsdoc(node, source)
136
137
store.create_node(
138
NodeLabel.Class,
139
{
140
"name": name,
141
"file_path": file_path,
142
"line_start": node.start_point[0] + 1,
143
"line_end": node.end_point[0] + 1,
144
"docstring": docstring,
145
},
146
)
147
store.create_edge(
148
NodeLabel.File,
149
{"path": file_path},
150
EdgeType.CONTAINS,
151
NodeLabel.Class,
152
{"name": name, "file_path": file_path},
153
)
154
stats["classes"] += 1
155
stats["edges"] += 1
156
157
# Inheritance: extends clause
158
heritage = next((c for c in node.children if c.type == "class_heritage"), None)
159
if heritage:
160
for child in heritage.children:
161
if child.type == "extends_clause":
162
for c in child.children:
163
if c.type == "identifier":
164
parent_name = _node_text(c, source)
165
store.create_edge(
166
NodeLabel.Class,
167
{"name": name, "file_path": file_path},
168
EdgeType.INHERITS,
169
NodeLabel.Class,
170
{"name": parent_name, "file_path": file_path},
171
)
172
stats["edges"] += 1
173
174
body = next((c for c in node.children if c.type == "class_body"), None)
175
if body:
176
for child in body.children:
177
if child.type == "method_definition":
178
self._handle_function(child, source, file_path, store, stats, class_name=name)
179
180
def _handle_interface(
181
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
182
) -> None:
183
name_node = next((c for c in node.children if c.type == "type_identifier"), None)
184
if not name_node:
185
return
186
name = _node_text(name_node, source)
187
docstring = _jsdoc(node, source)
188
kind = "interface" if node.type == "interface_declaration" else "type"
189
190
store.create_node(
191
NodeLabel.Class,
192
{
193
"name": name,
194
"file_path": file_path,
195
"line_start": node.start_point[0] + 1,
196
"line_end": node.end_point[0] + 1,
197
"docstring": f"{kind}: {docstring}".strip(": "),
198
},
199
)
200
store.create_edge(
201
NodeLabel.File,
202
{"path": file_path},
203
EdgeType.CONTAINS,
204
NodeLabel.Class,
205
{"name": name, "file_path": file_path},
206
)
207
stats["classes"] += 1
208
stats["edges"] += 1
209
210
def _handle_function(
211
self,
212
node,
213
source: bytes,
214
file_path: str,
215
store: GraphStore,
216
stats: dict,
217
class_name: str | None,
218
) -> None:
219
name_node = next(
220
(c for c in node.children if c.type in ("identifier", "property_identifier")), None
221
)
222
if not name_node:
223
return
224
name = _node_text(name_node, source)
225
if name in ("constructor", "get", "set", "static", "async"):
226
# These are keywords, not useful names — look for next identifier
227
name_node = next(
228
(
229
c
230
for c in node.children
231
if c.type in ("identifier", "property_identifier") and c != name_node
232
),
233
None,
234
)
235
if not name_node:
236
return
237
name = _node_text(name_node, source)
238
239
docstring = _jsdoc(node, source)
240
label = NodeLabel.Method if class_name else NodeLabel.Function
241
242
store.create_node(
243
label,
244
{
245
"name": name,
246
"file_path": file_path,
247
"line_start": node.start_point[0] + 1,
248
"line_end": node.end_point[0] + 1,
249
"docstring": docstring,
250
"class_name": class_name or "",
251
},
252
)
253
254
container_label = NodeLabel.Class if class_name else NodeLabel.File
255
container_key = (
256
{"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
257
)
258
store.create_edge(
259
container_label,
260
container_key,
261
EdgeType.CONTAINS,
262
label,
263
{"name": name, "file_path": file_path},
264
)
265
stats["functions"] += 1
266
stats["edges"] += 1
267
268
self._extract_calls(node, source, file_path, name, label, store, stats)
269
270
def _handle_lexical(
271
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
272
) -> None:
273
"""Handle: const foo = () => {} and const bar = function() {}"""
274
for child in node.children:
275
if child.type != "variable_declarator":
276
continue
277
name_node = child.child_by_field_name("name")
278
value_node = child.child_by_field_name("value")
279
if not name_node or not value_node:
280
continue
281
if value_node.type not in ("arrow_function", "function_expression", "function"):
282
continue
283
name = _node_text(name_node, source)
284
docstring = _jsdoc(node, source)
285
286
store.create_node(
287
NodeLabel.Function,
288
{
289
"name": name,
290
"file_path": file_path,
291
"line_start": node.start_point[0] + 1,
292
"line_end": node.end_point[0] + 1,
293
"docstring": docstring,
294
"class_name": "",
295
},
296
)
297
store.create_edge(
298
NodeLabel.File,
299
{"path": file_path},
300
EdgeType.CONTAINS,
301
NodeLabel.Function,
302
{"name": name, "file_path": file_path},
303
)
304
stats["functions"] += 1
305
stats["edges"] += 1
306
307
self._extract_calls(
308
value_node, source, file_path, name, NodeLabel.Function, store, stats
309
)
310
311
def _handle_import(
312
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
313
) -> None:
314
for child in node.children:
315
if child.type == "string":
316
module = _node_text(child, source).strip("'\"")
317
store.create_node(
318
NodeLabel.Import,
319
{
320
"name": module,
321
"file_path": file_path,
322
"line_start": node.start_point[0] + 1,
323
"module": module,
324
},
325
)
326
store.create_edge(
327
NodeLabel.File,
328
{"path": file_path},
329
EdgeType.IMPORTS,
330
NodeLabel.Import,
331
{"name": module, "file_path": file_path},
332
)
333
stats["edges"] += 1
334
break
335
336
def _extract_calls(
337
self,
338
fn_node,
339
source: bytes,
340
file_path: str,
341
fn_name: str,
342
fn_label: str,
343
store: GraphStore,
344
stats: dict,
345
) -> None:
346
def walk(node):
347
if node.type == "call_expression":
348
func = node.child_by_field_name("function")
349
if func:
350
text = _node_text(func, source)
351
callee = text.split(".")[-1]
352
store.create_edge(
353
fn_label,
354
{"name": fn_name, "file_path": file_path},
355
EdgeType.CALLS,
356
NodeLabel.Function,
357
{"name": callee, "file_path": file_path},
358
)
359
stats["edges"] += 1
360
for child in node.children:
361
walk(child)
362
363
# Body is statement_block for functions, expression for arrow fns
364
body = next(
365
(c for c in fn_node.children if c.type in ("statement_block", "expression_statement")),
366
None,
367
)
368
if body:
369
walk(body)
370

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button