Navegador

navegador / navegador / ingestion / java.py
Blame History Raw 299 lines
1
"""
2
Java AST parser — extracts classes, interfaces, methods, constructors,
3
imports, inheritance, and call edges from .java 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_java_language():
17
try:
18
import tree_sitter_java as tsjava # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tsjava.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-java: pip install tree-sitter-java") 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 _javadoc(node, source: bytes) -> str:
31
"""Return the Javadoc (/** ... */) comment preceding a node, if any."""
32
parent = node.parent
33
if not parent:
34
return ""
35
siblings = list(parent.children)
36
idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
37
if idx <= 0:
38
return ""
39
prev = siblings[idx - 1]
40
if prev.type == "block_comment":
41
raw = _node_text(prev, source).strip()
42
if raw.startswith("/**"):
43
return raw.strip("/**").strip("*/").strip()
44
return ""
45
46
47
class JavaParser(LanguageParser):
48
"""Parses Java source files into the navegador graph."""
49
50
def __init__(self) -> None:
51
from tree_sitter import Parser # type: ignore[import]
52
53
self._parser = Parser(_get_java_language())
54
55
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
56
source = path.read_bytes()
57
tree = self._parser.parse(source)
58
rel_path = str(path.relative_to(repo_root))
59
60
store.create_node(
61
NodeLabel.File,
62
{
63
"name": path.name,
64
"path": rel_path,
65
"language": "java",
66
"line_count": source.count(b"\n"),
67
},
68
)
69
70
stats = {"functions": 0, "classes": 0, "edges": 0}
71
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
72
return stats
73
74
# ── AST walker ────────────────────────────────────────────────────────────
75
76
def _walk(
77
self,
78
node,
79
source: bytes,
80
file_path: str,
81
store: GraphStore,
82
stats: dict,
83
class_name: str | None,
84
) -> None:
85
if node.type in ("class_declaration", "record_declaration"):
86
self._handle_class(node, source, file_path, store, stats)
87
return
88
if node.type == "interface_declaration":
89
self._handle_interface(node, source, file_path, store, stats)
90
return
91
if node.type == "import_declaration":
92
self._handle_import(node, source, file_path, store, stats)
93
return
94
for child in node.children:
95
self._walk(child, source, file_path, store, stats, class_name)
96
97
# ── Handlers ──────────────────────────────────────────────────────────────
98
99
def _handle_class(
100
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
101
) -> None:
102
name_node = node.child_by_field_name("name")
103
if not name_node:
104
return
105
name = _node_text(name_node, source)
106
docstring = _javadoc(node, source)
107
108
store.create_node(
109
NodeLabel.Class,
110
{
111
"name": name,
112
"file_path": file_path,
113
"line_start": node.start_point[0] + 1,
114
"line_end": node.end_point[0] + 1,
115
"docstring": docstring,
116
},
117
)
118
store.create_edge(
119
NodeLabel.File,
120
{"path": file_path},
121
EdgeType.CONTAINS,
122
NodeLabel.Class,
123
{"name": name, "file_path": file_path},
124
)
125
stats["classes"] += 1
126
stats["edges"] += 1
127
128
# Superclass → INHERITS edge
129
superclass = node.child_by_field_name("superclass")
130
if superclass:
131
for child in superclass.children:
132
if child.type == "type_identifier":
133
parent_name = _node_text(child, source)
134
store.create_edge(
135
NodeLabel.Class,
136
{"name": name, "file_path": file_path},
137
EdgeType.INHERITS,
138
NodeLabel.Class,
139
{"name": parent_name, "file_path": file_path},
140
)
141
stats["edges"] += 1
142
break
143
144
# Walk class body for methods and constructors
145
body = node.child_by_field_name("body")
146
if body:
147
for child in body.children:
148
if child.type in ("method_declaration", "constructor_declaration"):
149
self._handle_method(child, source, file_path, store, stats, class_name=name)
150
elif child.type in (
151
"class_declaration",
152
"record_declaration",
153
"interface_declaration",
154
):
155
# Nested class — register but don't recurse into methods
156
inner_name_node = child.child_by_field_name("name")
157
if inner_name_node:
158
inner_name = _node_text(inner_name_node, source)
159
store.create_node(
160
NodeLabel.Class,
161
{
162
"name": inner_name,
163
"file_path": file_path,
164
"line_start": child.start_point[0] + 1,
165
"line_end": child.end_point[0] + 1,
166
"docstring": "",
167
},
168
)
169
store.create_edge(
170
NodeLabel.Class,
171
{"name": name, "file_path": file_path},
172
EdgeType.CONTAINS,
173
NodeLabel.Class,
174
{"name": inner_name, "file_path": file_path},
175
)
176
stats["classes"] += 1
177
stats["edges"] += 1
178
179
def _handle_interface(
180
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
181
) -> None:
182
name_node = node.child_by_field_name("name")
183
if not name_node:
184
return
185
name = _node_text(name_node, source)
186
docstring = _javadoc(node, source)
187
188
store.create_node(
189
NodeLabel.Class,
190
{
191
"name": name,
192
"file_path": file_path,
193
"line_start": node.start_point[0] + 1,
194
"line_end": node.end_point[0] + 1,
195
"docstring": f"interface: {docstring}".strip(": "),
196
},
197
)
198
store.create_edge(
199
NodeLabel.File,
200
{"path": file_path},
201
EdgeType.CONTAINS,
202
NodeLabel.Class,
203
{"name": name, "file_path": file_path},
204
)
205
stats["classes"] += 1
206
stats["edges"] += 1
207
208
# Walk interface body for method signatures
209
body = node.child_by_field_name("body")
210
if body:
211
for child in body.children:
212
if child.type == "method_declaration":
213
self._handle_method(child, source, file_path, store, stats, class_name=name)
214
215
def _handle_method(
216
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict, class_name: str
217
) -> None:
218
name_node = node.child_by_field_name("name")
219
if not name_node:
220
return
221
name = _node_text(name_node, source)
222
docstring = _javadoc(node, source)
223
224
store.create_node(
225
NodeLabel.Method,
226
{
227
"name": name,
228
"file_path": file_path,
229
"line_start": node.start_point[0] + 1,
230
"line_end": node.end_point[0] + 1,
231
"docstring": docstring,
232
"class_name": class_name,
233
},
234
)
235
store.create_edge(
236
NodeLabel.Class,
237
{"name": class_name, "file_path": file_path},
238
EdgeType.CONTAINS,
239
NodeLabel.Method,
240
{"name": name, "file_path": file_path},
241
)
242
stats["functions"] += 1
243
stats["edges"] += 1
244
245
self._extract_calls(node, source, file_path, name, store, stats)
246
247
def _handle_import(
248
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
249
) -> None:
250
# import java.util.List; → strip keyword + semicolon
251
raw = _node_text(node, source)
252
module = raw.removeprefix("import").removeprefix(" static").removesuffix(";").strip()
253
store.create_node(
254
NodeLabel.Import,
255
{
256
"name": module,
257
"file_path": file_path,
258
"line_start": node.start_point[0] + 1,
259
"module": module,
260
},
261
)
262
store.create_edge(
263
NodeLabel.File,
264
{"path": file_path},
265
EdgeType.IMPORTS,
266
NodeLabel.Import,
267
{"name": module, "file_path": file_path},
268
)
269
stats["edges"] += 1
270
271
def _extract_calls(
272
self,
273
method_node,
274
source: bytes,
275
file_path: str,
276
method_name: str,
277
store: GraphStore,
278
stats: dict,
279
) -> None:
280
def walk(node):
281
if node.type == "method_invocation":
282
name_node = node.child_by_field_name("name")
283
if name_node:
284
callee = _node_text(name_node, source)
285
store.create_edge(
286
NodeLabel.Method,
287
{"name": method_name, "file_path": file_path},
288
EdgeType.CALLS,
289
NodeLabel.Function,
290
{"name": callee, "file_path": file_path},
291
)
292
stats["edges"] += 1
293
for child in node.children:
294
walk(child)
295
296
body = method_node.child_by_field_name("body")
297
if body:
298
walk(body)
299

Keyboard Shortcuts

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