Navegador

navegador / navegador / ingestion / puppet.py
Blame History Raw 340 lines
1
"""
2
Puppet manifest parser — extracts classes, defined types, node definitions,
3
resource declarations, includes, and parameters from .pp 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_puppet_language():
17
try:
18
import tree_sitter_puppet as tspuppet # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tspuppet.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-puppet: pip install tree-sitter-puppet") 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 _class_identifier_text(node, source: bytes) -> str:
31
"""Join identifier children of a class_identifier with '::'."""
32
parts = [_node_text(child, source) for child in node.children if child.type == "identifier"]
33
return "::".join(parts) if parts else _node_text(node, source)
34
35
36
class PuppetParser(LanguageParser):
37
"""Parses Puppet manifest files into the navegador graph."""
38
39
def __init__(self) -> None:
40
from tree_sitter import Parser # type: ignore[import]
41
42
self._parser = Parser(_get_puppet_language())
43
44
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
45
source = path.read_bytes()
46
tree = self._parser.parse(source)
47
rel_path = str(path.relative_to(repo_root))
48
49
store.create_node(
50
NodeLabel.File,
51
{
52
"name": path.name,
53
"path": rel_path,
54
"language": "puppet",
55
"line_count": source.count(b"\n"),
56
},
57
)
58
59
stats = {"functions": 0, "classes": 0, "edges": 0}
60
self._walk(tree.root_node, source, rel_path, store, stats)
61
return stats
62
63
# ── AST walker ────────────────────────────────────────────────────────────
64
65
def _walk(self, node, source: bytes, file_path: str, store: GraphStore, stats: dict) -> None:
66
if node.type == "class_definition":
67
self._handle_class(node, source, file_path, store, stats)
68
return
69
if node.type == "defined_resource_type":
70
self._handle_defined_type(node, source, file_path, store, stats)
71
return
72
if node.type == "node_definition":
73
self._handle_node(node, source, file_path, store, stats)
74
return
75
if node.type == "include_statement":
76
self._handle_include(node, source, file_path, store, stats)
77
return
78
for child in node.children:
79
self._walk(child, source, file_path, store, stats)
80
81
# ── Handlers ──────────────────────────────────────────────────────────────
82
83
def _handle_class(
84
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
85
) -> None:
86
name = self._extract_class_identifier(node, source)
87
if not name:
88
return
89
90
store.create_node(
91
NodeLabel.Class,
92
{
93
"name": name,
94
"file_path": file_path,
95
"line_start": node.start_point[0] + 1,
96
"line_end": node.end_point[0] + 1,
97
"docstring": "",
98
"semantic_type": "puppet_class",
99
},
100
)
101
store.create_edge(
102
NodeLabel.File,
103
{"path": file_path},
104
EdgeType.CONTAINS,
105
NodeLabel.Class,
106
{"name": name, "file_path": file_path},
107
)
108
stats["classes"] += 1
109
stats["edges"] += 1
110
111
self._extract_parameters(node, source, file_path, name, store, stats)
112
self._extract_resources(node, source, file_path, name, store, stats)
113
114
def _handle_defined_type(
115
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
116
) -> None:
117
name = self._extract_class_identifier(node, source)
118
if not name:
119
return
120
121
store.create_node(
122
NodeLabel.Class,
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": "",
129
"semantic_type": "puppet_defined_type",
130
},
131
)
132
store.create_edge(
133
NodeLabel.File,
134
{"path": file_path},
135
EdgeType.CONTAINS,
136
NodeLabel.Class,
137
{"name": name, "file_path": file_path},
138
)
139
stats["classes"] += 1
140
stats["edges"] += 1
141
142
self._extract_parameters(node, source, file_path, name, store, stats)
143
self._extract_resources(node, source, file_path, name, store, stats)
144
145
def _handle_node(
146
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
147
) -> None:
148
name = self._extract_node_name(node, source)
149
if not name:
150
return
151
152
store.create_node(
153
NodeLabel.Class,
154
{
155
"name": name,
156
"file_path": file_path,
157
"line_start": node.start_point[0] + 1,
158
"line_end": node.end_point[0] + 1,
159
"docstring": "",
160
"semantic_type": "puppet_node",
161
},
162
)
163
store.create_edge(
164
NodeLabel.File,
165
{"path": file_path},
166
EdgeType.CONTAINS,
167
NodeLabel.Class,
168
{"name": name, "file_path": file_path},
169
)
170
stats["classes"] += 1
171
stats["edges"] += 1
172
173
self._extract_resources(node, source, file_path, name, store, stats)
174
175
def _handle_include(
176
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
177
) -> None:
178
ident_node = None
179
for child in node.children:
180
if child.type == "class_identifier":
181
ident_node = child
182
break
183
if not ident_node:
184
return
185
186
module = _class_identifier_text(ident_node, source)
187
store.create_node(
188
NodeLabel.Import,
189
{
190
"name": module,
191
"file_path": file_path,
192
"line_start": node.start_point[0] + 1,
193
"module": module,
194
"semantic_type": "puppet_include",
195
},
196
)
197
store.create_edge(
198
NodeLabel.File,
199
{"path": file_path},
200
EdgeType.IMPORTS,
201
NodeLabel.Import,
202
{"name": module, "file_path": file_path},
203
)
204
stats["edges"] += 1
205
206
# ── Extractors ────────────────────────────────────────────────────────────
207
208
def _extract_class_identifier(self, node, source: bytes) -> str | None:
209
"""Find and return the class_identifier text from a class/define node."""
210
for child in node.children:
211
if child.type == "class_identifier":
212
return _class_identifier_text(child, source)
213
return None
214
215
def _extract_node_name(self, node, source: bytes) -> str | None:
216
"""Extract the node name from a node_definition (string child of node_name)."""
217
for child in node.children:
218
if child.type == "node_name":
219
for grandchild in child.children:
220
if grandchild.type == "string":
221
return _node_text(grandchild, source).strip("'\"")
222
return _node_text(child, source).strip("'\"")
223
return None
224
225
def _extract_parameters(
226
self,
227
node,
228
source: bytes,
229
file_path: str,
230
class_name: str,
231
store: GraphStore,
232
stats: dict,
233
) -> None:
234
"""Extract parameters from a parameter_list inside a class/define."""
235
for child in node.children:
236
if child.type != "parameter_list":
237
continue
238
for param in child.children:
239
if param.type != "parameter":
240
continue
241
var_node = None
242
for pc in param.children:
243
if pc.type == "variable":
244
var_node = pc
245
break
246
if not var_node:
247
continue
248
var_name = _node_text(var_node, source).lstrip("$")
249
store.create_node(
250
NodeLabel.Variable,
251
{
252
"name": var_name,
253
"file_path": file_path,
254
"line_start": param.start_point[0] + 1,
255
"semantic_type": "puppet_parameter",
256
},
257
)
258
store.create_edge(
259
NodeLabel.Class,
260
{"name": class_name, "file_path": file_path},
261
EdgeType.CONTAINS,
262
NodeLabel.Variable,
263
{"name": var_name, "file_path": file_path},
264
)
265
stats["edges"] += 1
266
267
def _extract_resources(
268
self,
269
node,
270
source: bytes,
271
file_path: str,
272
class_name: str,
273
store: GraphStore,
274
stats: dict,
275
) -> None:
276
"""Walk the block of a class/define/node to find resource declarations."""
277
for child in node.children:
278
if child.type == "block":
279
self._walk_block_for_resources(child, source, file_path, class_name, store, stats)
280
break
281
282
def _walk_block_for_resources(
283
self,
284
node,
285
source: bytes,
286
file_path: str,
287
class_name: str,
288
store: GraphStore,
289
stats: dict,
290
) -> None:
291
"""Recursively find resource_declaration nodes inside a block."""
292
if node.type == "resource_declaration":
293
self._handle_resource(node, source, file_path, class_name, store, stats)
294
return
295
for child in node.children:
296
self._walk_block_for_resources(child, source, file_path, class_name, store, stats)
297
298
def _handle_resource(
299
self,
300
node,
301
source: bytes,
302
file_path: str,
303
class_name: str,
304
store: GraphStore,
305
stats: dict,
306
) -> None:
307
"""Handle a resource_declaration: first identifier = type, first string = title."""
308
res_type = None
309
res_title = None
310
for child in node.children:
311
if child.type == "identifier" and res_type is None:
312
res_type = _node_text(child, source)
313
if child.type == "string" and res_title is None:
314
res_title = _node_text(child, source).strip("'\"")
315
if not res_type:
316
return
317
318
name = f"{res_type}[{res_title}]" if res_title else res_type
319
store.create_node(
320
NodeLabel.Function,
321
{
322
"name": name,
323
"file_path": file_path,
324
"line_start": node.start_point[0] + 1,
325
"line_end": node.end_point[0] + 1,
326
"docstring": "",
327
"class_name": class_name,
328
"semantic_type": "puppet_resource",
329
},
330
)
331
store.create_edge(
332
NodeLabel.Class,
333
{"name": class_name, "file_path": file_path},
334
EdgeType.CONTAINS,
335
NodeLabel.Function,
336
{"name": name, "file_path": file_path},
337
)
338
stats["functions"] += 1
339
stats["edges"] += 1
340

Keyboard Shortcuts

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