Navegador

Blame History Raw 284 lines
1
"""
2
C++ AST parser — extracts classes, structs, namespaces, functions, methods,
3
and #include directives from .cpp, .hpp, .cc, and .cxx 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_cpp_language():
17
try:
18
import tree_sitter_cpp as tscpp # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tscpp.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-cpp: pip install tree-sitter-cpp") 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 CppParser(LanguageParser):
31
"""Parses C++ 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_cpp_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": "cpp",
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 in ("class_specifier", "struct_specifier"):
69
self._handle_class(node, source, file_path, store, stats)
70
return
71
if node.type == "function_definition":
72
self._handle_function(node, source, file_path, store, stats, class_name)
73
return
74
if node.type == "preproc_include":
75
self._handle_include(node, source, file_path, store, stats)
76
return
77
if node.type == "namespace_definition":
78
# Recurse into namespace body
79
body = node.child_by_field_name("body")
80
if not body:
81
body = next((c for c in node.children if c.type == "declaration_list"), None)
82
if body:
83
for child in body.children:
84
self._walk(child, source, file_path, store, stats, class_name)
85
return
86
for child in node.children:
87
self._walk(child, source, file_path, store, stats, class_name)
88
89
# ── Handlers ──────────────────────────────────────────────────────────────
90
91
def _handle_class(
92
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
93
) -> None:
94
name_node = node.child_by_field_name("name")
95
if not name_node:
96
name_node = next((c for c in node.children if c.type == "type_identifier"), None)
97
if not name_node:
98
return
99
name = _node_text(name_node, source)
100
101
store.create_node(
102
NodeLabel.Class,
103
{
104
"name": name,
105
"file_path": file_path,
106
"line_start": node.start_point[0] + 1,
107
"line_end": node.end_point[0] + 1,
108
"docstring": "",
109
},
110
)
111
store.create_edge(
112
NodeLabel.File,
113
{"path": file_path},
114
EdgeType.CONTAINS,
115
NodeLabel.Class,
116
{"name": name, "file_path": file_path},
117
)
118
stats["classes"] += 1
119
stats["edges"] += 1
120
121
# Base classes
122
base_clause = node.child_by_field_name("base_clause")
123
if not base_clause:
124
base_clause = next((c for c in node.children if c.type == "base_class_clause"), None)
125
if base_clause:
126
for child in base_clause.children:
127
if child.type == "type_identifier":
128
parent_name = _node_text(child, source)
129
store.create_edge(
130
NodeLabel.Class,
131
{"name": name, "file_path": file_path},
132
EdgeType.INHERITS,
133
NodeLabel.Class,
134
{"name": parent_name, "file_path": file_path},
135
)
136
stats["edges"] += 1
137
138
# Walk class body for member functions
139
body = node.child_by_field_name("body")
140
if not body:
141
body = next((c for c in node.children if c.type == "field_declaration_list"), None)
142
if body:
143
for child in body.children:
144
if child.type == "function_definition":
145
self._handle_function(child, source, file_path, store, stats, class_name=name)
146
147
def _handle_function(
148
self,
149
node,
150
source: bytes,
151
file_path: str,
152
store: GraphStore,
153
stats: dict,
154
class_name: str | None,
155
) -> None:
156
declarator = node.child_by_field_name("declarator")
157
name = self._extract_function_name(declarator, source)
158
if not name:
159
return
160
161
label = NodeLabel.Method if class_name else NodeLabel.Function
162
store.create_node(
163
label,
164
{
165
"name": name,
166
"file_path": file_path,
167
"line_start": node.start_point[0] + 1,
168
"line_end": node.end_point[0] + 1,
169
"docstring": "",
170
"class_name": class_name or "",
171
},
172
)
173
174
container_label = NodeLabel.Class if class_name else NodeLabel.File
175
container_key = (
176
{"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
177
)
178
store.create_edge(
179
container_label,
180
container_key,
181
EdgeType.CONTAINS,
182
label,
183
{"name": name, "file_path": file_path},
184
)
185
stats["functions"] += 1
186
stats["edges"] += 1
187
188
self._extract_calls(node, source, file_path, name, label, store, stats)
189
190
def _extract_function_name(self, declarator, source: bytes) -> str | None:
191
"""Recursively dig through C++ declarator nodes to find the function name."""
192
if declarator is None:
193
return None
194
if declarator.type == "identifier":
195
return _node_text(declarator, source)
196
if declarator.type == "qualified_identifier":
197
# MyClass::method — take the last component
198
name_node = declarator.child_by_field_name("name")
199
if name_node:
200
return _node_text(name_node, source)
201
# fallback: last identifier child
202
ids = [c for c in declarator.children if c.type == "identifier"]
203
if ids:
204
return _node_text(ids[-1], source)
205
if declarator.type in ("function_declarator", "pointer_declarator", "reference_declarator"):
206
inner = declarator.child_by_field_name("declarator")
207
return self._extract_function_name(inner, source)
208
# Fallback: first identifier child
209
for child in declarator.children:
210
if child.type == "identifier":
211
return _node_text(child, source)
212
return None
213
214
def _handle_include(
215
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
216
) -> None:
217
path_node = node.child_by_field_name("path")
218
if not path_node:
219
path_node = next(
220
(c for c in node.children if c.type in ("string_literal", "system_lib_string")),
221
None,
222
)
223
if not path_node:
224
return
225
module = _node_text(path_node, source).strip('<>"')
226
store.create_node(
227
NodeLabel.Import,
228
{
229
"name": module,
230
"file_path": file_path,
231
"line_start": node.start_point[0] + 1,
232
"module": module,
233
},
234
)
235
store.create_edge(
236
NodeLabel.File,
237
{"path": file_path},
238
EdgeType.IMPORTS,
239
NodeLabel.Import,
240
{"name": module, "file_path": file_path},
241
)
242
stats["edges"] += 1
243
244
def _extract_calls(
245
self,
246
fn_node,
247
source: bytes,
248
file_path: str,
249
fn_name: str,
250
fn_label: str,
251
store: GraphStore,
252
stats: dict,
253
) -> None:
254
def walk(node):
255
if node.type == "call_expression":
256
func = node.child_by_field_name("function")
257
if not func:
258
func = next(
259
(
260
c
261
for c in node.children
262
if c.type in ("identifier", "qualified_identifier", "field_expression")
263
),
264
None,
265
)
266
if func:
267
callee = _node_text(func, source).split("::")[-1].split(".")[-1].split("->")[-1]
268
store.create_edge(
269
fn_label,
270
{"name": fn_name, "file_path": file_path},
271
EdgeType.CALLS,
272
NodeLabel.Function,
273
{"name": callee, "file_path": file_path},
274
)
275
stats["edges"] += 1
276
for child in node.children:
277
walk(child)
278
279
body = fn_node.child_by_field_name("body")
280
if not body:
281
body = next((c for c in fn_node.children if c.type == "compound_statement"), None)
282
if body:
283
walk(body)
284

Keyboard Shortcuts

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