Navegador

navegador / navegador / ingestion / kotlin.py
Blame History Raw 233 lines
1
"""
2
Kotlin AST parser — extracts classes, objects, functions, methods, and
3
imports from .kt and .kts 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_kotlin_language():
17
try:
18
import tree_sitter_kotlin as tskotlin # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tskotlin.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-kotlin: pip install tree-sitter-kotlin") 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 KotlinParser(LanguageParser):
31
"""Parses Kotlin 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_kotlin_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": "kotlin",
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_declaration", "object_declaration", "interface_declaration"):
69
self._handle_class(node, source, file_path, store, stats)
70
return
71
if node.type == "function_declaration":
72
self._handle_function(node, source, file_path, store, stats, class_name)
73
return
74
if node.type == "import_header":
75
self._handle_import(node, source, file_path, store, stats)
76
return
77
for child in node.children:
78
self._walk(child, source, file_path, store, stats, class_name)
79
80
# ── Handlers ──────────────────────────────────────────────────────────────
81
82
def _handle_class(
83
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
84
) -> None:
85
name_node = node.child_by_field_name("name")
86
if not name_node:
87
# fallback: first simple_identifier child
88
name_node = next((c for c in node.children if c.type == "simple_identifier"), None)
89
if not name_node:
90
return
91
name = _node_text(name_node, source)
92
93
store.create_node(
94
NodeLabel.Class,
95
{
96
"name": name,
97
"file_path": file_path,
98
"line_start": node.start_point[0] + 1,
99
"line_end": node.end_point[0] + 1,
100
"docstring": "",
101
},
102
)
103
store.create_edge(
104
NodeLabel.File,
105
{"path": file_path},
106
EdgeType.CONTAINS,
107
NodeLabel.Class,
108
{"name": name, "file_path": file_path},
109
)
110
stats["classes"] += 1
111
stats["edges"] += 1
112
113
# Walk class body for member functions
114
body = node.child_by_field_name("body")
115
if not body:
116
body = next((c for c in node.children if c.type in ("class_body", "object_body")), None)
117
if body:
118
for child in body.children:
119
if child.type == "function_declaration":
120
self._handle_function(child, source, file_path, store, stats, class_name=name)
121
122
def _handle_function(
123
self,
124
node,
125
source: bytes,
126
file_path: str,
127
store: GraphStore,
128
stats: dict,
129
class_name: str | None,
130
) -> None:
131
name_node = node.child_by_field_name("name")
132
if not name_node:
133
name_node = next((c for c in node.children if c.type == "simple_identifier"), None)
134
if not name_node:
135
return
136
name = _node_text(name_node, source)
137
138
label = NodeLabel.Method if class_name else NodeLabel.Function
139
store.create_node(
140
label,
141
{
142
"name": name,
143
"file_path": file_path,
144
"line_start": node.start_point[0] + 1,
145
"line_end": node.end_point[0] + 1,
146
"docstring": "",
147
"class_name": class_name or "",
148
},
149
)
150
151
container_label = NodeLabel.Class if class_name else NodeLabel.File
152
container_key = (
153
{"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
154
)
155
store.create_edge(
156
container_label,
157
container_key,
158
EdgeType.CONTAINS,
159
label,
160
{"name": name, "file_path": file_path},
161
)
162
stats["functions"] += 1
163
stats["edges"] += 1
164
165
self._extract_calls(node, source, file_path, name, label, store, stats)
166
167
def _handle_import(
168
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
169
) -> None:
170
# import_header: "import" identifier ("." identifier)*
171
raw = _node_text(node, source).strip()
172
module = raw.removeprefix("import").strip()
173
if not module:
174
return
175
store.create_node(
176
NodeLabel.Import,
177
{
178
"name": module,
179
"file_path": file_path,
180
"line_start": node.start_point[0] + 1,
181
"module": module,
182
},
183
)
184
store.create_edge(
185
NodeLabel.File,
186
{"path": file_path},
187
EdgeType.IMPORTS,
188
NodeLabel.Import,
189
{"name": module, "file_path": file_path},
190
)
191
stats["edges"] += 1
192
193
def _extract_calls(
194
self,
195
fn_node,
196
source: bytes,
197
file_path: str,
198
fn_name: str,
199
fn_label: str,
200
store: GraphStore,
201
stats: dict,
202
) -> None:
203
def walk(node):
204
if node.type == "call_expression":
205
func = node.child_by_field_name("calleeExpression")
206
if not func:
207
func = next(
208
(
209
c
210
for c in node.children
211
if c.type in ("simple_identifier", "navigation_expression")
212
),
213
None,
214
)
215
if func:
216
callee = _node_text(func, source).split(".")[-1]
217
store.create_edge(
218
fn_label,
219
{"name": fn_name, "file_path": file_path},
220
EdgeType.CALLS,
221
NodeLabel.Function,
222
{"name": callee, "file_path": file_path},
223
)
224
stats["edges"] += 1
225
for child in node.children:
226
walk(child)
227
228
body = fn_node.child_by_field_name("body")
229
if not body:
230
body = next((c for c in fn_node.children if c.type in ("function_body", "block")), None)
231
if body:
232
walk(body)
233

Keyboard Shortcuts

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