Navegador

navegador / navegador / ingestion / rust.py
Blame History Raw 249 lines
1
"""
2
Rust AST parser — extracts functions, methods (from impl blocks),
3
structs/enums/traits, use declarations, and call edges from .rs files.
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_rust_language():
17
try:
18
import tree_sitter_rust as tsrust # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tsrust.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-rust: pip install tree-sitter-rust") 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
def _doc_comment(node, source: bytes) -> str:
31
"""Collect preceding /// doc-comment lines from siblings."""
32
parent = node.parent
33
if not parent:
34
return ""
35
siblings = list(parent.children)
36
idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
37
if idx <= 0:
38
return ""
39
lines: list[str] = []
40
for i in range(idx - 1, -1, -1):
41
sib = siblings[i]
42
raw = _node_text(sib, source)
43
if sib.type == "line_comment" and raw.startswith("///"):
44
lines.insert(0, raw.lstrip("/").strip())
45
else:
46
break
47
return " ".join(lines)
48
49
50
class RustParser(LanguageParser):
51
"""Parses Rust source files into the navegador graph."""
52
53
def __init__(self) -> None:
54
from tree_sitter import Parser # type: ignore[import]
55
56
self._parser = Parser(_get_rust_language())
57
58
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
59
source = path.read_bytes()
60
tree = self._parser.parse(source)
61
rel_path = str(path.relative_to(repo_root))
62
63
store.create_node(
64
NodeLabel.File,
65
{
66
"name": path.name,
67
"path": rel_path,
68
"language": "rust",
69
"line_count": source.count(b"\n"),
70
},
71
)
72
73
stats = {"functions": 0, "classes": 0, "edges": 0}
74
self._walk(tree.root_node, source, rel_path, store, stats, impl_type=None)
75
return stats
76
77
# ── AST walker ────────────────────────────────────────────────────────────
78
79
def _walk(
80
self,
81
node,
82
source: bytes,
83
file_path: str,
84
store: GraphStore,
85
stats: dict,
86
impl_type: str | None,
87
) -> None:
88
if node.type == "function_item":
89
self._handle_function(node, source, file_path, store, stats, impl_type)
90
return
91
if node.type == "impl_item":
92
self._handle_impl(node, source, file_path, store, stats)
93
return
94
if node.type in ("struct_item", "enum_item", "trait_item"):
95
self._handle_type(node, source, file_path, store, stats)
96
return
97
if node.type == "use_declaration":
98
self._handle_use(node, source, file_path, store, stats)
99
return
100
for child in node.children:
101
self._walk(child, source, file_path, store, stats, impl_type)
102
103
# ── Handlers ──────────────────────────────────────────────────────────────
104
105
def _handle_function(
106
self,
107
node,
108
source: bytes,
109
file_path: str,
110
store: GraphStore,
111
stats: dict,
112
impl_type: str | None,
113
) -> None:
114
name_node = node.child_by_field_name("name")
115
if not name_node:
116
return
117
name = _node_text(name_node, source)
118
docstring = _doc_comment(node, source)
119
label = NodeLabel.Method if impl_type else NodeLabel.Function
120
121
store.create_node(
122
label,
123
{
124
"name": name,
125
"file_path": file_path,
126
"line_start": node.start_point[0] + 1,
127
"line_end": node.end_point[0] + 1,
128
"docstring": docstring,
129
"class_name": impl_type or "",
130
},
131
)
132
133
container_label = NodeLabel.Class if impl_type else NodeLabel.File
134
container_key = (
135
{"name": impl_type, "file_path": file_path} if impl_type else {"path": file_path}
136
)
137
store.create_edge(
138
container_label,
139
container_key,
140
EdgeType.CONTAINS,
141
label,
142
{"name": name, "file_path": file_path},
143
)
144
stats["functions"] += 1
145
stats["edges"] += 1
146
147
self._extract_calls(node, source, file_path, name, label, store, stats)
148
149
def _handle_impl(
150
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
151
) -> None:
152
type_node = node.child_by_field_name("type")
153
impl_type_name = _node_text(type_node, source) if type_node else ""
154
155
body = node.child_by_field_name("body")
156
if body:
157
for child in body.children:
158
if child.type == "function_item":
159
self._handle_function(
160
child, source, file_path, store, stats, impl_type=impl_type_name or None
161
)
162
163
def _handle_type(
164
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
165
) -> None:
166
name_node = node.child_by_field_name("name")
167
if not name_node:
168
return
169
name = _node_text(name_node, source)
170
kind = {"struct_item": "struct", "enum_item": "enum", "trait_item": "trait"}.get(
171
node.type, ""
172
)
173
docstring = _doc_comment(node, source)
174
175
store.create_node(
176
NodeLabel.Class,
177
{
178
"name": name,
179
"file_path": file_path,
180
"line_start": node.start_point[0] + 1,
181
"line_end": node.end_point[0] + 1,
182
"docstring": f"{kind}: {docstring}".strip(": ") if kind else docstring,
183
},
184
)
185
store.create_edge(
186
NodeLabel.File,
187
{"path": file_path},
188
EdgeType.CONTAINS,
189
NodeLabel.Class,
190
{"name": name, "file_path": file_path},
191
)
192
stats["classes"] += 1
193
stats["edges"] += 1
194
195
def _handle_use(
196
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
197
) -> None:
198
raw = _node_text(node, source)
199
module = raw.removeprefix("use ").removesuffix(";").strip()
200
store.create_node(
201
NodeLabel.Import,
202
{
203
"name": module,
204
"file_path": file_path,
205
"line_start": node.start_point[0] + 1,
206
"module": module,
207
},
208
)
209
store.create_edge(
210
NodeLabel.File,
211
{"path": file_path},
212
EdgeType.IMPORTS,
213
NodeLabel.Import,
214
{"name": module, "file_path": file_path},
215
)
216
stats["edges"] += 1
217
218
def _extract_calls(
219
self,
220
fn_node,
221
source: bytes,
222
file_path: str,
223
fn_name: str,
224
fn_label: str,
225
store: GraphStore,
226
stats: dict,
227
) -> None:
228
def walk(node):
229
if node.type == "call_expression":
230
func = node.child_by_field_name("function")
231
if func:
232
text = _node_text(func, source)
233
# Handle Foo::bar() and obj.method()
234
callee = text.replace("::", ".").split(".")[-1]
235
store.create_edge(
236
fn_label,
237
{"name": fn_name, "file_path": file_path},
238
EdgeType.CALLS,
239
NodeLabel.Function,
240
{"name": callee, "file_path": file_path},
241
)
242
stats["edges"] += 1
243
for child in node.children:
244
walk(child)
245
246
body = fn_node.child_by_field_name("body")
247
if body:
248
walk(body)
249

Keyboard Shortcuts

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