Navegador

Blame History Raw 229 lines
1
"""
2
C AST parser — extracts functions, structs, typedefs, and #include directives
3
from .c and .h 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_c_language():
17
try:
18
import tree_sitter_c as tsc # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tsc.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-c: pip install tree-sitter-c") 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 CParser(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_c_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": "c",
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)
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
) -> None:
67
if node.type == "function_definition":
68
self._handle_function(node, source, file_path, store, stats)
69
return
70
if node.type in ("struct_specifier", "union_specifier", "enum_specifier"):
71
self._handle_struct(node, source, file_path, store, stats)
72
return
73
if node.type == "preproc_include":
74
self._handle_include(node, source, file_path, store, stats)
75
return
76
for child in node.children:
77
self._walk(child, source, file_path, store, stats)
78
79
# ── Handlers ──────────────────────────────────────────────────────────────
80
81
def _handle_function(
82
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
83
) -> None:
84
# function_definition: type declarator body
85
# declarator may be a function_declarator with a name field
86
declarator = node.child_by_field_name("declarator")
87
name = self._extract_function_name(declarator, source)
88
if not name:
89
return
90
91
store.create_node(
92
NodeLabel.Function,
93
{
94
"name": name,
95
"file_path": file_path,
96
"line_start": node.start_point[0] + 1,
97
"line_end": node.end_point[0] + 1,
98
"docstring": "",
99
"class_name": "",
100
},
101
)
102
store.create_edge(
103
NodeLabel.File,
104
{"path": file_path},
105
EdgeType.CONTAINS,
106
NodeLabel.Function,
107
{"name": name, "file_path": file_path},
108
)
109
stats["functions"] += 1
110
stats["edges"] += 1
111
112
self._extract_calls(node, source, file_path, name, store, stats)
113
114
def _extract_function_name(self, declarator, source: bytes) -> str | None:
115
"""Recursively dig through declarator nodes to find the function name."""
116
if declarator is None:
117
return None
118
if declarator.type == "identifier":
119
return _node_text(declarator, source)
120
if declarator.type == "function_declarator":
121
inner = declarator.child_by_field_name("declarator")
122
return self._extract_function_name(inner, source)
123
if declarator.type == "pointer_declarator":
124
inner = declarator.child_by_field_name("declarator")
125
return self._extract_function_name(inner, source)
126
# Fallback: look for identifier child
127
for child in declarator.children:
128
if child.type == "identifier":
129
return _node_text(child, source)
130
return None
131
132
def _handle_struct(
133
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
134
) -> None:
135
name_node = node.child_by_field_name("name")
136
if not name_node:
137
name_node = next((c for c in node.children if c.type == "type_identifier"), None)
138
if not name_node:
139
return
140
name = _node_text(name_node, source)
141
142
kind = (
143
"struct"
144
if node.type == "struct_specifier"
145
else ("union" if node.type == "union_specifier" else "enum")
146
)
147
store.create_node(
148
NodeLabel.Class,
149
{
150
"name": name,
151
"file_path": file_path,
152
"line_start": node.start_point[0] + 1,
153
"line_end": node.end_point[0] + 1,
154
"docstring": kind,
155
},
156
)
157
store.create_edge(
158
NodeLabel.File,
159
{"path": file_path},
160
EdgeType.CONTAINS,
161
NodeLabel.Class,
162
{"name": name, "file_path": file_path},
163
)
164
stats["classes"] += 1
165
stats["edges"] += 1
166
167
def _handle_include(
168
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
169
) -> None:
170
path_node = node.child_by_field_name("path")
171
if not path_node:
172
path_node = next(
173
(c for c in node.children if c.type in ("string_literal", "system_lib_string")),
174
None,
175
)
176
if not path_node:
177
return
178
module = _node_text(path_node, source).strip('<>"')
179
store.create_node(
180
NodeLabel.Import,
181
{
182
"name": module,
183
"file_path": file_path,
184
"line_start": node.start_point[0] + 1,
185
"module": module,
186
},
187
)
188
store.create_edge(
189
NodeLabel.File,
190
{"path": file_path},
191
EdgeType.IMPORTS,
192
NodeLabel.Import,
193
{"name": module, "file_path": file_path},
194
)
195
stats["edges"] += 1
196
197
def _extract_calls(
198
self,
199
fn_node,
200
source: bytes,
201
file_path: str,
202
fn_name: str,
203
store: GraphStore,
204
stats: dict,
205
) -> None:
206
def walk(node):
207
if node.type == "call_expression":
208
func = node.child_by_field_name("function")
209
if not func:
210
func = next((c for c in node.children if c.type == "identifier"), None)
211
if func:
212
callee = _node_text(func, source)
213
store.create_edge(
214
NodeLabel.Function,
215
{"name": fn_name, "file_path": file_path},
216
EdgeType.CALLS,
217
NodeLabel.Function,
218
{"name": callee, "file_path": file_path},
219
)
220
stats["edges"] += 1
221
for child in node.children:
222
walk(child)
223
224
body = fn_node.child_by_field_name("body")
225
if not body:
226
body = next((c for c in fn_node.children if c.type == "compound_statement"), None)
227
if body:
228
walk(body)
229

Keyboard Shortcuts

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