Navegador

navegador / navegador / ingestion / swift.py
Blame History Raw 267 lines
1
"""
2
Swift AST parser — extracts classes, structs, enums, protocols, functions,
3
methods, and imports from .swift 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_swift_language():
17
try:
18
import tree_sitter_swift as tsswift # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tsswift.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-swift: pip install tree-sitter-swift") 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 SwiftParser(LanguageParser):
31
"""Parses Swift 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_swift_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": "swift",
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 (
69
"class_declaration",
70
"struct_declaration",
71
"enum_declaration",
72
"protocol_declaration",
73
"extension_declaration",
74
):
75
self._handle_class(node, source, file_path, store, stats)
76
return
77
if node.type == "function_declaration":
78
self._handle_function(node, source, file_path, store, stats, class_name)
79
return
80
if node.type == "import_declaration":
81
self._handle_import(node, source, file_path, store, stats)
82
return
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 ("type_identifier", "simple_identifier")),
95
None,
96
)
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
# Inheritance / conformance
122
inheritance = node.child_by_field_name("type_inheritance_clause")
123
if inheritance:
124
for child in inheritance.children:
125
if child.type == "type_identifier":
126
parent_name = _node_text(child, source)
127
store.create_edge(
128
NodeLabel.Class,
129
{"name": name, "file_path": file_path},
130
EdgeType.INHERITS,
131
NodeLabel.Class,
132
{"name": parent_name, "file_path": file_path},
133
)
134
stats["edges"] += 1
135
136
# Walk body for member functions
137
body = node.child_by_field_name("body")
138
if not body:
139
body = next(
140
(
141
c
142
for c in node.children
143
if c.type
144
in ("class_body", "struct_body", "enum_body", "protocol_body", "extension_body")
145
),
146
None,
147
)
148
if body:
149
for child in body.children:
150
if child.type == "function_declaration":
151
self._handle_function(child, source, file_path, store, stats, class_name=name)
152
153
def _handle_function(
154
self,
155
node,
156
source: bytes,
157
file_path: str,
158
store: GraphStore,
159
stats: dict,
160
class_name: str | None,
161
) -> None:
162
name_node = node.child_by_field_name("name")
163
if not name_node:
164
name_node = next(
165
(c for c in node.children if c.type in ("simple_identifier", "identifier")), None
166
)
167
if not name_node:
168
return
169
name = _node_text(name_node, source)
170
171
label = NodeLabel.Method if class_name else NodeLabel.Function
172
store.create_node(
173
label,
174
{
175
"name": name,
176
"file_path": file_path,
177
"line_start": node.start_point[0] + 1,
178
"line_end": node.end_point[0] + 1,
179
"docstring": "",
180
"class_name": class_name or "",
181
},
182
)
183
184
container_label = NodeLabel.Class if class_name else NodeLabel.File
185
container_key = (
186
{"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
187
)
188
store.create_edge(
189
container_label,
190
container_key,
191
EdgeType.CONTAINS,
192
label,
193
{"name": name, "file_path": file_path},
194
)
195
stats["functions"] += 1
196
stats["edges"] += 1
197
198
self._extract_calls(node, source, file_path, name, label, store, stats)
199
200
def _handle_import(
201
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
202
) -> None:
203
raw = _node_text(node, source).strip()
204
module = raw.removeprefix("import").strip()
205
if not module:
206
return
207
store.create_node(
208
NodeLabel.Import,
209
{
210
"name": module,
211
"file_path": file_path,
212
"line_start": node.start_point[0] + 1,
213
"module": module,
214
},
215
)
216
store.create_edge(
217
NodeLabel.File,
218
{"path": file_path},
219
EdgeType.IMPORTS,
220
NodeLabel.Import,
221
{"name": module, "file_path": file_path},
222
)
223
stats["edges"] += 1
224
225
def _extract_calls(
226
self,
227
fn_node,
228
source: bytes,
229
file_path: str,
230
fn_name: str,
231
fn_label: str,
232
store: GraphStore,
233
stats: dict,
234
) -> None:
235
def walk(node):
236
if node.type == "call_expression":
237
func = node.child_by_field_name("function")
238
if not func:
239
func = next(
240
(
241
c
242
for c in node.children
243
if c.type in ("simple_identifier", "navigation_expression")
244
),
245
None,
246
)
247
if func:
248
callee = _node_text(func, source).split(".")[-1]
249
store.create_edge(
250
fn_label,
251
{"name": fn_name, "file_path": file_path},
252
EdgeType.CALLS,
253
NodeLabel.Function,
254
{"name": callee, "file_path": file_path},
255
)
256
stats["edges"] += 1
257
for child in node.children:
258
walk(child)
259
260
body = fn_node.child_by_field_name("body")
261
if not body:
262
body = next(
263
(c for c in fn_node.children if c.type in ("function_body", "code_block")), None
264
)
265
if body:
266
walk(body)
267

Keyboard Shortcuts

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