Navegador

navegador / navegador / ingestion / ruby.py
Blame History Raw 298 lines
1
"""
2
Ruby AST parser — extracts classes, modules, methods, and requires/includes
3
from .rb files using tree-sitter.
4
"""
5
6
import logging
7
from pathlib import Path
8
9
from navegador.graph.schema import EdgeType, NodeLabel
10
from navegador.graph.store import GraphStore
11
from navegador.ingestion.parser import LanguageParser
12
13
logger = logging.getLogger(__name__)
14
15
16
def _get_ruby_language():
17
try:
18
import tree_sitter_ruby as tsruby # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tsruby.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-ruby: pip install tree-sitter-ruby") from e
24
25
26
def _node_text(node, source: bytes) -> str:
27
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
28
29
30
class RubyParser(LanguageParser):
31
"""Parses Ruby source files into the navegador graph."""
32
33
def __init__(self) -> None:
34
from tree_sitter import Parser # type: ignore[import]
35
36
self._parser = Parser(_get_ruby_language())
37
38
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
39
source = path.read_bytes()
40
tree = self._parser.parse(source)
41
rel_path = str(path.relative_to(repo_root))
42
43
store.create_node(
44
NodeLabel.File,
45
{
46
"name": path.name,
47
"path": rel_path,
48
"language": "ruby",
49
"line_count": source.count(b"\n"),
50
},
51
)
52
53
stats = {"functions": 0, "classes": 0, "edges": 0}
54
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
55
return stats
56
57
# ── AST walker ────────────────────────────────────────────────────────────
58
59
def _walk(
60
self,
61
node,
62
source: bytes,
63
file_path: str,
64
store: GraphStore,
65
stats: dict,
66
class_name: str | None,
67
) -> None:
68
if node.type == "class":
69
self._handle_class(node, source, file_path, store, stats)
70
return
71
if node.type == "module":
72
self._handle_module(node, source, file_path, store, stats)
73
return
74
if node.type == "method":
75
self._handle_method(node, source, file_path, store, stats, class_name)
76
return
77
if node.type == "singleton_method":
78
self._handle_method(node, source, file_path, store, stats, class_name)
79
return
80
if node.type == "call":
81
# require / require_relative / include
82
self._maybe_handle_require(node, source, file_path, store, stats)
83
for child in node.children:
84
self._walk(child, source, file_path, store, stats, class_name)
85
86
# ── Handlers ──────────────────────────────────────────────────────────────
87
88
def _handle_class(
89
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
90
) -> None:
91
name_node = node.child_by_field_name("name")
92
if not name_node:
93
name_node = next(
94
(c for c in node.children if c.type in ("constant", "scope_resolution")), None
95
)
96
if not name_node:
97
return
98
name = _node_text(name_node, source)
99
100
store.create_node(
101
NodeLabel.Class,
102
{
103
"name": name,
104
"file_path": file_path,
105
"line_start": node.start_point[0] + 1,
106
"line_end": node.end_point[0] + 1,
107
"docstring": "",
108
},
109
)
110
store.create_edge(
111
NodeLabel.File,
112
{"path": file_path},
113
EdgeType.CONTAINS,
114
NodeLabel.Class,
115
{"name": name, "file_path": file_path},
116
)
117
stats["classes"] += 1
118
stats["edges"] += 1
119
120
# Superclass
121
superclass = node.child_by_field_name("superclass")
122
if superclass:
123
parent_name = _node_text(superclass, source).lstrip("<").strip()
124
store.create_edge(
125
NodeLabel.Class,
126
{"name": name, "file_path": file_path},
127
EdgeType.INHERITS,
128
NodeLabel.Class,
129
{"name": parent_name, "file_path": file_path},
130
)
131
stats["edges"] += 1
132
133
# Walk body for methods
134
body = node.child_by_field_name("body")
135
if not body:
136
body = next((c for c in node.children if c.type == "body_statement"), None)
137
if body:
138
for child in body.children:
139
if child.type in ("method", "singleton_method"):
140
self._handle_method(child, source, file_path, store, stats, class_name=name)
141
142
def _handle_module(
143
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
144
) -> None:
145
name_node = node.child_by_field_name("name")
146
if not name_node:
147
name_node = next(
148
(c for c in node.children if c.type in ("constant", "scope_resolution")), None
149
)
150
if not name_node:
151
return
152
name = _node_text(name_node, source)
153
154
store.create_node(
155
NodeLabel.Class,
156
{
157
"name": name,
158
"file_path": file_path,
159
"line_start": node.start_point[0] + 1,
160
"line_end": node.end_point[0] + 1,
161
"docstring": "module",
162
},
163
)
164
store.create_edge(
165
NodeLabel.File,
166
{"path": file_path},
167
EdgeType.CONTAINS,
168
NodeLabel.Class,
169
{"name": name, "file_path": file_path},
170
)
171
stats["classes"] += 1
172
stats["edges"] += 1
173
174
body = node.child_by_field_name("body")
175
if not body:
176
body = next((c for c in node.children if c.type == "body_statement"), None)
177
if body:
178
for child in body.children:
179
if child.type in ("method", "singleton_method"):
180
self._handle_method(child, source, file_path, store, stats, class_name=name)
181
182
def _handle_method(
183
self,
184
node,
185
source: bytes,
186
file_path: str,
187
store: GraphStore,
188
stats: dict,
189
class_name: str | None,
190
) -> None:
191
name_node = node.child_by_field_name("name")
192
if not name_node:
193
name_node = next((c for c in node.children if c.type == "identifier"), None)
194
if not name_node:
195
return
196
name = _node_text(name_node, source)
197
198
label = NodeLabel.Method if class_name else NodeLabel.Function
199
store.create_node(
200
label,
201
{
202
"name": name,
203
"file_path": file_path,
204
"line_start": node.start_point[0] + 1,
205
"line_end": node.end_point[0] + 1,
206
"docstring": "",
207
"class_name": class_name or "",
208
},
209
)
210
211
container_label = NodeLabel.Class if class_name else NodeLabel.File
212
container_key = (
213
{"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
214
)
215
store.create_edge(
216
container_label,
217
container_key,
218
EdgeType.CONTAINS,
219
label,
220
{"name": name, "file_path": file_path},
221
)
222
stats["functions"] += 1
223
stats["edges"] += 1
224
225
self._extract_calls(node, source, file_path, name, label, store, stats)
226
227
def _maybe_handle_require(
228
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
229
) -> None:
230
method_node = node.child_by_field_name("method")
231
if not method_node:
232
method_node = next((c for c in node.children if c.type == "identifier"), None)
233
if not method_node:
234
return
235
method_name = _node_text(method_node, source)
236
if method_name not in ("require", "require_relative", "load"):
237
return
238
239
args = node.child_by_field_name("arguments")
240
if not args:
241
return
242
for child in args.children:
243
if child.type in ("string", "simple_symbol"):
244
module = _node_text(child, source).strip("'\":")
245
store.create_node(
246
NodeLabel.Import,
247
{
248
"name": module,
249
"file_path": file_path,
250
"line_start": node.start_point[0] + 1,
251
"module": module,
252
},
253
)
254
store.create_edge(
255
NodeLabel.File,
256
{"path": file_path},
257
EdgeType.IMPORTS,
258
NodeLabel.Import,
259
{"name": module, "file_path": file_path},
260
)
261
stats["edges"] += 1
262
break
263
264
def _extract_calls(
265
self,
266
fn_node,
267
source: bytes,
268
file_path: str,
269
fn_name: str,
270
fn_label: str,
271
store: GraphStore,
272
stats: dict,
273
) -> None:
274
def walk(node):
275
if node.type == "call":
276
method_node = node.child_by_field_name("method")
277
if not method_node:
278
method_node = next((c for c in node.children if c.type == "identifier"), None)
279
if method_node:
280
callee = _node_text(method_node, source)
281
if callee not in ("require", "require_relative", "load"):
282
store.create_edge(
283
fn_label,
284
{"name": fn_name, "file_path": file_path},
285
EdgeType.CALLS,
286
NodeLabel.Function,
287
{"name": callee, "file_path": file_path},
288
)
289
stats["edges"] += 1
290
for child in node.children:
291
walk(child)
292
293
body = fn_node.child_by_field_name("body")
294
if not body:
295
body = next((c for c in fn_node.children if c.type == "body_statement"), None)
296
if body:
297
walk(body)
298

Keyboard Shortcuts

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