Navegador

feat: add Go, Rust, Java parsers; implement TypeScript/JavaScript fully Full AST parsing support for Go (functions, methods via receiver, structs/interfaces, imports, call edges), Rust (functions, impl blocks, structs/enums/traits, use declarations, /// doc comments, call edges), and Java (classes, interfaces, records, constructors, Javadoc, inheritance, static imports, call edges). Rewrites the TypeScript/JavaScript stub with class heritage, interface/type-alias nodes, const arrow functions, JSDoc, and call extraction. Adds tree-sitter-go/rust/java/javascript to pyproject.toml dependencies. Tests for all four parsers (312 passing, 83% total coverage), ruff-clean.

lmata 2026-03-22 23:45 trunk
Commit e52aae0022dbeb503f4be5540ea9baa9efe7937cbbfb825cd0a2bffb05b92a30
--- a/navegador/ingestion/go.py
+++ b/navegador/ingestion/go.py
@@ -0,0 +1,159 @@
1
+"""
2
+Go AST parser — extracts functions, methods, struct/interface types,
3
+imports, and call edges from .go 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_go_language():
17
+ try:
18
+ import tree_sitter_go as tsgo # type: ignore[import]
19
+ from tee_sitter import Language
20
+
21
+ return Language(tsgo.language())
22
+ except ImportError as e:
23
+ raise ImportError("Install tree-sitter-go: pip install tree-sitter-go") from e
24
+
25
+
26
+def _node_text(node, so:n source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
27
+
28
+
29
+class GoParser(LanguageParser):
30
+ """Parses Go source files into the navegador graph."""
31
+
32
+ def __init__(self) -> None:
33
+ f self._parser = Parser(_get_go_language())
34
+
35
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
36
+ source = path.read_bytes()
37
+ tree = self._parser.parse(source)
38
+ rel_path = str(path.relative_to(repo_root))
39
+NodeLabel.File, {
40
+ype"):
41
+
42
+ {
43
+ "nampath.name,
44
+ "pa"language": "go",
45
+ ge": "go",
46
+ })
47
+
48
+"struct_ },
49
+ )
50
+
51
+ stats = {"functions": 0, "classes": 0, "edges": 0}
52
+ self._walk(tree.root_node, source, rel_path, store, stats)
53
+ return stats
54
+
55
+ # ── AST walker ────────────────────────────────────────────────────────────
56
+
57
+ def _walk(self, node, sou
58
+raphStore, stats: de
59
+ name_node =) -> None:
60
+ if node.type == "function_declaration":
61
+ self._handle_function(node, source, file_path, store, stats, receiver=None)
62
+ return
63
+ if node.type == "method_declaration":
64
+ self._handle_method(node, source, file_path, store, stats)
65
+ return
66
+ if node.type == "type_declaration":
67
+ self._handle_type(node, source, file_path, store, stats)
68
+ return
69
+ if node.type == "import_declaration":
70
+ self._handle_import(node, source, file_path, store, stats)
71
+ return
72
+ for child in node.children:
73
+ self._walk(child, source, file_path, store, stats)
74
+
75
+ # ── Handlers ──────────────────────────────────────────────────────────────
76
+
77
+ def _handle_function(self, node, souource: bytes, file_path: store: GraphStore, stats: dicttart_point[0] + 1
78
+ ath.relative_to(repo_) -> None:
79
+ abel.File,
80
+ {
81
+ "name": path.name,
82
+ "path": rel_path,
83
+ "language": "go",
84
+ "line_count": source.count(b"\n"),
85
+ },
86
+ )
87
+label, {
88
+ype"):
89
+ , "edges": 0}
90
+ self._wastats)
91
+ return st"line_end": node.end"docstring": "",
92
+ store.c�───�}��─────────────
93
+
94
+ def _walk(self, node, source: bytes, file_path: str, store: GraphStore, stats: dict) -> None:
95
+ if node.
96
+*").strip()
97
+ifath.relat self._handle_function(node, source, file_path, store, stats, receiver=None)
98
+ if node.type == "method_declaration":
99
+ self._handde, source, file_path, store, stats)
100
+ return
101
+ if node.type == "type_declaration":
102
+ self._handle_type(node, source, file_path, store, stats)
103
+ return
104
+ if node.type == "impself, node, souource: bytes, file_path:ce: bytes, file_path: str, sto) -> None:
105
+ receiver_type = ""
106
+ recv_node = node.child_by_field_name("receiver")
107
+ if recv_node:
108
+ for child in recv_node.children:
109
+ if child.type == "parameter_declaration":
110
+ for c in child.children:
111
+ if c.type in ("type_identifier", "pointer_type"):
112
+ receiver_type = _node_text(c, source).lstrip("*").strip()
113
+ break
114
+ self._handle_function(node, sourcource: bytes, file_path: receiver=receiver_type or Nself, node, souource: bytes, file_patce: bytes, file_path: str, sto) -> None:
115
+ start_point[0] + 1
116
+
117
+ if child.type != "type_spec":
118
+ continue
119
+ name_node = child.child_by_field_name("name")
120
+ type_node = child.child_by_field_name("type")
121
+ if not name_node or not type_node:
122
+ continue
123
+ if type_node.type not in ("struct_type", "interface_type"):
124
+ continue
125
+ name = _node_text(name_node, source)
126
+ kind = "struct" if type_node.type == "structNodeLabel.Class, {
127
+ "name": name,
128
+ path, store, stats)
129
+ return stats
130
+
131
+ # ── AST walker ───────────────────────────────kind,
132
+ stats })ruct_type" else "interfaceedge(
133
+ NodeLabel.File, {"path": file_path},
134
+ EdgeType.CONTAINS,
135
+ NodeLabel.Class,de, source, file_path, )
136
+pe == "imporstats["classes"] += 1
137
+ stats["edges"] += 1
138
+
139
+ def _handle_import(self, node, souource: bytes, file_path:ce: bytes, file_path: str, sto) -> None:
140
+ ict
141
+ ) -> None:
142
+ line_start = node.start_point[0] + 1
143
+ for child in node.children:
144
+ if child.type == "import_spec":
145
+ self._ingest_import_spec(child, source, file_path, line_start, store, stats)
146
+ elif child.type == "import_spec_list":
147
+ for spec in child.children:
148
+ if spec.type == "import_spec":
149
+ self._iource: bytes, file_path: ce).lstrip("*").strip()
150
+ urce files into thedren:
151
+ if chi if ource: bytes, file_path: name_node =) -> None:
152
+ _node = child.child_by_field_name("type")
153
+ if not name_node or not type_node:
154
+ continue
155
+ if type_node.type notNodeLabel.Import, {
156
+ype"):
157
+ "namre.create_node(
158
+ = "struct_t else "interface"
159
+ } if type_node.typ NodeLab
--- a/navegador/ingestion/go.py
+++ b/navegador/ingestion/go.py
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/go.py
+++ b/navegador/ingestion/go.py
@@ -0,0 +1,159 @@
1 """
2 Go AST parser — extracts functions, methods, struct/interface types,
3 imports, and call edges from .go 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_go_language():
17 try:
18 import tree_sitter_go as tsgo # type: ignore[import]
19 from tee_sitter import Language
20
21 return Language(tsgo.language())
22 except ImportError as e:
23 raise ImportError("Install tree-sitter-go: pip install tree-sitter-go") from e
24
25
26 def _node_text(node, so:n source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
27
28
29 class GoParser(LanguageParser):
30 """Parses Go source files into the navegador graph."""
31
32 def __init__(self) -> None:
33 f self._parser = Parser(_get_go_language())
34
35 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
36 source = path.read_bytes()
37 tree = self._parser.parse(source)
38 rel_path = str(path.relative_to(repo_root))
39 NodeLabel.File, {
40 ype"):
41
42 {
43 "nampath.name,
44 "pa"language": "go",
45 ge": "go",
46 })
47
48 "struct_ },
49 )
50
51 stats = {"functions": 0, "classes": 0, "edges": 0}
52 self._walk(tree.root_node, source, rel_path, store, stats)
53 return stats
54
55 # ── AST walker ────────────────────────────────────────────────────────────
56
57 def _walk(self, node, sou
58 raphStore, stats: de
59 name_node =) -> None:
60 if node.type == "function_declaration":
61 self._handle_function(node, source, file_path, store, stats, receiver=None)
62 return
63 if node.type == "method_declaration":
64 self._handle_method(node, source, file_path, store, stats)
65 return
66 if node.type == "type_declaration":
67 self._handle_type(node, source, file_path, store, stats)
68 return
69 if node.type == "import_declaration":
70 self._handle_import(node, source, file_path, store, stats)
71 return
72 for child in node.children:
73 self._walk(child, source, file_path, store, stats)
74
75 # ── Handlers ──────────────────────────────────────────────────────────────
76
77 def _handle_function(self, node, souource: bytes, file_path: store: GraphStore, stats: dicttart_point[0] + 1
78 ath.relative_to(repo_) -> None:
79 abel.File,
80 {
81 "name": path.name,
82 "path": rel_path,
83 "language": "go",
84 "line_count": source.count(b"\n"),
85 },
86 )
87 label, {
88 ype"):
89 , "edges": 0}
90 self._wastats)
91 return st"line_end": node.end"docstring": "",
92 store.c�───�}��─────────────
93
94 def _walk(self, node, source: bytes, file_path: str, store: GraphStore, stats: dict) -> None:
95 if node.
96 *").strip()
97 ifath.relat self._handle_function(node, source, file_path, store, stats, receiver=None)
98 if node.type == "method_declaration":
99 self._handde, source, file_path, store, stats)
100 return
101 if node.type == "type_declaration":
102 self._handle_type(node, source, file_path, store, stats)
103 return
104 if node.type == "impself, node, souource: bytes, file_path:ce: bytes, file_path: str, sto) -> None:
105 receiver_type = ""
106 recv_node = node.child_by_field_name("receiver")
107 if recv_node:
108 for child in recv_node.children:
109 if child.type == "parameter_declaration":
110 for c in child.children:
111 if c.type in ("type_identifier", "pointer_type"):
112 receiver_type = _node_text(c, source).lstrip("*").strip()
113 break
114 self._handle_function(node, sourcource: bytes, file_path: receiver=receiver_type or Nself, node, souource: bytes, file_patce: bytes, file_path: str, sto) -> None:
115 start_point[0] + 1
116
117 if child.type != "type_spec":
118 continue
119 name_node = child.child_by_field_name("name")
120 type_node = child.child_by_field_name("type")
121 if not name_node or not type_node:
122 continue
123 if type_node.type not in ("struct_type", "interface_type"):
124 continue
125 name = _node_text(name_node, source)
126 kind = "struct" if type_node.type == "structNodeLabel.Class, {
127 "name": name,
128 path, store, stats)
129 return stats
130
131 # ── AST walker ───────────────────────────────kind,
132 stats })ruct_type" else "interfaceedge(
133 NodeLabel.File, {"path": file_path},
134 EdgeType.CONTAINS,
135 NodeLabel.Class,de, source, file_path, )
136 pe == "imporstats["classes"] += 1
137 stats["edges"] += 1
138
139 def _handle_import(self, node, souource: bytes, file_path:ce: bytes, file_path: str, sto) -> None:
140 ict
141 ) -> None:
142 line_start = node.start_point[0] + 1
143 for child in node.children:
144 if child.type == "import_spec":
145 self._ingest_import_spec(child, source, file_path, line_start, store, stats)
146 elif child.type == "import_spec_list":
147 for spec in child.children:
148 if spec.type == "import_spec":
149 self._iource: bytes, file_path: ce).lstrip("*").strip()
150 urce files into thedren:
151 if chi if ource: bytes, file_path: name_node =) -> None:
152 _node = child.child_by_field_name("type")
153 if not name_node or not type_node:
154 continue
155 if type_node.type notNodeLabel.Import, {
156 ype"):
157 "namre.create_node(
158 = "struct_t else "interface"
159 } if type_node.typ NodeLab
--- a/navegador/ingestion/java.py
+++ b/navegador/ingestion/java.py
@@ -0,0 +1,219 @@
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
+file_patreturn Language(tsjava.language())
21
+ except ImportError as e:
22
+ raise ImportError("Install tree-sitter-java: pip install tree-sitter-java") from e
23
+
24
+
25
+def _node_text(node, source: bytes) -> str:
26
+ re:node.end_byte].decode("utf-8", errors="replace")
27
+
28
+
29
+def _javadoc(node, source: bytes) -> str:
30
+ """Return the Javadoc (/** ... */) comment preceding a node, if any."""
31
+ parent = node.parent
32
+ if not parent:
33
+ return ""
34
+ siblings = list(parent.children)
35
+ idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
36
+ if idx <= 0:
37
+ return ""
38
+ prev = siblings[idx - 1]
39
+ if prev.type == "block_comment":
40
+ raw = _node_text(prev, source).strip()
41
+ if raw.startswith("/**"):
42
+ return raw.strip("/**").strip("*/").strip()
43
+ return ""
44
+
45
+
46
+class JavaParser(LanguageParser):
47
+ """Parses Java source files into the navegador graph."""
48
+
49
+ def __init__(self) -> None:
50
+ from tree_sitter import Parser # type: ignore[import]
51
+ self._parser = Parser(_get_java_language())
52
+
53
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
54
+ source = path.read_bytes()
55
+ tree = self._parser.parse(source)
56
+ rel_path = str(path.relative_to(repo_root))
57
+NodeLabel.File, {
58
+"\n"),
59
+ }path.name,
60
+ path.name,
61
+ "pa"language": "java",
62
+ ": "java",
63
+ })
64
+on"):
65
+ },
66
+ )
67
+
68
+ stats = {"functions": 0, "classes": 0, "edges": 0}
69
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
70
+ return stats
71
+
72
+ # ── AST walker ───────────────────────────────────────────────�self, node, soua",
73
+ _javadoc(node, sour store.creat | Nonecomment preceding a node, if any."""
74
+ parent = node.parent
75
+ if not parent:
76
+ return ""
77
+ siblings = list(parent.children)
78
+ idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
79
+ if idx <= 0:
80
+ return ""
81
+ prev = siblings[idx - 1]
82
+ if prev.type == "block_comment":
83
+ raw = _node_text(prev, source).strip()
84
+ if raw.startswith("/**"):
85
+ return raw.strip("/**").strip("*/").strip()
86
+ return ""
87
+
88
+
89
+class JavaParser(LanguageParser):
90
+ """Parses Java source files into the navegador graph."""
91
+
92
+ def __init__(self) -> None:
93
+ from tree_sitter import Parser # type: ignore[import]
94
+
95
+ self._parser = Parser(_get_java_language())
96
+
97
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[self, node, sou {"name": pare store.create_edge(
98
+ comment preceding a str(path.relative_to(repo_root))
99
+
100
+ store.create_node(
101
+ NodeLabel.File,
102
+ {
103
+ "name": path.name,
104
+ "path": rel_path,
105
+ "language": "javNodeLabel.Class, {
106
+"\n"),
107
+ {"name": parent_n,
108
+ "line_start": node.startclaration",
109
+ "docstring": docstring,
110
+ } inner_name_node = child.cNodeLabel.File,
111
+""
112
+Java AST parser —ser — extractsSuperclass → INHERITS edge
113
+ superclass = node.child_by_field_name("superclass")
114
+ if superclass:
115
+ for child in superclass.children:
116
+ if child.type == "type_identifier":
117
+ parent_name = _node_text(child, source)
118
+ store.create_edge(
119
+ NodeLabel.Class,node_text(inner_name_nfile_path": fileEdgeType.INHERITS,
120
+ {"name": parent_name, "file_path": file_path},
121
+ )
122
+ stats["edges"] += 1
123
+ break
124
+
125
+ # Walk class body for methods and constructors
126
+ body = node.child_by_field_name("body")
127
+ if body:
128
+ for child in body.children:
129
+ if child.type in ("method_declaration", "constructor_declaration"):
130
+ self._handle_method(child, source, file_path, store, stats,
131
+ "docstring": file_path": fileelif child comment preceding a nod,
132
+ cord_declaration",
133
+ ):
134
+ # Nested class — register but don't recurse into methods
135
+ inner_name_node = child.child_by_field_name("name")
136
+ if inner_name_node:
137
+ inner_name = _node_text(inner_name_node, source)
138
+ store.create_node(NodeLabel.Class, elif child.t"\n"),
139
+ }inner_,
140
+ )
141
+
142
+ s""
143
+Java er — extracts c "line_ance, and call edges from .java fil"""
144
+Java AST parser — extracts classes, interfaces, methods, c"docstring": "}) {"name": parentname_node = child.child_by_fielfile_path": fileNodeLabel.Class,node_text(inner_name_n EdgeType.CONTAINS,
145
+ file_path": fileNodeLabel.Class, {"name": inner_name,e in (
146
+ },
147
+ ) {"name": parentstore.create_node(
148
+ file_path": filestats["edges"] += 1
149
+
150
+ breakself, node, sou
151
+ store.create_edge(
152
+ comment preceding a str(path.relative_to(repo_root))
153
+
154
+ store.create_node(
155
+ NodeLabel.File,
156
+ {
157
+ "name": path.name,
158
+ "path": rel_path,
159
+ "language": "javNodeLabel.Class, {
160
+"\n"),
161
+ {"name": parent_n,
162
+ "line_start": node.startclaration",
163
+ # Nested class — register but d} inner_name_node = child.cNodeLabel.File,
164
+""
165
+Java AST parser —ser — extracts
166
+ "docstringself, node, sou {"name": paren store.creatcomment preceding a str(path.relative_to(repo_root))
167
+
168
+ store.create_node(
169
+ NodeLabel.File,
170
+ {
171
+ "name": path.name,
172
+ "path": rel_path,
173
+ "language": "javes using tree-sit""""\n"),
174
+ {"name": parent_n,
175
+ "line_start": node.startclaration",
176
+ "class_name": class_name,
177
+
178
+ } inner_name_node = chClass,odeLabel.Class,
179
+ {"name": class_name, "file_path": file_path},
180
+ node_text(inner_name_node, source)
181
+ store.cfunctions"] += 1
182
+ stats["edges"] += 1
183
+
184
+ self._extract_calls(node, source, file_path, name, store, statself, node, sou {"name": paren store.create_edge(
185
+ comment preceding aict
186
+ ) -> None:
187
+ # import java.util.List; → strip keyword + semicolon
188
+ (raw.removeprefix("imporint[0] + 1,
189
+ .removesuffix(";").strip() inner_name_node = cnode(NodeLabel.Import, {
190
+"\n"),
191
+ }module,""
192
+Java Aser — extradges": 0}
193
+ self._"module": module,
194
+
195
+ } inner_name_node = child.cNodeLabel.File,
196
+ {"path": file_path},
197
+ EdgeType.IMPORTS,
198
+ {"name": module, "file_path": file_path},
199
+ )
200
+ stats["edges"] +=self, method_ {"name": parenmethod_name: str, store.create_edge(
201
+ comment preceding act,
202
+ ) -> None:
203
+ def walk(node):
204
+ if node.type == "method_invocation":
205
+ name_node = node.child_by_field_name("name")
206
+ if name_node:
207
+ callee = _node_text(name_node, source)
208
+ store.od,
209
+ {"name": method_name, "file_path": file_path},
210
+ EdgeType.CALLS,
211
+ {"name": callee, "file_path": file_path},
212
+ )
213
+ stats["edges"] += 1
214
+ for child in node.children:
215
+ walk(child)
216
+
217
+ body = method_node.child_by_field_name("body")
218
+ if body:
219
+ walk(body)
--- a/navegador/ingestion/java.py
+++ b/navegador/ingestion/java.py
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/java.py
+++ b/navegador/ingestion/java.py
@@ -0,0 +1,219 @@
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 file_patreturn Language(tsjava.language())
21 except ImportError as e:
22 raise ImportError("Install tree-sitter-java: pip install tree-sitter-java") from e
23
24
25 def _node_text(node, source: bytes) -> str:
26 re:node.end_byte].decode("utf-8", errors="replace")
27
28
29 def _javadoc(node, source: bytes) -> str:
30 """Return the Javadoc (/** ... */) comment preceding a node, if any."""
31 parent = node.parent
32 if not parent:
33 return ""
34 siblings = list(parent.children)
35 idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
36 if idx <= 0:
37 return ""
38 prev = siblings[idx - 1]
39 if prev.type == "block_comment":
40 raw = _node_text(prev, source).strip()
41 if raw.startswith("/**"):
42 return raw.strip("/**").strip("*/").strip()
43 return ""
44
45
46 class JavaParser(LanguageParser):
47 """Parses Java source files into the navegador graph."""
48
49 def __init__(self) -> None:
50 from tree_sitter import Parser # type: ignore[import]
51 self._parser = Parser(_get_java_language())
52
53 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
54 source = path.read_bytes()
55 tree = self._parser.parse(source)
56 rel_path = str(path.relative_to(repo_root))
57 NodeLabel.File, {
58 "\n"),
59 }path.name,
60 path.name,
61 "pa"language": "java",
62 ": "java",
63 })
64 on"):
65 },
66 )
67
68 stats = {"functions": 0, "classes": 0, "edges": 0}
69 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
70 return stats
71
72 # ── AST walker ───────────────────────────────────────────────�self, node, soua",
73 _javadoc(node, sour store.creat | Nonecomment preceding a node, if any."""
74 parent = node.parent
75 if not parent:
76 return ""
77 siblings = list(parent.children)
78 idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
79 if idx <= 0:
80 return ""
81 prev = siblings[idx - 1]
82 if prev.type == "block_comment":
83 raw = _node_text(prev, source).strip()
84 if raw.startswith("/**"):
85 return raw.strip("/**").strip("*/").strip()
86 return ""
87
88
89 class JavaParser(LanguageParser):
90 """Parses Java source files into the navegador graph."""
91
92 def __init__(self) -> None:
93 from tree_sitter import Parser # type: ignore[import]
94
95 self._parser = Parser(_get_java_language())
96
97 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[self, node, sou {"name": pare store.create_edge(
98 comment preceding a str(path.relative_to(repo_root))
99
100 store.create_node(
101 NodeLabel.File,
102 {
103 "name": path.name,
104 "path": rel_path,
105 "language": "javNodeLabel.Class, {
106 "\n"),
107 {"name": parent_n,
108 "line_start": node.startclaration",
109 "docstring": docstring,
110 } inner_name_node = child.cNodeLabel.File,
111 ""
112 Java AST parser —ser — extractsSuperclass → INHERITS edge
113 superclass = node.child_by_field_name("superclass")
114 if superclass:
115 for child in superclass.children:
116 if child.type == "type_identifier":
117 parent_name = _node_text(child, source)
118 store.create_edge(
119 NodeLabel.Class,node_text(inner_name_nfile_path": fileEdgeType.INHERITS,
120 {"name": parent_name, "file_path": file_path},
121 )
122 stats["edges"] += 1
123 break
124
125 # Walk class body for methods and constructors
126 body = node.child_by_field_name("body")
127 if body:
128 for child in body.children:
129 if child.type in ("method_declaration", "constructor_declaration"):
130 self._handle_method(child, source, file_path, store, stats,
131 "docstring": file_path": fileelif child comment preceding a nod,
132 cord_declaration",
133 ):
134 # Nested class — register but don't recurse into methods
135 inner_name_node = child.child_by_field_name("name")
136 if inner_name_node:
137 inner_name = _node_text(inner_name_node, source)
138 store.create_node(NodeLabel.Class, elif child.t"\n"),
139 }inner_,
140 )
141
142 s""
143 Java er — extracts c "line_ance, and call edges from .java fil"""
144 Java AST parser — extracts classes, interfaces, methods, c"docstring": "}) {"name": parentname_node = child.child_by_fielfile_path": fileNodeLabel.Class,node_text(inner_name_n EdgeType.CONTAINS,
145 file_path": fileNodeLabel.Class, {"name": inner_name,e in (
146 },
147 ) {"name": parentstore.create_node(
148 file_path": filestats["edges"] += 1
149
150 breakself, node, sou
151 store.create_edge(
152 comment preceding a str(path.relative_to(repo_root))
153
154 store.create_node(
155 NodeLabel.File,
156 {
157 "name": path.name,
158 "path": rel_path,
159 "language": "javNodeLabel.Class, {
160 "\n"),
161 {"name": parent_n,
162 "line_start": node.startclaration",
163 # Nested class — register but d} inner_name_node = child.cNodeLabel.File,
164 ""
165 Java AST parser —ser — extracts
166 "docstringself, node, sou {"name": paren store.creatcomment preceding a str(path.relative_to(repo_root))
167
168 store.create_node(
169 NodeLabel.File,
170 {
171 "name": path.name,
172 "path": rel_path,
173 "language": "javes using tree-sit""""\n"),
174 {"name": parent_n,
175 "line_start": node.startclaration",
176 "class_name": class_name,
177
178 } inner_name_node = chClass,odeLabel.Class,
179 {"name": class_name, "file_path": file_path},
180 node_text(inner_name_node, source)
181 store.cfunctions"] += 1
182 stats["edges"] += 1
183
184 self._extract_calls(node, source, file_path, name, store, statself, node, sou {"name": paren store.create_edge(
185 comment preceding aict
186 ) -> None:
187 # import java.util.List; → strip keyword + semicolon
188 (raw.removeprefix("imporint[0] + 1,
189 .removesuffix(";").strip() inner_name_node = cnode(NodeLabel.Import, {
190 "\n"),
191 }module,""
192 Java Aser — extradges": 0}
193 self._"module": module,
194
195 } inner_name_node = child.cNodeLabel.File,
196 {"path": file_path},
197 EdgeType.IMPORTS,
198 {"name": module, "file_path": file_path},
199 )
200 stats["edges"] +=self, method_ {"name": parenmethod_name: str, store.create_edge(
201 comment preceding act,
202 ) -> None:
203 def walk(node):
204 if node.type == "method_invocation":
205 name_node = node.child_by_field_name("name")
206 if name_node:
207 callee = _node_text(name_node, source)
208 store.od,
209 {"name": method_name, "file_path": file_path},
210 EdgeType.CALLS,
211 {"name": callee, "file_path": file_path},
212 )
213 stats["edges"] += 1
214 for child in node.children:
215 walk(child)
216
217 body = method_node.child_by_field_name("body")
218 if body:
219 walk(body)
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -1,8 +1,16 @@
11
"""
22
RepoIngester — walks a repository, parses source files with tree-sitter,
33
and writes nodes + edges into the GraphStore.
4
+
5
+Supported languages (all via tree-sitter):
6
+ Python .py
7
+ TypeScript .ts .tsx
8
+ JavaScript .js .jsx
9
+ Go .go
10
+ Rust .rs
11
+ Java .java
412
"""
513
614
import logging
715
from pathlib import Path
816
@@ -11,15 +19,18 @@
1119
1220
logger = logging.getLogger(__name__)
1321
1422
# File extensions → language key
1523
LANGUAGE_MAP: dict[str, str] = {
16
- ".py": "python",
17
- ".ts": "typescript",
18
- ".tsx": "typescript",
19
- ".js": "javascript",
20
- ".jsx": "javascript",
24
+ ".py": "python",
25
+ ".ts": "typescript",
26
+ ".tsx": "typescript",
27
+ ".js": "javascript",
28
+ ".jsx": "javascript",
29
+ ".go": "go",
30
+ ".rs": "rust",
31
+ ".java": "java",
2132
}
2233
2334
2435
class RepoIngester:
2536
"""
@@ -81,11 +92,15 @@
8192
)
8293
return stats
8394
8495
def _iter_source_files(self, repo_path: Path):
8596
skip_dirs = {
86
- ".git", ".venv", "venv", "node_modules", "__pycache__", "dist", "build", ".next"
97
+ ".git", ".venv", "venv", "node_modules", "__pycache__",
98
+ "dist", "build", ".next",
99
+ "target", # Rust / Java (Maven/Gradle)
100
+ "vendor", # Go modules cache
101
+ ".gradle", # Gradle cache
87102
}
88103
for path in repo_path.rglob("*"):
89104
if path.is_file() and path.suffix in LANGUAGE_MAP:
90105
if not any(part in skip_dirs for part in path.parts):
91106
yield path
@@ -96,10 +111,19 @@
96111
from navegador.ingestion.python import PythonParser
97112
self._parsers[language] = PythonParser()
98113
elif language in ("typescript", "javascript"):
99114
from navegador.ingestion.typescript import TypeScriptParser
100115
self._parsers[language] = TypeScriptParser(language)
116
+ elif language == "go":
117
+ from navegador.ingestion.go import GoParser
118
+ self._parsers[language] = GoParser()
119
+ elif language == "rust":
120
+ from navegador.ingestion.rust import RustParser
121
+ self._parsers[language] = RustParser()
122
+ elif language == "java":
123
+ from navegador.ingestion.java import JavaParser
124
+ self._parsers[language] = JavaParser()
101125
else:
102126
raise ValueError(f"Unsupported language: {language}")
103127
return self._parsers[language]
104128
105129
106130
107131
ADDED navegador/ingestion/rust.py
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -1,8 +1,16 @@
1 """
2 RepoIngester — walks a repository, parses source files with tree-sitter,
3 and writes nodes + edges into the GraphStore.
 
 
 
 
 
 
 
 
4 """
5
6 import logging
7 from pathlib import Path
8
@@ -11,15 +19,18 @@
11
12 logger = logging.getLogger(__name__)
13
14 # File extensions → language key
15 LANGUAGE_MAP: dict[str, str] = {
16 ".py": "python",
17 ".ts": "typescript",
18 ".tsx": "typescript",
19 ".js": "javascript",
20 ".jsx": "javascript",
 
 
 
21 }
22
23
24 class RepoIngester:
25 """
@@ -81,11 +92,15 @@
81 )
82 return stats
83
84 def _iter_source_files(self, repo_path: Path):
85 skip_dirs = {
86 ".git", ".venv", "venv", "node_modules", "__pycache__", "dist", "build", ".next"
 
 
 
 
87 }
88 for path in repo_path.rglob("*"):
89 if path.is_file() and path.suffix in LANGUAGE_MAP:
90 if not any(part in skip_dirs for part in path.parts):
91 yield path
@@ -96,10 +111,19 @@
96 from navegador.ingestion.python import PythonParser
97 self._parsers[language] = PythonParser()
98 elif language in ("typescript", "javascript"):
99 from navegador.ingestion.typescript import TypeScriptParser
100 self._parsers[language] = TypeScriptParser(language)
 
 
 
 
 
 
 
 
 
101 else:
102 raise ValueError(f"Unsupported language: {language}")
103 return self._parsers[language]
104
105
106
107 DDED navegador/ingestion/rust.py
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -1,8 +1,16 @@
1 """
2 RepoIngester — walks a repository, parses source files with tree-sitter,
3 and writes nodes + edges into the GraphStore.
4
5 Supported languages (all via tree-sitter):
6 Python .py
7 TypeScript .ts .tsx
8 JavaScript .js .jsx
9 Go .go
10 Rust .rs
11 Java .java
12 """
13
14 import logging
15 from pathlib import Path
16
@@ -11,15 +19,18 @@
19
20 logger = logging.getLogger(__name__)
21
22 # File extensions → language key
23 LANGUAGE_MAP: dict[str, str] = {
24 ".py": "python",
25 ".ts": "typescript",
26 ".tsx": "typescript",
27 ".js": "javascript",
28 ".jsx": "javascript",
29 ".go": "go",
30 ".rs": "rust",
31 ".java": "java",
32 }
33
34
35 class RepoIngester:
36 """
@@ -81,11 +92,15 @@
92 )
93 return stats
94
95 def _iter_source_files(self, repo_path: Path):
96 skip_dirs = {
97 ".git", ".venv", "venv", "node_modules", "__pycache__",
98 "dist", "build", ".next",
99 "target", # Rust / Java (Maven/Gradle)
100 "vendor", # Go modules cache
101 ".gradle", # Gradle cache
102 }
103 for path in repo_path.rglob("*"):
104 if path.is_file() and path.suffix in LANGUAGE_MAP:
105 if not any(part in skip_dirs for part in path.parts):
106 yield path
@@ -96,10 +111,19 @@
111 from navegador.ingestion.python import PythonParser
112 self._parsers[language] = PythonParser()
113 elif language in ("typescript", "javascript"):
114 from navegador.ingestion.typescript import TypeScriptParser
115 self._parsers[language] = TypeScriptParser(language)
116 elif language == "go":
117 from navegador.ingestion.go import GoParser
118 self._parsers[language] = GoParser()
119 elif language == "rust":
120 from navegador.ingestion.rust import RustParser
121 self._parsers[language] = RustParser()
122 elif language == "java":
123 from navegador.ingestion.java import JavaParser
124 self._parsers[language] = JavaParser()
125 else:
126 raise ValueError(f"Unsupported language: {language}")
127 return self._parsers[language]
128
129
130
131 DDED navegador/ingestion/rust.py
--- a/navegador/ingestion/rust.py
+++ b/navegador/ingestion/rust.py
@@ -0,0 +1,112 @@
1
+"""
2
+Rust AST parser — extracts functions, methods (from impl blocks),
3
+structs/enums/traits, use declarations, and call edges from .rs files.
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_rust_language():
17
+ try:
18
+ import tree_sitter_rust as tsrust # type: ignore[import]
19
+ from tree_sitter import Language
20
+ return Language(tsrust.language())
21
+ except ImportError as e:
22
+ raise ImportError("Install tree-sitter-rust: pip install tree-sitter-rust") from e
23
+
24
+
25
+def _node_text(node, source: bytes) -> str:
26
+ re:node.end_byte].decode("utf-8", errors="replace")
27
+
28
+
29
+def _doc_comment(node, source: bytes) -> str:
30
+ """Collect preceding /// doc-comment lines from siblings."""
31
+ parent = node.parent
32
+ if not parent:
33
+ return ""
34
+ siblings = list(parent.children)
35
+ idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
36
+ if idx <= 0:
37
+ = []
38
+ for i in range(idx - 1, -1, -1):
39
+ sib = siblings[i]
40
+ raw = _node_text(sib, source)
41
+ if sib.type == "line_comment" and raw.startswith("///"):
42
+ lines.insert(0, raw.lstrip("/").strip())
43
+ else:
44
+ break
45
+ return " ".join(lines)
46
+
47
+
48
+class RustParser(LanguageParser):
49
+ """Parses Rust source files into the navegador graph."""
50
+
51
+ def __init__(self) -> None:
52
+ from tree_sitter import Parser # type: ignore[import]
53
+r # type: ignore[import]
54
+
55
+ self._parser = Parser(_get_rust_language())
56
+
57
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
58
+ source = path.read_bytes()
59
+ tree = self._parser.parse(source)
60
+ rel_path = str(paNodeLabel.File, {
61
+ "line_e"name": path.name,
62
+ "path": reluse declarations, a"""
63
+Rust AST parser — extracts": "rust",
64
+ }"),
65
+ },
66
+ )
67
+
68
+ stats = {"functions": 0, "classes": 0, "edges": 0}
69
+ self._walk(tree.root_node, source, rel_path, store, stats, impl_type=None)
70
+ return stats
71
+
72
+ # ── AST walker ────────────────────────────────────────────────────────� def _handle_im
73
+ store: GraphStore, stats: dict, impl_type: str | None) -> None:
74
+ nt:
75
+ return ""
76
+ siblings = list(parent.children)
77
+ idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
78
+ if idx <= 0:
79
+ return ""
80
+ lines: list[str] = []
81
+ for i in range(idx - 1, -1, -1):
82
+ sib = siblings[i]
83
+ raw = _node_text(sib, source)
84
+ if sib.type == "line_comment" and raw.startswith("///"):
85
+ lines.insert(0, raw.lstrip("/").strip())
86
+ else:
87
+ break
88
+ return " ".join(lines)
89
+
90
+
91
+class RustParser(LanguageParser):
92
+ """Parses Rust source files into the navegador graph."""
93
+
94
+ def __init__(self) -> None:
95
+ from tree_sitter import Parser # type: ignore[import]
96
+
97
+ self._parser = Parser(_get_rust_language())
98
+
99
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
100
+ source = path.read_bytes()
101
+ tree = self def _handle_imfor child in body store: GraphStore, stats: dict""
102
+Rust AST parser —"""
103
+Rust AST parser — ex str | NoneJ@OT,4N@wN,9:label, {
104
+B@1~B,R@1UF,M@1aG,2:,
105
+7@eU,t@1he,k@1WD,_@13G,c@13t,1:}2V@14j,9@15w,3: 1d@17D,11@191,12@1a0,2K@1BG,f@1Di,H@1Il,4: V@1NE,J@OT,4~@1FE,5:childY@gk,H@1Il,O@1z0,1: X@1LD,O:)
106
+
107
+ def _handle_type(f@1Di,H@1Il,4: V@1NE,J@OT,2F@wN,q@1QK,H@1Il,R@1RA,E:node.type, "")9@FG,c@yb,M@1YW,O:node(NodeLabel.Class, {
108
+B@1~B,R@1UF,M@1aG,2:,
109
+7@eU,t@1he,k@1WD,1G@1X0,1:}t@1YV,K@17V,i@19F,G:NodeLabel.Class,22@1a0,f@1Di,H@1Il,3: V@1NE,J@OT,1y@1dh,K:NodeLabel.Import, {
110
+d@1gQ,M@1aG,2:,
111
+7@eU,t@1he,I:"module": module,
112
+8@1hW,1:}t@1YV,K@17V,N@19F,b@1ky,1e@1lk,9:self, fn__@1Mf,H@1Il,7@8E,R:fn_name: str, fn_label: strM@1~0,2: V@1NE,J@OT,6D@1qR,1~@1x0,49@1zN,1P_dm9;
--- a/navegador/ingestion/rust.py
+++ b/navegador/ingestion/rust.py
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/rust.py
+++ b/navegador/ingestion/rust.py
@@ -0,0 +1,112 @@
1 """
2 Rust AST parser — extracts functions, methods (from impl blocks),
3 structs/enums/traits, use declarations, and call edges from .rs files.
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_rust_language():
17 try:
18 import tree_sitter_rust as tsrust # type: ignore[import]
19 from tree_sitter import Language
20 return Language(tsrust.language())
21 except ImportError as e:
22 raise ImportError("Install tree-sitter-rust: pip install tree-sitter-rust") from e
23
24
25 def _node_text(node, source: bytes) -> str:
26 re:node.end_byte].decode("utf-8", errors="replace")
27
28
29 def _doc_comment(node, source: bytes) -> str:
30 """Collect preceding /// doc-comment lines from siblings."""
31 parent = node.parent
32 if not parent:
33 return ""
34 siblings = list(parent.children)
35 idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
36 if idx <= 0:
37 = []
38 for i in range(idx - 1, -1, -1):
39 sib = siblings[i]
40 raw = _node_text(sib, source)
41 if sib.type == "line_comment" and raw.startswith("///"):
42 lines.insert(0, raw.lstrip("/").strip())
43 else:
44 break
45 return " ".join(lines)
46
47
48 class RustParser(LanguageParser):
49 """Parses Rust source files into the navegador graph."""
50
51 def __init__(self) -> None:
52 from tree_sitter import Parser # type: ignore[import]
53 r # type: ignore[import]
54
55 self._parser = Parser(_get_rust_language())
56
57 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
58 source = path.read_bytes()
59 tree = self._parser.parse(source)
60 rel_path = str(paNodeLabel.File, {
61 "line_e"name": path.name,
62 "path": reluse declarations, a"""
63 Rust AST parser — extracts": "rust",
64 }"),
65 },
66 )
67
68 stats = {"functions": 0, "classes": 0, "edges": 0}
69 self._walk(tree.root_node, source, rel_path, store, stats, impl_type=None)
70 return stats
71
72 # ── AST walker ────────────────────────────────────────────────────────� def _handle_im
73 store: GraphStore, stats: dict, impl_type: str | None) -> None:
74 nt:
75 return ""
76 siblings = list(parent.children)
77 idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
78 if idx <= 0:
79 return ""
80 lines: list[str] = []
81 for i in range(idx - 1, -1, -1):
82 sib = siblings[i]
83 raw = _node_text(sib, source)
84 if sib.type == "line_comment" and raw.startswith("///"):
85 lines.insert(0, raw.lstrip("/").strip())
86 else:
87 break
88 return " ".join(lines)
89
90
91 class RustParser(LanguageParser):
92 """Parses Rust source files into the navegador graph."""
93
94 def __init__(self) -> None:
95 from tree_sitter import Parser # type: ignore[import]
96
97 self._parser = Parser(_get_rust_language())
98
99 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
100 source = path.read_bytes()
101 tree = self def _handle_imfor child in body store: GraphStore, stats: dict""
102 Rust AST parser —"""
103 Rust AST parser — ex str | NoneJ@OT,4N@wN,9:label, {
104 B@1~B,R@1UF,M@1aG,2:,
105 7@eU,t@1he,k@1WD,_@13G,c@13t,1:}2V@14j,9@15w,3: 1d@17D,11@191,12@1a0,2K@1BG,f@1Di,H@1Il,4: V@1NE,J@OT,4~@1FE,5:childY@gk,H@1Il,O@1z0,1: X@1LD,O:)
106
107 def _handle_type(f@1Di,H@1Il,4: V@1NE,J@OT,2F@wN,q@1QK,H@1Il,R@1RA,E:node.type, "")9@FG,c@yb,M@1YW,O:node(NodeLabel.Class, {
108 B@1~B,R@1UF,M@1aG,2:,
109 7@eU,t@1he,k@1WD,1G@1X0,1:}t@1YV,K@17V,i@19F,G:NodeLabel.Class,22@1a0,f@1Di,H@1Il,3: V@1NE,J@OT,1y@1dh,K:NodeLabel.Import, {
110 d@1gQ,M@1aG,2:,
111 7@eU,t@1he,I:"module": module,
112 8@1hW,1:}t@1YV,K@17V,N@19F,b@1ky,1e@1lk,9:self, fn__@1Mf,H@1Il,7@8E,R:fn_name: str, fn_label: strM@1~0,2: V@1NE,J@OT,6D@1qR,1~@1x0,49@1zN,1P_dm9;
--- navegador/ingestion/typescript.py
+++ navegador/ingestion/typescript.py
@@ -1,8 +1,12 @@
11
"""
2
-TypeScript/JavaScript AST parser — placeholder for tree-sitter-typescript.
3
-Full implementation follows the same pattern as python.py.
2
+TypeScript/JavaScript AST parser — extracts classes, interfaces, functions
3
+(including const arrow functions), methods, imports, and call edges.
4
+
5
+Supports:
6
+ - .ts / .tsx → tree-sitter-typescript (TypeScript grammar)
7
+ - .js / .jsx → tree-sitter-javascript (JavaScript grammar)
48
"""
59
610
import logging
711
from pathlib import Path
812
@@ -26,12 +30,35 @@
2630
except ImportError as e:
2731
raise ImportError(
2832
f"Install tree-sitter-{language}: pip install tree-sitter-{language}"
2933
) from e
3034
35
+
36
+def _node_text(node, source: bytes) -> str:
37
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
38
+
39
+
40
+def _jsdoc(node, source: bytes) -> str:
41
+ """Return the JSDoc comment (/** ... */) preceding a node, if any."""
42
+ parent = node.parent
43
+ if not parent:
44
+ return ""
45
+ siblings = list(parent.children)
46
+ idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
47
+ if idx <= 0:
48
+ return ""
49
+ prev = siblings[idx - 1]
50
+ if prev.type == "comment":
51
+ raw = _node_text(prev, source).strip()
52
+ if raw.startswith("/**"):
53
+ return raw.strip("/**").strip("*/").strip()
54
+ return ""
55
+
3156
3257
class TypeScriptParser(LanguageParser):
58
+ """Parses TypeScript/JavaScript source files into the navegador graph."""
59
+
3360
def __init__(self, language: str = "typescript") -> None:
3461
from tree_sitter import Parser # type: ignore[import]
3562
self._parser = Parser(_get_ts_language(language))
3663
self._language = language
3764
@@ -49,33 +76,205 @@
4976
5077
stats = {"functions": 0, "classes": 0, "edges": 0}
5178
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
5279
return stats
5380
54
- def _walk(self, node, source: bytes, file_path: str, store: GraphStore,
55
- stats: dict, class_name: str | None) -> None:
81
+ # ── AST walker ────────────────────────────────────────────────────────────
82
+
83
+ def _walk(self, node, source: bytes, file_path: str,
84
+ store: GraphStore, stats: dict, class_name: str | None) -> None:
5685
if node.type in ("class_declaration", "abstract_class_declaration"):
5786
self._handle_class(node, source, file_path, store, stats)
5887
return
59
- if node.type in (
60
- "function_declaration", "arrow_function", "method_definition",
61
- "function_expression",
62
- ):
88
+ if node.type in ("interface_declaration", "type_alias_declaration"):
89
+ self._handle_interface(node, source, file_path, store, stats)
90
+ return
91
+ if node.type == "function_declaration":
92
+ self._handle_function(node, source, file_path, store, stats, class_name)
93
+ return
94
+ if node.type == "method_definition":
6395
self._handle_function(node, source, file_path, store, stats, class_name)
96
+ return
97
+ if node.type in ("lexical_declaration", "variable_declaration"):
98
+ self._handle_lexical(node, source, file_path, store, stats)
6499
return
65100
if node.type in ("import_statement", "import_declaration"):
66101
self._handle_import(node, source, file_path, store, stats)
102
+ return
103
+ if node.type == "export_statement":
104
+ # Recurse into exported declarations
105
+ for child in node.children:
106
+ if child.type not in ("export", "default", "from"):
107
+ self._walk(child, source, file_path, store, stats, class_name)
108
+ return
67109
68110
for child in node.children:
69111
self._walk(child, source, file_path, store, stats, class_name)
70112
113
+ # ── Handlers ──────────────────────────────────────────────────────────────
114
+
115
+ def _handle_class(self, node, source: bytes, file_path: str,
116
+ store: GraphStore, stats: dict) -> None:
117
+ name_node = next(
118
+ (c for c in node.children if c.type == "type_identifier"), None
119
+ )
120
+ if not name_node:
121
+ return
122
+ name = _node_text(name_node, source)
123
+ docstring = _jsdoc(node, source)
124
+
125
+ store.create_node(NodeLabel.Class, {
126
+ "name": name,
127
+ "file_path": file_path,
128
+ "line_start": node.start_point[0] + 1,
129
+ "line_end": node.end_point[0] + 1,
130
+ "docstring": docstring,
131
+ })
132
+ store.create_edge(
133
+ NodeLabel.File, {"path": file_path},
134
+ EdgeType.CONTAINS,
135
+ NodeLabel.Class, {"name": name, "file_path": file_path},
136
+ )
137
+ stats["classes"] += 1
138
+ stats["edges"] += 1
139
+
140
+ # Inheritance: extends clause
141
+ heritage = next((c for c in node.children if c.type == "class_heritage"), None)
142
+ if heritage:
143
+ for child in heritage.children:
144
+ if child.type == "extends_clause":
145
+ for c in child.children:
146
+ if c.type == "identifier":
147
+ parent_name = _node_text(c, source)
148
+ store.create_edge(
149
+ NodeLabel.Class, {"name": name, "file_path": file_path},
150
+ EdgeType.INHERITS,
151
+ NodeLabel.Class, {"name": parent_name, "file_path": file_path},
152
+ )
153
+ stats["edges"] += 1
154
+
155
+ body = next((c for c in node.children if c.type == "class_body"), None)
156
+ if body:
157
+ for child in body.children:
158
+ if child.type == "method_definition":
159
+ self._handle_function(child, source, file_path, store, stats,
160
+ class_name=name)
161
+
162
+ def _handle_interface(self, node, source: bytes, file_path: str,
163
+ store: GraphStore, stats: dict) -> None:
164
+ name_node = next(
165
+ (c for c in node.children if c.type == "type_identifier"), None
166
+ )
167
+ if not name_node:
168
+ return
169
+ name = _node_text(name_node, source)
170
+ docstring = _jsdoc(node, source)
171
+ kind = "interface" if node.type == "interface_declaration" else "type"
172
+
173
+ store.create_node(NodeLabel.Class, {
174
+ "name": name,
175
+ "file_path": file_path,
176
+ "line_start": node.start_point[0] + 1,
177
+ "line_end": node.end_point[0] + 1,
178
+ "docstring": f"{kind}: {docstring}".strip(": "),
179
+ })
180
+ store.create_edge(
181
+ NodeLabel.File, {"path": file_path},
182
+ EdgeType.CONTAINS,
183
+ NodeLabel.Class, {"name": name, "file_path": file_path},
184
+ )
185
+ stats["classes"] += 1
186
+ stats["edges"] += 1
187
+
188
+ def _handle_function(self, node, source: bytes, file_path: str,
189
+ store: GraphStore, stats: dict,
190
+ class_name: str | None) -> None:
191
+ name_node = next(
192
+ (c for c in node.children
193
+ if c.type in ("identifier", "property_identifier")), None
194
+ )
195
+ if not name_node:
196
+ return
197
+ name = _node_text(name_node, source)
198
+ if name in ("constructor", "get", "set", "static", "async"):
199
+ # These are keywords, not useful names — look for next identifier
200
+ name_node = next(
201
+ (c for c in node.children
202
+ if c.type in ("identifier", "property_identifier") and c != name_node),
203
+ None,
204
+ )
205
+ if not name_node:
206
+ return
207
+ name = _node_text(name_node, source)
208
+
209
+ docstring = _jsdoc(node, source)
210
+ label = NodeLabel.Method if class_name else NodeLabel.Function
211
+
212
+ store.create_node(label, {
213
+ "name": name,
214
+ "file_path": file_path,
215
+ "line_start": node.start_point[0] + 1,
216
+ "line_end": node.end_point[0] + 1,
217
+ "docstring": docstring,
218
+ "class_name": class_name or "",
219
+ })
220
+
221
+ container_label = NodeLabel.Class if class_name else NodeLabel.File
222
+ container_key = (
223
+ {"name": class_name, "file_path": file_path}
224
+ if class_name else {"path": file_path}
225
+ )
226
+ store.create_edge(
227
+ container_label, container_key,
228
+ EdgeType.CONTAINS,
229
+ label, {"name": name, "file_path": file_path},
230
+ )
231
+ stats["functions"] += 1
232
+ stats["edges"] += 1
233
+
234
+ self._extract_calls(node, source, file_path, name, label, store, stats)
235
+
236
+ def _handle_lexical(self, node, source: bytes, file_path: str,
237
+ store: GraphStore, stats: dict) -> None:
238
+ """Handle: const foo = () => {} and const bar = function() {}"""
239
+ for child in node.children:
240
+ if child.type != "variable_declarator":
241
+ continue
242
+ name_node = child.child_by_field_name("name")
243
+ value_node = child.child_by_field_name("value")
244
+ if not name_node or not value_node:
245
+ continue
246
+ if value_node.type not in ("arrow_function", "function_expression",
247
+ "function"):
248
+ continue
249
+ name = _node_text(name_node, source)
250
+ docstring = _jsdoc(node, source)
251
+
252
+ store.create_node(NodeLabel.Function, {
253
+ "name": name,
254
+ "file_path": file_path,
255
+ "line_start": node.start_point[0] + 1,
256
+ "line_end": node.end_point[0] + 1,
257
+ "docstring": docstring,
258
+ "class_name": "",
259
+ })
260
+ store.create_edge(
261
+ NodeLabel.File, {"path": file_path},
262
+ EdgeType.CONTAINS,
263
+ NodeLabel.Function, {"name": name, "file_path": file_path},
264
+ )
265
+ stats["functions"] += 1
266
+ stats["edges"] += 1
267
+
268
+ self._extract_calls(value_node, source, file_path, name,
269
+ NodeLabel.Function, store, stats)
270
+
71271
def _handle_import(self, node, source: bytes, file_path: str,
72272
store: GraphStore, stats: dict) -> None:
73
- # Extract "from '...'" module path
74273
for child in node.children:
75274
if child.type == "string":
76
- module = source[child.start_byte:child.end_byte].decode().strip("'\"")
275
+ module = _node_text(child, source).strip("'\"")
77276
store.create_node(NodeLabel.Import, {
78277
"name": module,
79278
"file_path": file_path,
80279
"line_start": node.start_point[0] + 1,
81280
"module": module,
@@ -86,64 +285,31 @@
86285
NodeLabel.Import, {"name": module, "file_path": file_path},
87286
)
88287
stats["edges"] += 1
89288
break
90289
91
- def _handle_class(self, node, source: bytes, file_path: str,
92
- store: GraphStore, stats: dict) -> None:
93
- name_node = next((c for c in node.children if c.type == "type_identifier"), None)
94
- if not name_node:
95
- return
96
- name = source[name_node.start_byte:name_node.end_byte].decode()
97
-
98
- store.create_node(NodeLabel.Class, {
99
- "name": name,
100
- "file_path": file_path,
101
- "line_start": node.start_point[0] + 1,
102
- "line_end": node.end_point[0] + 1,
103
- "docstring": "",
104
- })
105
- store.create_edge(
106
- NodeLabel.File, {"path": file_path},
107
- EdgeType.CONTAINS,
108
- NodeLabel.Class, {"name": name, "file_path": file_path},
109
- )
110
- stats["classes"] += 1
111
- stats["edges"] += 1
112
-
113
- body = next((c for c in node.children if c.type == "class_body"), None)
290
+ def _extract_calls(self, fn_node, source: bytes, file_path: str,
291
+ fn_name: str, fn_label: str,
292
+ store: GraphStore, stats: dict) -> None:
293
+ def walk(node):
294
+ if node.type == "call_expression":
295
+ func = node.child_by_field_name("function")
296
+ if func:
297
+ text = _node_text(func, source)
298
+ callee = text.split(".")[-1]
299
+ store.create_edge(
300
+ fn_label, {"name": fn_name, "file_path": file_path},
301
+ EdgeType.CALLS,
302
+ NodeLabel.Function, {"name": callee, "file_path": file_path},
303
+ )
304
+ stats["edges"] += 1
305
+ for child in node.children:
306
+ walk(child)
307
+
308
+ # Body is statement_block for functions, expression for arrow fns
309
+ body = next(
310
+ (c for c in fn_node.children
311
+ if c.type in ("statement_block", "expression_statement")),
312
+ None,
313
+ )
114314
if body:
115
- for child in body.children:
116
- if child.type == "method_definition":
117
- self._handle_function(child, source, file_path, store, stats, class_name=name)
118
-
119
- def _handle_function(self, node, source: bytes, file_path: str,
120
- store: GraphStore, stats: dict, class_name: str | None) -> None:
121
- name_node = next(
122
- (c for c in node.children if c.type in ("identifier", "property_identifier")), None
123
- )
124
- if not name_node:
125
- return
126
- name = source[name_node.start_byte:name_node.end_byte].decode()
127
-
128
- label = NodeLabel.Method if class_name else NodeLabel.Function
129
- store.create_node(label, {
130
- "name": name,
131
- "file_path": file_path,
132
- "line_start": node.start_point[0] + 1,
133
- "line_end": node.end_point[0] + 1,
134
- "docstring": "",
135
- "class_name": class_name or "",
136
- })
137
-
138
- container_label = NodeLabel.Class if class_name else NodeLabel.File
139
- container_key = (
140
- {"name": class_name, "file_path": file_path}
141
- if class_name
142
- else {"path": file_path}
143
- )
144
- store.create_edge(
145
- container_label, container_key, EdgeType.CONTAINS, label,
146
- {"name": name, "file_path": file_path},
147
- )
148
- stats["functions"] += 1
149
- stats["edges"] += 1
315
+ walk(body)
150316
--- navegador/ingestion/typescript.py
+++ navegador/ingestion/typescript.py
@@ -1,8 +1,12 @@
1 """
2 TypeScript/JavaScript AST parser — placeholder for tree-sitter-typescript.
3 Full implementation follows the same pattern as python.py.
 
 
 
 
4 """
5
6 import logging
7 from pathlib import Path
8
@@ -26,12 +30,35 @@
26 except ImportError as e:
27 raise ImportError(
28 f"Install tree-sitter-{language}: pip install tree-sitter-{language}"
29 ) from e
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
32 class TypeScriptParser(LanguageParser):
 
 
33 def __init__(self, language: str = "typescript") -> None:
34 from tree_sitter import Parser # type: ignore[import]
35 self._parser = Parser(_get_ts_language(language))
36 self._language = language
37
@@ -49,33 +76,205 @@
49
50 stats = {"functions": 0, "classes": 0, "edges": 0}
51 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
52 return stats
53
54 def _walk(self, node, source: bytes, file_path: str, store: GraphStore,
55 stats: dict, class_name: str | None) -> None:
 
 
56 if node.type in ("class_declaration", "abstract_class_declaration"):
57 self._handle_class(node, source, file_path, store, stats)
58 return
59 if node.type in (
60 "function_declaration", "arrow_function", "method_definition",
61 "function_expression",
62 ):
 
 
 
63 self._handle_function(node, source, file_path, store, stats, class_name)
 
 
 
64 return
65 if node.type in ("import_statement", "import_declaration"):
66 self._handle_import(node, source, file_path, store, stats)
 
 
 
 
 
 
 
67
68 for child in node.children:
69 self._walk(child, source, file_path, store, stats, class_name)
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71 def _handle_import(self, node, source: bytes, file_path: str,
72 store: GraphStore, stats: dict) -> None:
73 # Extract "from '...'" module path
74 for child in node.children:
75 if child.type == "string":
76 module = source[child.start_byte:child.end_byte].decode().strip("'\"")
77 store.create_node(NodeLabel.Import, {
78 "name": module,
79 "file_path": file_path,
80 "line_start": node.start_point[0] + 1,
81 "module": module,
@@ -86,64 +285,31 @@
86 NodeLabel.Import, {"name": module, "file_path": file_path},
87 )
88 stats["edges"] += 1
89 break
90
91 def _handle_class(self, node, source: bytes, file_path: str,
92 store: GraphStore, stats: dict) -> None:
93 name_node = next((c for c in node.children if c.type == "type_identifier"), None)
94 if not name_node:
95 return
96 name = source[name_node.start_byte:name_node.end_byte].decode()
97
98 store.create_node(NodeLabel.Class, {
99 "name": name,
100 "file_path": file_path,
101 "line_start": node.start_point[0] + 1,
102 "line_end": node.end_point[0] + 1,
103 "docstring": "",
104 })
105 store.create_edge(
106 NodeLabel.File, {"path": file_path},
107 EdgeType.CONTAINS,
108 NodeLabel.Class, {"name": name, "file_path": file_path},
109 )
110 stats["classes"] += 1
111 stats["edges"] += 1
112
113 body = next((c for c in node.children if c.type == "class_body"), None)
 
114 if body:
115 for child in body.children:
116 if child.type == "method_definition":
117 self._handle_function(child, source, file_path, store, stats, class_name=name)
118
119 def _handle_function(self, node, source: bytes, file_path: str,
120 store: GraphStore, stats: dict, class_name: str | None) -> None:
121 name_node = next(
122 (c for c in node.children if c.type in ("identifier", "property_identifier")), None
123 )
124 if not name_node:
125 return
126 name = source[name_node.start_byte:name_node.end_byte].decode()
127
128 label = NodeLabel.Method if class_name else NodeLabel.Function
129 store.create_node(label, {
130 "name": name,
131 "file_path": file_path,
132 "line_start": node.start_point[0] + 1,
133 "line_end": node.end_point[0] + 1,
134 "docstring": "",
135 "class_name": class_name or "",
136 })
137
138 container_label = NodeLabel.Class if class_name else NodeLabel.File
139 container_key = (
140 {"name": class_name, "file_path": file_path}
141 if class_name
142 else {"path": file_path}
143 )
144 store.create_edge(
145 container_label, container_key, EdgeType.CONTAINS, label,
146 {"name": name, "file_path": file_path},
147 )
148 stats["functions"] += 1
149 stats["edges"] += 1
150
--- navegador/ingestion/typescript.py
+++ navegador/ingestion/typescript.py
@@ -1,8 +1,12 @@
1 """
2 TypeScript/JavaScript AST parser — extracts classes, interfaces, functions
3 (including const arrow functions), methods, imports, and call edges.
4
5 Supports:
6 - .ts / .tsx → tree-sitter-typescript (TypeScript grammar)
7 - .js / .jsx → tree-sitter-javascript (JavaScript grammar)
8 """
9
10 import logging
11 from pathlib import Path
12
@@ -26,12 +30,35 @@
30 except ImportError as e:
31 raise ImportError(
32 f"Install tree-sitter-{language}: pip install tree-sitter-{language}"
33 ) from e
34
35
36 def _node_text(node, source: bytes) -> str:
37 return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
38
39
40 def _jsdoc(node, source: bytes) -> str:
41 """Return the JSDoc comment (/** ... */) preceding a node, if any."""
42 parent = node.parent
43 if not parent:
44 return ""
45 siblings = list(parent.children)
46 idx = next((i for i, c in enumerate(siblings) if c.id == node.id), -1)
47 if idx <= 0:
48 return ""
49 prev = siblings[idx - 1]
50 if prev.type == "comment":
51 raw = _node_text(prev, source).strip()
52 if raw.startswith("/**"):
53 return raw.strip("/**").strip("*/").strip()
54 return ""
55
56
57 class TypeScriptParser(LanguageParser):
58 """Parses TypeScript/JavaScript source files into the navegador graph."""
59
60 def __init__(self, language: str = "typescript") -> None:
61 from tree_sitter import Parser # type: ignore[import]
62 self._parser = Parser(_get_ts_language(language))
63 self._language = language
64
@@ -49,33 +76,205 @@
76
77 stats = {"functions": 0, "classes": 0, "edges": 0}
78 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
79 return stats
80
81 # ── AST walker ────────────────────────────────────────────────────────────
82
83 def _walk(self, node, source: bytes, file_path: str,
84 store: GraphStore, stats: dict, class_name: str | None) -> None:
85 if node.type in ("class_declaration", "abstract_class_declaration"):
86 self._handle_class(node, source, file_path, store, stats)
87 return
88 if node.type in ("interface_declaration", "type_alias_declaration"):
89 self._handle_interface(node, source, file_path, store, stats)
90 return
91 if node.type == "function_declaration":
92 self._handle_function(node, source, file_path, store, stats, class_name)
93 return
94 if node.type == "method_definition":
95 self._handle_function(node, source, file_path, store, stats, class_name)
96 return
97 if node.type in ("lexical_declaration", "variable_declaration"):
98 self._handle_lexical(node, source, file_path, store, stats)
99 return
100 if node.type in ("import_statement", "import_declaration"):
101 self._handle_import(node, source, file_path, store, stats)
102 return
103 if node.type == "export_statement":
104 # Recurse into exported declarations
105 for child in node.children:
106 if child.type not in ("export", "default", "from"):
107 self._walk(child, source, file_path, store, stats, class_name)
108 return
109
110 for child in node.children:
111 self._walk(child, source, file_path, store, stats, class_name)
112
113 # ── Handlers ──────────────────────────────────────────────────────────────
114
115 def _handle_class(self, node, source: bytes, file_path: str,
116 store: GraphStore, stats: dict) -> None:
117 name_node = next(
118 (c for c in node.children if c.type == "type_identifier"), None
119 )
120 if not name_node:
121 return
122 name = _node_text(name_node, source)
123 docstring = _jsdoc(node, source)
124
125 store.create_node(NodeLabel.Class, {
126 "name": name,
127 "file_path": file_path,
128 "line_start": node.start_point[0] + 1,
129 "line_end": node.end_point[0] + 1,
130 "docstring": docstring,
131 })
132 store.create_edge(
133 NodeLabel.File, {"path": file_path},
134 EdgeType.CONTAINS,
135 NodeLabel.Class, {"name": name, "file_path": file_path},
136 )
137 stats["classes"] += 1
138 stats["edges"] += 1
139
140 # Inheritance: extends clause
141 heritage = next((c for c in node.children if c.type == "class_heritage"), None)
142 if heritage:
143 for child in heritage.children:
144 if child.type == "extends_clause":
145 for c in child.children:
146 if c.type == "identifier":
147 parent_name = _node_text(c, source)
148 store.create_edge(
149 NodeLabel.Class, {"name": name, "file_path": file_path},
150 EdgeType.INHERITS,
151 NodeLabel.Class, {"name": parent_name, "file_path": file_path},
152 )
153 stats["edges"] += 1
154
155 body = next((c for c in node.children if c.type == "class_body"), None)
156 if body:
157 for child in body.children:
158 if child.type == "method_definition":
159 self._handle_function(child, source, file_path, store, stats,
160 class_name=name)
161
162 def _handle_interface(self, node, source: bytes, file_path: str,
163 store: GraphStore, stats: dict) -> None:
164 name_node = next(
165 (c for c in node.children if c.type == "type_identifier"), None
166 )
167 if not name_node:
168 return
169 name = _node_text(name_node, source)
170 docstring = _jsdoc(node, source)
171 kind = "interface" if node.type == "interface_declaration" else "type"
172
173 store.create_node(NodeLabel.Class, {
174 "name": name,
175 "file_path": file_path,
176 "line_start": node.start_point[0] + 1,
177 "line_end": node.end_point[0] + 1,
178 "docstring": f"{kind}: {docstring}".strip(": "),
179 })
180 store.create_edge(
181 NodeLabel.File, {"path": file_path},
182 EdgeType.CONTAINS,
183 NodeLabel.Class, {"name": name, "file_path": file_path},
184 )
185 stats["classes"] += 1
186 stats["edges"] += 1
187
188 def _handle_function(self, node, source: bytes, file_path: str,
189 store: GraphStore, stats: dict,
190 class_name: str | None) -> None:
191 name_node = next(
192 (c for c in node.children
193 if c.type in ("identifier", "property_identifier")), None
194 )
195 if not name_node:
196 return
197 name = _node_text(name_node, source)
198 if name in ("constructor", "get", "set", "static", "async"):
199 # These are keywords, not useful names — look for next identifier
200 name_node = next(
201 (c for c in node.children
202 if c.type in ("identifier", "property_identifier") and c != name_node),
203 None,
204 )
205 if not name_node:
206 return
207 name = _node_text(name_node, source)
208
209 docstring = _jsdoc(node, source)
210 label = NodeLabel.Method if class_name else NodeLabel.Function
211
212 store.create_node(label, {
213 "name": name,
214 "file_path": file_path,
215 "line_start": node.start_point[0] + 1,
216 "line_end": node.end_point[0] + 1,
217 "docstring": docstring,
218 "class_name": class_name or "",
219 })
220
221 container_label = NodeLabel.Class if class_name else NodeLabel.File
222 container_key = (
223 {"name": class_name, "file_path": file_path}
224 if class_name else {"path": file_path}
225 )
226 store.create_edge(
227 container_label, container_key,
228 EdgeType.CONTAINS,
229 label, {"name": name, "file_path": file_path},
230 )
231 stats["functions"] += 1
232 stats["edges"] += 1
233
234 self._extract_calls(node, source, file_path, name, label, store, stats)
235
236 def _handle_lexical(self, node, source: bytes, file_path: str,
237 store: GraphStore, stats: dict) -> None:
238 """Handle: const foo = () => {} and const bar = function() {}"""
239 for child in node.children:
240 if child.type != "variable_declarator":
241 continue
242 name_node = child.child_by_field_name("name")
243 value_node = child.child_by_field_name("value")
244 if not name_node or not value_node:
245 continue
246 if value_node.type not in ("arrow_function", "function_expression",
247 "function"):
248 continue
249 name = _node_text(name_node, source)
250 docstring = _jsdoc(node, source)
251
252 store.create_node(NodeLabel.Function, {
253 "name": name,
254 "file_path": file_path,
255 "line_start": node.start_point[0] + 1,
256 "line_end": node.end_point[0] + 1,
257 "docstring": docstring,
258 "class_name": "",
259 })
260 store.create_edge(
261 NodeLabel.File, {"path": file_path},
262 EdgeType.CONTAINS,
263 NodeLabel.Function, {"name": name, "file_path": file_path},
264 )
265 stats["functions"] += 1
266 stats["edges"] += 1
267
268 self._extract_calls(value_node, source, file_path, name,
269 NodeLabel.Function, store, stats)
270
271 def _handle_import(self, node, source: bytes, file_path: str,
272 store: GraphStore, stats: dict) -> None:
 
273 for child in node.children:
274 if child.type == "string":
275 module = _node_text(child, source).strip("'\"")
276 store.create_node(NodeLabel.Import, {
277 "name": module,
278 "file_path": file_path,
279 "line_start": node.start_point[0] + 1,
280 "module": module,
@@ -86,64 +285,31 @@
285 NodeLabel.Import, {"name": module, "file_path": file_path},
286 )
287 stats["edges"] += 1
288 break
289
290 def _extract_calls(self, fn_node, source: bytes, file_path: str,
291 fn_name: str, fn_label: str,
292 store: GraphStore, stats: dict) -> None:
293 def walk(node):
294 if node.type == "call_expression":
295 func = node.child_by_field_name("function")
296 if func:
297 text = _node_text(func, source)
298 callee = text.split(".")[-1]
299 store.create_edge(
300 fn_label, {"name": fn_name, "file_path": file_path},
301 EdgeType.CALLS,
302 NodeLabel.Function, {"name": callee, "file_path": file_path},
303 )
304 stats["edges"] += 1
305 for child in node.children:
306 walk(child)
307
308 # Body is statement_block for functions, expression for arrow fns
309 body = next(
310 (c for c in fn_node.children
311 if c.type in ("statement_block", "expression_statement")),
312 None,
313 )
314 if body:
315 walk(body)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
+5 -1
--- pyproject.toml
+++ pyproject.toml
@@ -10,11 +10,11 @@
1010
license = "MIT"
1111
requires-python = ">=3.12"
1212
authors = [
1313
{ name = "CONFLICT LLC" },
1414
]
15
-keywords = ["ast", "knowledge-graph", "code-analysis", "ai-agents", "mcp", "context-management", "falkordb"]
15
+keywords = ["ast", "knowledge-graph", "code-analysis", "ai-agents", "mcp", "context-management", "falkordb", "go", "rust", "java", "typescript"]
1616
classifiers = [
1717
"Development Status :: 3 - Alpha",
1818
"Intended Audience :: Developers",
1919
"Operating System :: OS Independent",
2020
"Programming Language :: Python :: 3",
@@ -31,10 +31,14 @@
3131
"falkordblite>=0.8.0", # embedded SQLite-backed storage (zero-infra local)
3232
# AST parsing — multi-language via tree-sitter grammars
3333
"tree-sitter>=0.24.0",
3434
"tree-sitter-python>=0.23.0",
3535
"tree-sitter-typescript>=0.23.0",
36
+ "tree-sitter-javascript>=0.23.0",
37
+ "tree-sitter-go>=0.23.0",
38
+ "tree-sitter-rust>=0.23.0",
39
+ "tree-sitter-java>=0.23.0",
3640
# CLI
3741
"click>=8.1.0",
3842
"rich>=13.0.0",
3943
# MCP server
4044
"mcp>=1.0.0",
4145
4246
ADDED tests/test_go_parser.py
--- pyproject.toml
+++ pyproject.toml
@@ -10,11 +10,11 @@
10 license = "MIT"
11 requires-python = ">=3.12"
12 authors = [
13 { name = "CONFLICT LLC" },
14 ]
15 keywords = ["ast", "knowledge-graph", "code-analysis", "ai-agents", "mcp", "context-management", "falkordb"]
16 classifiers = [
17 "Development Status :: 3 - Alpha",
18 "Intended Audience :: Developers",
19 "Operating System :: OS Independent",
20 "Programming Language :: Python :: 3",
@@ -31,10 +31,14 @@
31 "falkordblite>=0.8.0", # embedded SQLite-backed storage (zero-infra local)
32 # AST parsing — multi-language via tree-sitter grammars
33 "tree-sitter>=0.24.0",
34 "tree-sitter-python>=0.23.0",
35 "tree-sitter-typescript>=0.23.0",
 
 
 
 
36 # CLI
37 "click>=8.1.0",
38 "rich>=13.0.0",
39 # MCP server
40 "mcp>=1.0.0",
41
42 DDED tests/test_go_parser.py
--- pyproject.toml
+++ pyproject.toml
@@ -10,11 +10,11 @@
10 license = "MIT"
11 requires-python = ">=3.12"
12 authors = [
13 { name = "CONFLICT LLC" },
14 ]
15 keywords = ["ast", "knowledge-graph", "code-analysis", "ai-agents", "mcp", "context-management", "falkordb", "go", "rust", "java", "typescript"]
16 classifiers = [
17 "Development Status :: 3 - Alpha",
18 "Intended Audience :: Developers",
19 "Operating System :: OS Independent",
20 "Programming Language :: Python :: 3",
@@ -31,10 +31,14 @@
31 "falkordblite>=0.8.0", # embedded SQLite-backed storage (zero-infra local)
32 # AST parsing — multi-language via tree-sitter grammars
33 "tree-sitter>=0.24.0",
34 "tree-sitter-python>=0.23.0",
35 "tree-sitter-typescript>=0.23.0",
36 "tree-sitter-javascript>=0.23.0",
37 "tree-sitter-go>=0.23.0",
38 "tree-sitter-rust>=0.23.0",
39 "tree-sitter-java>=0.23.0",
40 # CLI
41 "click>=8.1.0",
42 "rich>=13.0.0",
43 # MCP server
44 "mcp>=1.0.0",
45
46 DDED tests/test_go_parser.py
--- a/tests/test_go_parser.py
+++ b/tests/test_go_parser.py
@@ -0,0 +1,2 @@
1
+func foo() { bar() }"
2
+ callestore.create_edg
--- a/tests/test_go_parser.py
+++ b/tests/test_go_parser.py
@@ -0,0 +1,2 @@
 
 
--- a/tests/test_go_parser.py
+++ b/tests/test_go_parser.py
@@ -0,0 +1,2 @@
1 func foo() { bar() }"
2 callestore.create_edg
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -30,11 +30,11 @@
3030
assert LANGUAGE_MAP[".js"] == "javascript"
3131
assert LANGUAGE_MAP[".jsx"] == "javascript"
3232
3333
def test_no_entry_for_unknown(self):
3434
assert ".rb" not in LANGUAGE_MAP
35
- assert ".go" not in LANGUAGE_MAP
35
+ assert ".php" not in LANGUAGE_MAP
3636
3737
3838
# ── ingest() ─────────────────────────────────────────────────────────────────
3939
4040
class TestRepoIngester:
4141
4242
ADDED tests/test_java_parser.py
4343
ADDED tests/test_rust_parser.py
4444
ADDED tests/test_typescript_parser.py
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -30,11 +30,11 @@
30 assert LANGUAGE_MAP[".js"] == "javascript"
31 assert LANGUAGE_MAP[".jsx"] == "javascript"
32
33 def test_no_entry_for_unknown(self):
34 assert ".rb" not in LANGUAGE_MAP
35 assert ".go" not in LANGUAGE_MAP
36
37
38 # ── ingest() ─────────────────────────────────────────────────────────────────
39
40 class TestRepoIngester:
41
42 DDED tests/test_java_parser.py
43 DDED tests/test_rust_parser.py
44 DDED tests/test_typescript_parser.py
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -30,11 +30,11 @@
30 assert LANGUAGE_MAP[".js"] == "javascript"
31 assert LANGUAGE_MAP[".jsx"] == "javascript"
32
33 def test_no_entry_for_unknown(self):
34 assert ".rb" not in LANGUAGE_MAP
35 assert ".php" not in LANGUAGE_MAP
36
37
38 # ── ingest() ─────────────────────────────────────────────────────────────────
39
40 class TestRepoIngester:
41
42 DDED tests/test_java_parser.py
43 DDED tests/test_rust_parser.py
44 DDED tests/test_typescript_parser.py
--- a/tests/test_java_parser.py
+++ b/tests/test_java_parser.py
@@ -0,0 +1,253 @@
1
+"""Tests for navegador.ingestion.java — JavaParser internal methods."""
2
+
3
+from unittest.mock import MagicMock, patch
4
+
5
+import pytest
6
+
7
+from navegador.graph.schema import NodeLabel
8
+
9
+
10
+class MockNode:
11
+ _id_counter = 0
12
+
13
+ def __init__(self, type_: str, text: bytes = b"", children: list = None,
14
+ start_byte: int = 0, end_byte: int = 0,
15
+ start_point: tuple = (0, 0), end_point: tuple = (0, 0),
16
+ parent=None):
17
+ MockNode._id_counter += 1
18
+ self.id = MockNode._id_counter
19
+ self.type = type_
20
+ self.children = children or []
21
+ self.start_byte = start_byte
22
+ self.end_byte = end_byte
23
+ self.start_point = start_point
24
+ self.end_point = end_point
25
+ self.parent = parent
26
+ self._fields: dict = {}
27
+ for child in self.children:
28
+ child.parent = self
29
+
30
+ def child_by_field_name(self, name: str):
31
+ return self._fields.get(name)
32
+
33
+ def set_field(self, name: str, node):
34
+ self._fields[name] = node
35
+ node.parent = self
36
+ return self
37
+
38
+
39
+def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
40
+ return MockNode(type_, text, start_byte=0, end_byte=len(text))
41
+
42
+
43
+def _make_store():
44
+ store = MagicMock()
45
+ store.query.return_value = MagicMock(result_set=[])
46
+ return store
47
+
48
+
49
+def _make_parser():
50
+ from navegador.ingestion.java import JavaParser
51
+ parser = JavaParser.__new__(JavaParser)
52
+ parser._parser = MagicMock()
53
+ return parser
54
+
55
+
56
+class TestJavaGetLanguage:
57
+ def test_raises_when_not_installed(self):
58
+ from navegador.ingestion.java import _get_java_language
59
+ with patch.dict("sys.modules", {"tree_sitter_java": None, "tree_sitter": None}):
60
+ with pytest.raises(ImportError, match="tree-sitter-java"):
61
+ _get_java_language()
62
+
63
+
64
+class TestJavaNodeText:
65
+ def test_extracts_bytes(self):
66
+ from navegador.ingestion.java import _node_text
67
+ source = b"class Foo {}"
68
+ node = MockNode("identifier", start_byte=6, end_byte=9)
69
+ assert _node_text(node, source) == "Foo"
70
+
71
+
72
+class TestJavadoc:
73
+ def test_extracts_javadoc(self):
74
+ from navegador.ingestion.java import _javadoc
75
+ source = b"/** My class */\nclass Foo {}"
76
+ comment = MockNode("block_comment", start_byte=0, end_byte=15)
77
+ cls_node = MockNode("class_declaration", start_byte=16, end_byte=28)
78
+ _parent = MockNode("program", children=[comment, cls_node])
79
+ result = _javadoc(cls_node, source)
80
+ assert "My class" in result
81
+
82
+ def test_ignores_regular_block_comment(self):
83
+ from navegador.ingestion.java import _javadoc
84
+ source = b"/* regular */\nclass Foo {}"
85
+ comment = MockNode("block_comment", start_byte=0, end_byte=13)
86
+ cls_node = MockNode("class_declaration", start_byte=14, end_byte=26)
87
+ MockNode("program", children=[comment, cls_node])
88
+ result = _javadoc(cls_node, source)
89
+ assert result == ""
90
+
91
+
92
+class TestJavaHandleClass:
93
+ def test_creates_class_node(self):
94
+ parser = _make_parser()
95
+ store = _make_store()
96
+ source = b"class Foo {}"
97
+ name_node = _text_node(b"Foo")
98
+ body = MockNode("class_body")
99
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
100
+ node.set_field("name", name_node)
101
+ node.set_field("body", body)
102
+ _parent = MockNode("program", children=[node])
103
+ stats = {"functions": 0, "classes": 0, "edges": 0}
104
+ parser._handle_class(node, source, "Foo.java", store, stats)
105
+ assert stats["classes"] == 1
106
+ label = store.create_node.call_args[0][0]
107
+ assert label == NodeLabel.Class
108
+
109
+ def test_creates_inherits_edge(self):
110
+ parser = _make_parser()
111
+ store = _make_store()
112
+ source = b"class Child extends Parent {}"
113
+ name_node = _text_node(b"Child")
114
+ parent_id = _text_node(b"Parent", "type_identifier")
115
+ superclass = MockNode("superclass", children=[parent_id])
116
+ body = MockNode("class_body")
117
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 28))
118
+ node.set_field("name", name_node)
119
+ node.set_field("superclass", superclass)
120
+ node.set_field("body", body)
121
+ MockNode("program", children=[node])
122
+ stats = {"functions": 0, "classes": 0, "edges": 0}
123
+ parser._handle_class(node, source, "Child.java", store, stats)
124
+ # Should have CONTAINS edge + INHERITS edge
125
+ assert stats["edges"] == 2
126
+
127
+ def test_ingests_methods_in_body(self):
128
+ parser = _make_parser()
129
+ store = _make_store()
130
+ source = b"class Foo { void bar() {} }"
131
+ name_node = _text_node(b"Foo")
132
+ method_name = _text_node(b"bar")
133
+ method_body = MockNode("block")
134
+ method = MockNode("method_declaration", start_point=(1, 2), end_point=(1, 14))
135
+ method.set_field("name", method_name)
136
+ method.set_field("body", method_body)
137
+ body = MockNode("class_body", children=[method])
138
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 26))
139
+ node.set_field("name", name_node)
140
+ node.set_field("body", body)
141
+ MockNode("program", children=[node])
142
+ stats = {"functions": 0, "classes": 0, "edges": 0}
143
+ parser._handle_class(node, source, "Foo.java", store, stats)
144
+ assert stats["functions"] == 1
145
+
146
+ def test_skips_if_no_name(self):
147
+ parser = _make_parser()
148
+ store = _make_store()
149
+ node = MockNode("class_declaration")
150
+ stats = {"functions": 0, "classes": 0, "edges": 0}
151
+ parser._handle_class(node, b"", "X.java", store, stats)
152
+ assert stats["classes"] == 0
153
+
154
+
155
+class TestJavaHandleInterface:
156
+ def test_creates_interface_node(self):
157
+ parser = _make_parser()
158
+ store = _make_store()
159
+ source = b"interface Saveable {}"
160
+ name_node = _text_node(b"Saveable")
161
+ body = MockNode("interface_body")
162
+ node = MockNode("interface_declaration", start_point=(0, 0), end_point=(0, 20))
163
+ node.set_field("name", name_node)
164
+ node.set_field("body", body)
165
+ MockNode("program", children=[node])
166
+ stats = {"functions": 0, "classes": 0, "edges": 0}
167
+ parser._handle_interface(node, source, "Saveable.java", store, stats)
168
+ assert stats["classes"] == 1
169
+ props = store.create_node.call_args[0][1]
170
+ assert "interface" in props.get("docstring", "")
171
+
172
+ def test_skips_if_no_name(self):
173
+ parser = _make_parser()
174
+ store = _make_store()
175
+ node = MockNode("interface_declaration")
176
+ stats = {"functions": 0, "classes": 0, "edges": 0}
177
+ parser._handle_interface(node, b"", "X.java", store, stats)
178
+ assert stats["classes"] == 0
179
+
180
+
181
+class TestJavaHandleMethod:
182
+ def test_creates_method_node(self):
183
+ parser = _make_parser()
184
+ store = _make_store()
185
+ source = b"void save() {}"
186
+ name_node = _text_node(b"save")
187
+ body = MockNode("block")
188
+ node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 13))
189
+ node.set_field("name", name_node)
190
+ node.set_field("body", body)
191
+ MockNode("class_body", children=[node])
192
+ stats = {"functions": 0, "classes": 0, "edges": 0}
193
+ parser._handle_method(node, source, "Foo.java", store, stats, class_name="Foo")
194
+ assert stats["functions"] == 1
195
+ label = store.create_node.call_args[0][0]
196
+ assert label == NodeLabel.Method
197
+
198
+ def test_skips_if_no_name(self):
199
+ parser = _make_parser()
200
+ store = _make_store()
201
+ node = MockNode("method_declaration")
202
+ stats = {"functions": 0, "classes": 0, "edges": 0}
203
+ parser._handle_method(node, b"", "X.java", store, stats, class_name="X")
204
+ assert stats["functions"] == 0
205
+
206
+
207
+class TestJavaHandleImport:
208
+ def test_ingests_import(self):
209
+ parser = _make_parser()
210
+ store = _make_store()
211
+ source = b"import java.util.List;"
212
+ node = MockNode("import_declaration", start_byte=0, end_byte=22,
213
+ start_point=(0, 0))
214
+ stats = {"functions": 0, "classes": 0, "edges": 0}
215
+ parser._handle_import(node, source, "Foo.java", store, stats)
216
+ assert stats["edges"] == 1
217
+ props = store.create_node.call_args[0][1]
218
+ assert "java.util.List" in props["name"]
219
+
220
+ def test_handles_static_import(self):
221
+ parser = _make_parser()
222
+ store = _make_store()
223
+ source = b"import static java.util.Collections.sort;"
224
+ node = MockNode("import_declaration", start_byte=0, end_byte=41,
225
+ start_point=(0, 0))
226
+ stats = {"functions": 0, "classes": 0, "edges": 0}
227
+ parser._handle_import(node, source, "Foo.java", store, stats)
228
+ assert stats["edges"] == 1
229
+
230
+
231
+class TestJavaExtractCalls:
232
+ def test_extracts_method_invocation(self):
233
+ parser = _make_parser()
234
+ store = _make_void foo() { bar(); }re()
235
+ source = b"bar"
236
+ callee = _text_node(b"bar")
237
+ invocation = MockNode("method_invocation")
238
+ invocation.set_field("name", callee)
239
+ body = MockNode("block", children=[invocation])
240
+ node = MockNode("method_declaration")
241
+ node.set_field("body", body)
242
+ stats = {"functions": 0, "classes": 0, "edges": 0}
243
+ parser._extract_calls(node, source, "Foo.java", "foo", store, stats)
244
+ assert sstore.creat
245
+
246
+ def test_no_calls_in_empty_body(self):
247
+ parser = _make_parser()
248
+ store = _make_store()
249
+ node = MockNode("method_declaration")
250
+ node.set_field("body", MockNode("block"))
251
+ stats = {"functions": 0, "classes": 0, "edges": 0}
252
+ parser._extract_calls(node, b"", "X.java", "foo", store, stats)
253
+
--- a/tests/test_java_parser.py
+++ b/tests/test_java_parser.py
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_java_parser.py
+++ b/tests/test_java_parser.py
@@ -0,0 +1,253 @@
1 """Tests for navegador.ingestion.java — JavaParser internal methods."""
2
3 from unittest.mock import MagicMock, patch
4
5 import pytest
6
7 from navegador.graph.schema import NodeLabel
8
9
10 class MockNode:
11 _id_counter = 0
12
13 def __init__(self, type_: str, text: bytes = b"", children: list = None,
14 start_byte: int = 0, end_byte: int = 0,
15 start_point: tuple = (0, 0), end_point: tuple = (0, 0),
16 parent=None):
17 MockNode._id_counter += 1
18 self.id = MockNode._id_counter
19 self.type = type_
20 self.children = children or []
21 self.start_byte = start_byte
22 self.end_byte = end_byte
23 self.start_point = start_point
24 self.end_point = end_point
25 self.parent = parent
26 self._fields: dict = {}
27 for child in self.children:
28 child.parent = self
29
30 def child_by_field_name(self, name: str):
31 return self._fields.get(name)
32
33 def set_field(self, name: str, node):
34 self._fields[name] = node
35 node.parent = self
36 return self
37
38
39 def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
40 return MockNode(type_, text, start_byte=0, end_byte=len(text))
41
42
43 def _make_store():
44 store = MagicMock()
45 store.query.return_value = MagicMock(result_set=[])
46 return store
47
48
49 def _make_parser():
50 from navegador.ingestion.java import JavaParser
51 parser = JavaParser.__new__(JavaParser)
52 parser._parser = MagicMock()
53 return parser
54
55
56 class TestJavaGetLanguage:
57 def test_raises_when_not_installed(self):
58 from navegador.ingestion.java import _get_java_language
59 with patch.dict("sys.modules", {"tree_sitter_java": None, "tree_sitter": None}):
60 with pytest.raises(ImportError, match="tree-sitter-java"):
61 _get_java_language()
62
63
64 class TestJavaNodeText:
65 def test_extracts_bytes(self):
66 from navegador.ingestion.java import _node_text
67 source = b"class Foo {}"
68 node = MockNode("identifier", start_byte=6, end_byte=9)
69 assert _node_text(node, source) == "Foo"
70
71
72 class TestJavadoc:
73 def test_extracts_javadoc(self):
74 from navegador.ingestion.java import _javadoc
75 source = b"/** My class */\nclass Foo {}"
76 comment = MockNode("block_comment", start_byte=0, end_byte=15)
77 cls_node = MockNode("class_declaration", start_byte=16, end_byte=28)
78 _parent = MockNode("program", children=[comment, cls_node])
79 result = _javadoc(cls_node, source)
80 assert "My class" in result
81
82 def test_ignores_regular_block_comment(self):
83 from navegador.ingestion.java import _javadoc
84 source = b"/* regular */\nclass Foo {}"
85 comment = MockNode("block_comment", start_byte=0, end_byte=13)
86 cls_node = MockNode("class_declaration", start_byte=14, end_byte=26)
87 MockNode("program", children=[comment, cls_node])
88 result = _javadoc(cls_node, source)
89 assert result == ""
90
91
92 class TestJavaHandleClass:
93 def test_creates_class_node(self):
94 parser = _make_parser()
95 store = _make_store()
96 source = b"class Foo {}"
97 name_node = _text_node(b"Foo")
98 body = MockNode("class_body")
99 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
100 node.set_field("name", name_node)
101 node.set_field("body", body)
102 _parent = MockNode("program", children=[node])
103 stats = {"functions": 0, "classes": 0, "edges": 0}
104 parser._handle_class(node, source, "Foo.java", store, stats)
105 assert stats["classes"] == 1
106 label = store.create_node.call_args[0][0]
107 assert label == NodeLabel.Class
108
109 def test_creates_inherits_edge(self):
110 parser = _make_parser()
111 store = _make_store()
112 source = b"class Child extends Parent {}"
113 name_node = _text_node(b"Child")
114 parent_id = _text_node(b"Parent", "type_identifier")
115 superclass = MockNode("superclass", children=[parent_id])
116 body = MockNode("class_body")
117 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 28))
118 node.set_field("name", name_node)
119 node.set_field("superclass", superclass)
120 node.set_field("body", body)
121 MockNode("program", children=[node])
122 stats = {"functions": 0, "classes": 0, "edges": 0}
123 parser._handle_class(node, source, "Child.java", store, stats)
124 # Should have CONTAINS edge + INHERITS edge
125 assert stats["edges"] == 2
126
127 def test_ingests_methods_in_body(self):
128 parser = _make_parser()
129 store = _make_store()
130 source = b"class Foo { void bar() {} }"
131 name_node = _text_node(b"Foo")
132 method_name = _text_node(b"bar")
133 method_body = MockNode("block")
134 method = MockNode("method_declaration", start_point=(1, 2), end_point=(1, 14))
135 method.set_field("name", method_name)
136 method.set_field("body", method_body)
137 body = MockNode("class_body", children=[method])
138 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 26))
139 node.set_field("name", name_node)
140 node.set_field("body", body)
141 MockNode("program", children=[node])
142 stats = {"functions": 0, "classes": 0, "edges": 0}
143 parser._handle_class(node, source, "Foo.java", store, stats)
144 assert stats["functions"] == 1
145
146 def test_skips_if_no_name(self):
147 parser = _make_parser()
148 store = _make_store()
149 node = MockNode("class_declaration")
150 stats = {"functions": 0, "classes": 0, "edges": 0}
151 parser._handle_class(node, b"", "X.java", store, stats)
152 assert stats["classes"] == 0
153
154
155 class TestJavaHandleInterface:
156 def test_creates_interface_node(self):
157 parser = _make_parser()
158 store = _make_store()
159 source = b"interface Saveable {}"
160 name_node = _text_node(b"Saveable")
161 body = MockNode("interface_body")
162 node = MockNode("interface_declaration", start_point=(0, 0), end_point=(0, 20))
163 node.set_field("name", name_node)
164 node.set_field("body", body)
165 MockNode("program", children=[node])
166 stats = {"functions": 0, "classes": 0, "edges": 0}
167 parser._handle_interface(node, source, "Saveable.java", store, stats)
168 assert stats["classes"] == 1
169 props = store.create_node.call_args[0][1]
170 assert "interface" in props.get("docstring", "")
171
172 def test_skips_if_no_name(self):
173 parser = _make_parser()
174 store = _make_store()
175 node = MockNode("interface_declaration")
176 stats = {"functions": 0, "classes": 0, "edges": 0}
177 parser._handle_interface(node, b"", "X.java", store, stats)
178 assert stats["classes"] == 0
179
180
181 class TestJavaHandleMethod:
182 def test_creates_method_node(self):
183 parser = _make_parser()
184 store = _make_store()
185 source = b"void save() {}"
186 name_node = _text_node(b"save")
187 body = MockNode("block")
188 node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 13))
189 node.set_field("name", name_node)
190 node.set_field("body", body)
191 MockNode("class_body", children=[node])
192 stats = {"functions": 0, "classes": 0, "edges": 0}
193 parser._handle_method(node, source, "Foo.java", store, stats, class_name="Foo")
194 assert stats["functions"] == 1
195 label = store.create_node.call_args[0][0]
196 assert label == NodeLabel.Method
197
198 def test_skips_if_no_name(self):
199 parser = _make_parser()
200 store = _make_store()
201 node = MockNode("method_declaration")
202 stats = {"functions": 0, "classes": 0, "edges": 0}
203 parser._handle_method(node, b"", "X.java", store, stats, class_name="X")
204 assert stats["functions"] == 0
205
206
207 class TestJavaHandleImport:
208 def test_ingests_import(self):
209 parser = _make_parser()
210 store = _make_store()
211 source = b"import java.util.List;"
212 node = MockNode("import_declaration", start_byte=0, end_byte=22,
213 start_point=(0, 0))
214 stats = {"functions": 0, "classes": 0, "edges": 0}
215 parser._handle_import(node, source, "Foo.java", store, stats)
216 assert stats["edges"] == 1
217 props = store.create_node.call_args[0][1]
218 assert "java.util.List" in props["name"]
219
220 def test_handles_static_import(self):
221 parser = _make_parser()
222 store = _make_store()
223 source = b"import static java.util.Collections.sort;"
224 node = MockNode("import_declaration", start_byte=0, end_byte=41,
225 start_point=(0, 0))
226 stats = {"functions": 0, "classes": 0, "edges": 0}
227 parser._handle_import(node, source, "Foo.java", store, stats)
228 assert stats["edges"] == 1
229
230
231 class TestJavaExtractCalls:
232 def test_extracts_method_invocation(self):
233 parser = _make_parser()
234 store = _make_void foo() { bar(); }re()
235 source = b"bar"
236 callee = _text_node(b"bar")
237 invocation = MockNode("method_invocation")
238 invocation.set_field("name", callee)
239 body = MockNode("block", children=[invocation])
240 node = MockNode("method_declaration")
241 node.set_field("body", body)
242 stats = {"functions": 0, "classes": 0, "edges": 0}
243 parser._extract_calls(node, source, "Foo.java", "foo", store, stats)
244 assert sstore.creat
245
246 def test_no_calls_in_empty_body(self):
247 parser = _make_parser()
248 store = _make_store()
249 node = MockNode("method_declaration")
250 node.set_field("body", MockNode("block"))
251 stats = {"functions": 0, "classes": 0, "edges": 0}
252 parser._extract_calls(node, b"", "X.java", "foo", store, stats)
253
--- a/tests/test_rust_parser.py
+++ b/tests/test_rust_parser.py
@@ -0,0 +1,253 @@
1
+"""Tests for navegador.ingestion.rust — RustParser internal methods."""
2
+
3
+from unittest.mock import MagicMock, patch
4
+
5
+import pytest
6
+
7
+from navegador.graph.schema import NodeLabel
8
+
9
+
10
+class MockNode:
11
+ _id_counter = 0
12
+
13
+ def __init__(self, type_: str, text: bytes = b"", children: list = None,
14
+ start_byte: int = 0, end_byte: int = 0,
15
+ start_point: tuple = (0, 0), end_point: tuple = (0, 0),
16
+ parent=None):
17
+ MockNode._id_counter += 1
18
+ self.id = MockNode._id_counter
19
+ self.type = type_
20
+ self._text = text
21
+ self.children = children or []
22
+ self.start_byte = start_byte
23
+ self.end_byte = end_byte
24
+ self.start_point = start_point
25
+ self.end_point = end_point
26
+ self.parent = parent
27
+ self._fields: dict = {}
28
+ for child in self.children:
29
+ child.parent = self
30
+
31
+ def child_by_field_name(self, name: str):
32
+ return self._fields.get(name)
33
+
34
+ def set_field(self, name: str, node):
35
+ self._fields[name] = node
36
+ node.parent = self
37
+ return self
38
+
39
+
40
+def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
41
+ return MockNode(type_, text, start_byte=0, end_byte=len(text))
42
+
43
+
44
+def _make_store():
45
+ store = MagicMock()
46
+ store.query.return_value = MagicMock(result_set=[])
47
+ return store
48
+
49
+
50
+def _make_parser():
51
+ from navegador.ingestion.rust import RustParser
52
+ parser = RustParser.__new__(RustParser)
53
+ parser._parser = MagicMock()
54
+ return parser
55
+
56
+
57
+class TestRustGetLanguage:
58
+ def test_raises_when_not_installed(self):
59
+ from navegador.ingestion.rust import _get_rust_language
60
+ with patch.dict("sys.modules", {"tree_sitter_rust": None, "tree_sitter": None}):
61
+ with pytest.raises(ImportError, match="tree-sitter-rust"):
62
+ _get_rust_language()
63
+
64
+
65
+class TestRustNodeText:
66
+ def test_extracts_bytes(self):
67
+ from navegador.ingestion.rust import _node_text
68
+ source = b"fn main() {}"
69
+ node = MockNode("identifier", start_byte=3, end_byte=7)
70
+ assert _node_text(node, source) == "main"
71
+
72
+
73
+class TestRustDocComment:
74
+ def test_collects_triple_slash_comments(self):
75
+ from navegador.ingestion.rust import _doc_comment
76
+ source = b"/// Docs line 1\n/// Docs line 2\nfn foo() {}"
77
+ doc1 = MockNode("line_comment", start_byte=0, end_byte=15)
78
+ doc2 = MockNode("line_comment", start_byte=16, end_byte=31)
79
+ fn_node = MockNode("function_item", start_byte=32, end_byte=44)
80
+ _parent = MockNode("source_file", children=[doc1, doc2, fn_node])
81
+ result = _doc_comment(fn_node, source)
82
+ assert "Docs line 1" in result
83
+ assert "Docs line 2" in result
84
+
85
+ def test_ignores_non_doc_comments(self):
86
+ from navegador.ingestion.rust import _doc_comment
87
+ source = b"// regular comment\nfn foo() {}"
88
+ comment = MockNode("line_comment", start_byte=0, end_byte=18)
89
+ fn_node = MockNode("function_item", start_byte=19, end_byte=30)
90
+ MockNode("source_file", children=[comment, fn_node])
91
+ result = _doc_comment(fn_node, source)
92
+ assert result == ""
93
+
94
+ def test_no_parent(self):
95
+ from navegador.ingestion.rust import _doc_comment
96
+ fn_node = MockNode("function_item")
97
+ assert _doc_comment(fn_node, b"") == ""
98
+
99
+
100
+class TestRustHandleFunction:
101
+ def test_creates_function_node(self):
102
+ parser = _make_parser()
103
+ store = _make_store()
104
+ source = b"fn foo() {}"
105
+ name = _text_node(b"foo")
106
+ node = MockNode("function_item", start_point=(0, 0), end_point=(0, 10))
107
+ node.set_field("name", name)
108
+ stats = {"functions": 0, "classes": 0, "edges": 0}
109
+ parser._handle_function(node, source, "lib.rs", store, stats, impl_type=None)
110
+ assert stats["functions"] == 1
111
+ label = store.create_node.call_args[0][0]
112
+ assert label == NodeLabel.Function
113
+
114
+ def test_creates_method_when_impl_type_given(self):
115
+ parser = _make_parser()
116
+ store = _make_store()
117
+ source = b"fn save(&self) {}"
118
+ name = _text_node(b"save")
119
+ node = MockNode("function_item", start_point=(0, 0), end_point=(0, 16))
120
+ node.set_field("name", name)
121
+ stats = {"functions": 0, "classes": 0, "edges": 0}
122
+ parser._handle_function(node, source, "lib.rs", store, stats, impl_type="Repo")
123
+ label = store.create_node.call_args[0][0]
124
+ assert label == NodeLabel.Method
125
+
126
+ def test_skips_if_no_name(self):
127
+ parser = _make_parser()
128
+ store = _make_store()
129
+ node = MockNode("function_item", start_point=(0, 0), end_point=(0, 0))
130
+ stats = {"functions": 0, "classes": 0, "edges": 0}
131
+ parser._handle_function(node, b"", "lib.rs", store, stats, impl_type=None)
132
+ assert stats["functions"] == 0
133
+
134
+
135
+class TestRustHandleImpl:
136
+ def test_handles_impl_block(self):
137
+ parser = _make_parser()
138
+ store = _make_store()
139
+ source = b"impl Repo { fn save(&self) {} }"
140
+ type_node = _text_node(b"Repo", "type_identifier")
141
+ name_node = _text_node(b"save")
142
+ fn_item = MockNode("function_item", start_point=(0, 12), end_point=(0, 28))
143
+ fn_item.set_field("name", name_node)
144
+ body = MockNode("declaration_list", children=[fn_item])
145
+ impl = MockNode("impl_item", start_point=(0, 0), end_point=(0, 30))
146
+ impl.set_field("type", type_node)
147
+ impl.set_field("body", body)
148
+ stats = {"functions": 0, "classes": 0, "edges": 0}
149
+ parser._handle_impl(impl, source, "lib.rs", store, stats)
150
+ assert stats["functions"] == 1
151
+
152
+ def test_handles_impl_with_no_body(self):
153
+ parser = _make_parser()
154
+ store = _make_store()
155
+ impl = MockNode("impl_item")
156
+ stats = {"functions": 0, "classes": 0, "edges": 0}
157
+ parser._handle_impl(impl, b"", "lib.rs", store, stats)
158
+ assert stats["functions"] == 0
159
+
160
+
161
+class TestRustHandleType:
162
+ def test_ingests_struct(self):
163
+ parser = _make_parser()
164
+ store = _make_store()
165
+ source = b"struct Foo {}"
166
+ name = _text_node(b"Foo", "type_identifier")
167
+ node = MockNode("struct_item", start_point=(0, 0), end_point=(0, 12))
168
+ node.set_field("name", name)
169
+ _parent = MockNode("source_file", children=[node])
170
+ stats = {"functions": 0, "classes": 0, "edges": 0}
171
+ parser._handle_type(node, source, "lib.rs", store, stats)
172
+ assert stats["classes"] == 1
173
+ props = store.create_node.call_args[0][1]
174
+ assert "struct" in props["docstring"]
175
+
176
+ def test_ingests_enum(self):
177
+ parser = _make_parser()
178
+ store = _make_store()
179
+ source = b"enum Color { Red, Green }"
180
+ name = _text_node(b"Color", "type_identifier")
181
+ node = MockNode("enum_item", start_point=(0, 0), end_point=(0, 24))
182
+ node.set_field("name", name)
183
+ MockNode("source_file", children=[node])
184
+ stats = {"functions": 0, "classes": 0, "edges": 0}
185
+ parser._handle_type(node, source, "lib.rs", store, stats)
186
+ assert stats["classes"] == 1
187
+
188
+ def test_ingests_trait(self):
189
+ parser = _make_parser()
190
+ store = _make_store()
191
+ source = b"trait Saveable {}"
192
+ name = _text_node(b"Saveable", "type_identifier")
193
+ node = MockNode("trait_item", start_point=(0, 0), end_point=(0, 16))
194
+ node.set_field("name", name)
195
+ MockNode("source_file", children=[node])
196
+ stats = {"functions": 0, "classes": 0, "edges": 0}
197
+ parser._handle_type(node, source, "lib.rs", store, stats)
198
+ assert stats["classes"] == 1
199
+
200
+ def test_skips_if_no_name(self):
201
+ parser = _make_parser()
202
+ store = _make_store()
203
+ node = MockNode("struct_item")
204
+ stats = {"functions": 0, "classes": 0, "edges": 0}
205
+ parser._handle_type(node, b"", "lib.rs", store, stats)
206
+ assert stats["classes"] == 0
207
+
208
+
209
+class TestRustHandleUse:
210
+ def test_ingests_use_statement(self):
211
+ parser = _make_parser()
212
+ store = _make_store()
213
+ source = b"use std::collections::HashMap;"
214
+ node = MockNode("use_declaration", start_byte=0, end_byte=30,
215
+ start_point=(0, 0))
216
+ stats = {"functions": 0, "classes": 0, "edges": 0}
217
+ parser._handle_use(node, source, "lib.rs", store, stats)
218
+ assert stats["edges"] == 1
219
+ store.create_node.assert_called_once()
220
+ props = store.create_node.call_args[0][1]
221
+ assert "HashMap" in props["name"] or "std" in props["name"]
222
+
223
+
224
+class TestRustExtractCalls:
225
+ def test_extracts_call(self):
226
+ parser = _make_parser()
227
+ store = _make_fn foo() { bar() }re()
228
+ source = b"bar"
229
+ callee = _text_node(b"bar")
230
+ call_node = MockNode("call_expression")
231
+ call_node.set_field("function", callee)
232
+ body = MockNode("block", children=[call_node])
233
+ fn_node = MockNode("function_item")
234
+ fn_node.set_field("body", body)
235
+ stats = {"functions": 0, "classes": 0, "edges": 0}
236
+ parser._extract_calls(fn_node, source, "lib.rs", "foo",
237
+ NodeLabel.Funct
238
+ def test_handles_method_call_syntax(self):
239
+ parser = _make_parser()
240
+ store = _make_store()
241
+ source = b"Repo::save"
242
+ callee = _text_node(b"Repo::save")
243
+ call_node = MockNode("call_expression")
244
+ call_node.set_field("function", callee)
245
+ body = MockNode("block", children=[call_node])
246
+ fn_node = MockNode("function_item")
247
+ fn_node.set_field("body", body)
248
+ stats = {"functions": 0, "classes": 0, "edges": 0}
249
+ parser._extract_calls(fn_node, source, "lib.rs", "foo",
250
+ NodeLabel.Function, store, stats)
251
+ # "Repo::save" → callee = "save"
252
+ edge_call = store.create_edge.call_args[0]
253
+ assert edge
--- a/tests/test_rust_parser.py
+++ b/tests/test_rust_parser.py
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_rust_parser.py
+++ b/tests/test_rust_parser.py
@@ -0,0 +1,253 @@
1 """Tests for navegador.ingestion.rust — RustParser internal methods."""
2
3 from unittest.mock import MagicMock, patch
4
5 import pytest
6
7 from navegador.graph.schema import NodeLabel
8
9
10 class MockNode:
11 _id_counter = 0
12
13 def __init__(self, type_: str, text: bytes = b"", children: list = None,
14 start_byte: int = 0, end_byte: int = 0,
15 start_point: tuple = (0, 0), end_point: tuple = (0, 0),
16 parent=None):
17 MockNode._id_counter += 1
18 self.id = MockNode._id_counter
19 self.type = type_
20 self._text = text
21 self.children = children or []
22 self.start_byte = start_byte
23 self.end_byte = end_byte
24 self.start_point = start_point
25 self.end_point = end_point
26 self.parent = parent
27 self._fields: dict = {}
28 for child in self.children:
29 child.parent = self
30
31 def child_by_field_name(self, name: str):
32 return self._fields.get(name)
33
34 def set_field(self, name: str, node):
35 self._fields[name] = node
36 node.parent = self
37 return self
38
39
40 def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
41 return MockNode(type_, text, start_byte=0, end_byte=len(text))
42
43
44 def _make_store():
45 store = MagicMock()
46 store.query.return_value = MagicMock(result_set=[])
47 return store
48
49
50 def _make_parser():
51 from navegador.ingestion.rust import RustParser
52 parser = RustParser.__new__(RustParser)
53 parser._parser = MagicMock()
54 return parser
55
56
57 class TestRustGetLanguage:
58 def test_raises_when_not_installed(self):
59 from navegador.ingestion.rust import _get_rust_language
60 with patch.dict("sys.modules", {"tree_sitter_rust": None, "tree_sitter": None}):
61 with pytest.raises(ImportError, match="tree-sitter-rust"):
62 _get_rust_language()
63
64
65 class TestRustNodeText:
66 def test_extracts_bytes(self):
67 from navegador.ingestion.rust import _node_text
68 source = b"fn main() {}"
69 node = MockNode("identifier", start_byte=3, end_byte=7)
70 assert _node_text(node, source) == "main"
71
72
73 class TestRustDocComment:
74 def test_collects_triple_slash_comments(self):
75 from navegador.ingestion.rust import _doc_comment
76 source = b"/// Docs line 1\n/// Docs line 2\nfn foo() {}"
77 doc1 = MockNode("line_comment", start_byte=0, end_byte=15)
78 doc2 = MockNode("line_comment", start_byte=16, end_byte=31)
79 fn_node = MockNode("function_item", start_byte=32, end_byte=44)
80 _parent = MockNode("source_file", children=[doc1, doc2, fn_node])
81 result = _doc_comment(fn_node, source)
82 assert "Docs line 1" in result
83 assert "Docs line 2" in result
84
85 def test_ignores_non_doc_comments(self):
86 from navegador.ingestion.rust import _doc_comment
87 source = b"// regular comment\nfn foo() {}"
88 comment = MockNode("line_comment", start_byte=0, end_byte=18)
89 fn_node = MockNode("function_item", start_byte=19, end_byte=30)
90 MockNode("source_file", children=[comment, fn_node])
91 result = _doc_comment(fn_node, source)
92 assert result == ""
93
94 def test_no_parent(self):
95 from navegador.ingestion.rust import _doc_comment
96 fn_node = MockNode("function_item")
97 assert _doc_comment(fn_node, b"") == ""
98
99
100 class TestRustHandleFunction:
101 def test_creates_function_node(self):
102 parser = _make_parser()
103 store = _make_store()
104 source = b"fn foo() {}"
105 name = _text_node(b"foo")
106 node = MockNode("function_item", start_point=(0, 0), end_point=(0, 10))
107 node.set_field("name", name)
108 stats = {"functions": 0, "classes": 0, "edges": 0}
109 parser._handle_function(node, source, "lib.rs", store, stats, impl_type=None)
110 assert stats["functions"] == 1
111 label = store.create_node.call_args[0][0]
112 assert label == NodeLabel.Function
113
114 def test_creates_method_when_impl_type_given(self):
115 parser = _make_parser()
116 store = _make_store()
117 source = b"fn save(&self) {}"
118 name = _text_node(b"save")
119 node = MockNode("function_item", start_point=(0, 0), end_point=(0, 16))
120 node.set_field("name", name)
121 stats = {"functions": 0, "classes": 0, "edges": 0}
122 parser._handle_function(node, source, "lib.rs", store, stats, impl_type="Repo")
123 label = store.create_node.call_args[0][0]
124 assert label == NodeLabel.Method
125
126 def test_skips_if_no_name(self):
127 parser = _make_parser()
128 store = _make_store()
129 node = MockNode("function_item", start_point=(0, 0), end_point=(0, 0))
130 stats = {"functions": 0, "classes": 0, "edges": 0}
131 parser._handle_function(node, b"", "lib.rs", store, stats, impl_type=None)
132 assert stats["functions"] == 0
133
134
135 class TestRustHandleImpl:
136 def test_handles_impl_block(self):
137 parser = _make_parser()
138 store = _make_store()
139 source = b"impl Repo { fn save(&self) {} }"
140 type_node = _text_node(b"Repo", "type_identifier")
141 name_node = _text_node(b"save")
142 fn_item = MockNode("function_item", start_point=(0, 12), end_point=(0, 28))
143 fn_item.set_field("name", name_node)
144 body = MockNode("declaration_list", children=[fn_item])
145 impl = MockNode("impl_item", start_point=(0, 0), end_point=(0, 30))
146 impl.set_field("type", type_node)
147 impl.set_field("body", body)
148 stats = {"functions": 0, "classes": 0, "edges": 0}
149 parser._handle_impl(impl, source, "lib.rs", store, stats)
150 assert stats["functions"] == 1
151
152 def test_handles_impl_with_no_body(self):
153 parser = _make_parser()
154 store = _make_store()
155 impl = MockNode("impl_item")
156 stats = {"functions": 0, "classes": 0, "edges": 0}
157 parser._handle_impl(impl, b"", "lib.rs", store, stats)
158 assert stats["functions"] == 0
159
160
161 class TestRustHandleType:
162 def test_ingests_struct(self):
163 parser = _make_parser()
164 store = _make_store()
165 source = b"struct Foo {}"
166 name = _text_node(b"Foo", "type_identifier")
167 node = MockNode("struct_item", start_point=(0, 0), end_point=(0, 12))
168 node.set_field("name", name)
169 _parent = MockNode("source_file", children=[node])
170 stats = {"functions": 0, "classes": 0, "edges": 0}
171 parser._handle_type(node, source, "lib.rs", store, stats)
172 assert stats["classes"] == 1
173 props = store.create_node.call_args[0][1]
174 assert "struct" in props["docstring"]
175
176 def test_ingests_enum(self):
177 parser = _make_parser()
178 store = _make_store()
179 source = b"enum Color { Red, Green }"
180 name = _text_node(b"Color", "type_identifier")
181 node = MockNode("enum_item", start_point=(0, 0), end_point=(0, 24))
182 node.set_field("name", name)
183 MockNode("source_file", children=[node])
184 stats = {"functions": 0, "classes": 0, "edges": 0}
185 parser._handle_type(node, source, "lib.rs", store, stats)
186 assert stats["classes"] == 1
187
188 def test_ingests_trait(self):
189 parser = _make_parser()
190 store = _make_store()
191 source = b"trait Saveable {}"
192 name = _text_node(b"Saveable", "type_identifier")
193 node = MockNode("trait_item", start_point=(0, 0), end_point=(0, 16))
194 node.set_field("name", name)
195 MockNode("source_file", children=[node])
196 stats = {"functions": 0, "classes": 0, "edges": 0}
197 parser._handle_type(node, source, "lib.rs", store, stats)
198 assert stats["classes"] == 1
199
200 def test_skips_if_no_name(self):
201 parser = _make_parser()
202 store = _make_store()
203 node = MockNode("struct_item")
204 stats = {"functions": 0, "classes": 0, "edges": 0}
205 parser._handle_type(node, b"", "lib.rs", store, stats)
206 assert stats["classes"] == 0
207
208
209 class TestRustHandleUse:
210 def test_ingests_use_statement(self):
211 parser = _make_parser()
212 store = _make_store()
213 source = b"use std::collections::HashMap;"
214 node = MockNode("use_declaration", start_byte=0, end_byte=30,
215 start_point=(0, 0))
216 stats = {"functions": 0, "classes": 0, "edges": 0}
217 parser._handle_use(node, source, "lib.rs", store, stats)
218 assert stats["edges"] == 1
219 store.create_node.assert_called_once()
220 props = store.create_node.call_args[0][1]
221 assert "HashMap" in props["name"] or "std" in props["name"]
222
223
224 class TestRustExtractCalls:
225 def test_extracts_call(self):
226 parser = _make_parser()
227 store = _make_fn foo() { bar() }re()
228 source = b"bar"
229 callee = _text_node(b"bar")
230 call_node = MockNode("call_expression")
231 call_node.set_field("function", callee)
232 body = MockNode("block", children=[call_node])
233 fn_node = MockNode("function_item")
234 fn_node.set_field("body", body)
235 stats = {"functions": 0, "classes": 0, "edges": 0}
236 parser._extract_calls(fn_node, source, "lib.rs", "foo",
237 NodeLabel.Funct
238 def test_handles_method_call_syntax(self):
239 parser = _make_parser()
240 store = _make_store()
241 source = b"Repo::save"
242 callee = _text_node(b"Repo::save")
243 call_node = MockNode("call_expression")
244 call_node.set_field("function", callee)
245 body = MockNode("block", children=[call_node])
246 fn_node = MockNode("function_item")
247 fn_node.set_field("body", body)
248 stats = {"functions": 0, "classes": 0, "edges": 0}
249 parser._extract_calls(fn_node, source, "lib.rs", "foo",
250 NodeLabel.Function, store, stats)
251 # "Repo::save" → callee = "save"
252 edge_call = store.create_edge.call_args[0]
253 assert edge
--- a/tests/test_typescript_parser.py
+++ b/tests/test_typescript_parser.py
@@ -0,0 +1,270 @@
1
+"""Tests for navegador.ingestion.typescript — TypeScriptParser internal methods."""
2
+
3
+from unittest.mock import MagicMock, patch
4
+
5
+import pytest
6
+
7
+from navegador.graph.schema import NodeLabel
8
+
9
+
10
+class MockNode:
11
+ _id_counter = 0
12
+
13
+ def __init__(self, type_: str, text: bytes = b"", children: list = None,
14
+ start_byte: int = 0, end_byte: int = 0,
15
+ start_point: tuple = (0, 0), end_point: tuple = (0, 0),
16
+ parent=None):
17
+ MockNode._id_counter += 1
18
+ self.id = MockNode._id_counter
19
+ self.type = type_
20
+ self.children = children or []
21
+ self.start_byte = start_byte
22
+ self.end_byte = end_byte
23
+ self.start_point = start_point
24
+ self.end_point = end_point
25
+ self.parent = parent
26
+ self._fields: dict = {}
27
+ for child in self.children:
28
+ child.parent = self
29
+
30
+ def child_by_field_name(self, name: str):
31
+ return self._fields.get(name)
32
+
33
+ def set_field(self, name: str, node):
34
+ self._fields[name] = node
35
+ node.parent = self
36
+ return self
37
+
38
+
39
+def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
40
+ return MockNode(type_, text, start_byte=0, end_byte=len(text))
41
+
42
+
43
+def _make_store():
44
+ store = MagicMock()
45
+ store.query.return_value = MagicMock(result_set=[])
46
+ return store
47
+
48
+
49
+def _make_parser(language: str = "typescript"):
50
+ from navegador.ingestion.typescript import TypeScriptParser
51
+ parser = TypeScriptParser.__new__(TypeScriptParser)
52
+ parser._parser = MagicMock()
53
+ parser._language = language
54
+ return parser
55
+
56
+
57
+class TestTsGetLanguage:
58
+ def test_raises_when_ts_not_installed(self):
59
+ from navegador.ingestion.typescript import _get_ts_language
60
+ with patch.dict("sys.modules", {"tree_sitter_typescript": None, "tree_sitter": None}):
61
+ with pytest.raises(ImportError, match="tree-sitter-typescript"):
62
+ _get_ts_language("typescript")
63
+
64
+ def test_raises_when_js_not_installed(self):
65
+ from navegador.ingestion.typescript import _get_ts_language
66
+ with patch.dict("sys.modules", {"tree_sitter_javascript": None, "tree_sitter": None}):
67
+ with pytest.raises(ImportError, match="tree-sitter-javascript"):
68
+ _get_ts_language("javascript")
69
+
70
+
71
+class TestTsNodeText:
72
+ def test_extracts_text(self):
73
+ from navegador.ingestion.typescript import _node_text
74
+ source = b"class Foo {}"
75
+ node = MockNode("type_identifier", start_byte=6, end_byte=9)
76
+ assert _node_text(node, source) == "Foo"
77
+
78
+
79
+class TestTsJsdoc:
80
+ def test_extracts_jsdoc(self):
81
+ from navegador.ingestion.typescript import _jsdoc
82
+ source = b"/** My class */\nclass Foo {}"
83
+ comment = MockNode("comment", start_byte=0, end_byte=15)
84
+ cls_node = MockNode("class_declaration", start_byte=16, end_byte=28)
85
+ MockNode("program", children=[comment, cls_node])
86
+ result = _jsdoc(cls_node, source)
87
+ assert "My class" in result
88
+
89
+ def test_ignores_single_line_comment(self):
90
+ from navegador.ingestion.typescript import _jsdoc
91
+ source = b"// not jsdoc\nclass Foo {}"
92
+ comment = MockNode("comment", start_byte=0, end_byte=12)
93
+ cls_node = MockNode("class_declaration", start_byte=13, end_byte=25)
94
+ MockNode("program", children=[comment, cls_node])
95
+ result = _jsdoc(cls_node, source)
96
+ assert result == ""
97
+
98
+
99
+class TestTsHandleClass:
100
+ def test_creates_class_node(self):
101
+ parser = _make_parser()
102
+ store = _make_store()
103
+ source = b"class Foo {}"
104
+ name_node = _text_node(b"Foo", "type_identifier")
105
+ body = MockNode("class_body")
106
+ node = MockNode("class_declaration",
107
+ children=[name_node, body],
108
+ start_point=(0, 0), end_point=(0, 11))
109
+ MockNode("program", children=[node])
110
+ stats = {"functions": 0, "classes": 0, "edges": 0}
111
+ parser._handle_class(node, source, "app.ts", store, stats)
112
+ assert stats["classes"] == 1
113
+ label = store.create_node.call_args[0][0]
114
+ assert label == NodeLabel.Class
115
+
116
+ def test_creates_inherits_edge(self):
117
+ parser = _make_parser()
118
+ store = _make_store()
119
+ source = b"class Child extends Parent {}"
120
+ name_node = _text_node(b"Child", "type_identifier")
121
+ parent_id = _text_node(b"Parent")
122
+ extends = MockNode("extends_clause", children=[parent_id])
123
+ heritage = MockNode("class_heritage", children=[extends])
124
+ body = MockNode("class_body")
125
+ node = MockNode("class_declaration",
126
+ children=[name_node, heritage, body],
127
+ start_point=(0, 0), end_point=(0, 28))
128
+ MockNode("program", children=[node])
129
+ stats = {"functions": 0, "classes": 0, "edges": 0}
130
+ parser._handle_class(node, source, "app.ts", store, stats)
131
+ assert stats["edges"] == 2 # CONTAINS + INHERITS
132
+
133
+ def test_ingests_methods_in_body(self):
134
+ parser = _make_parser()
135
+ store = _make_store()
136
+ source = b"class Foo { bar() {} }"
137
+ name_node = _text_node(b"Foo", "type_identifier")
138
+ method_name = _text_node(b"bar", "property_identifier")
139
+ method_body = MockNode("statement_block")
140
+ method = MockNode("method_definition",
141
+ children=[method_name, method_body],
142
+ start_point=(1, 2), end_point=(1, 9))
143
+ body = MockNode("class_body", children=[method])
144
+ node = MockNode("class_declaration",
145
+ children=[name_node, body],
146
+ start_point=(0, 0), end_point=(0, 21))
147
+ MockNode("program", children=[node])
148
+ stats = {"functions": 0, "classes": 0, "edges": 0}
149
+ parser._handle_class(node, source, "app.ts", store, stats)
150
+ assert stats["functions"] == 1
151
+
152
+ def test_skips_if_no_name(self):
153
+ parser = _make_parser()
154
+ store = _make_store()
155
+ node = MockNode("class_declaration", children=[])
156
+ stats = {"functions": 0, "classes": 0, "edges": 0}
157
+ parser._handle_class(node, b"", "app.ts", store, stats)
158
+ assert stats["classes"] == 0
159
+
160
+
161
+class TestTsHandleInterface:
162
+ def test_creates_interface_node(self):
163
+ parser = _make_parser()
164
+ store = _make_store()
165
+ source = b"interface IFoo {}"
166
+ name_node = _text_node(b"IFoo", "type_identifier")
167
+ node = MockNode("interface_declaration",
168
+ children=[name_node],
169
+ start_point=(0, 0), end_point=(0, 16))
170
+ MockNode("program", children=[node])
171
+ stats = {"functions": 0, "classes": 0, "edges": 0}
172
+ parser._handle_interface(node, source, "app.ts", store, stats)
173
+ assert stats["classes"] == 1
174
+ props = store.create_node.call_args[0][1]
175
+ assert "interface" in props.get("docstring", "")
176
+
177
+ def test_skips_if_no_name(self):
178
+ parser = _make_parser()
179
+ store = _make_store()
180
+ node = MockNode("interface_declaration", children=[])
181
+ stats = {"functions": 0, "classes": 0, "edges": 0}
182
+ parser._handle_interface(node, b"", "app.ts", store, stats)
183
+ assert stats["classes"] == 0
184
+
185
+
186
+class TestTsHandleFunction:
187
+ def test_creates_function_node(self):
188
+ parser = _make_parser()
189
+ store = _make_store()
190
+ source = b"function foo() {}"
191
+ name_node = _text_node(b"foo")
192
+ body = MockNode("statement_block")
193
+ node = MockNode("function_declaration",
194
+ children=[name_node, body],
195
+ start_point=(0, 0), end_point=(0, 16))
196
+ MockNode("program", children=[node])
197
+ stats = {"functions": 0, "classes": 0, "edges": 0}
198
+ parser._handle_function(node, source, "app.ts", store, stats, class_name=None)
199
+ assert stats["functions"] == 1
200
+ label = store.create_node.call_args[0][0]
201
+ assert label == NodeLabel.Function
202
+
203
+ def test_creates_method_when_class_name_given(self):
204
+ parser = _make_parser()
205
+ store = _make_store()
206
+ source = b"foo() {}"
207
+ name_node = _text_node(b"foo", "property_identifier")
208
+ body = MockNode("statement_block")
209
+ node = MockNode("method_definition",
210
+ children=[name_node, body],
211
+ start_point=(0, 0), end_point=(0, 7))
212
+ stats = {"functions": 0, "classes": 0, "edges": 0}
213
+ parser._handle_function(node, source, "app.ts", store, stats, class_name="Bar")
214
+ label = store.create_node.call_args[0][0]
215
+ assert label == NodeLabel.Method
216
+
217
+ def test_skips_if_no_name(self):
218
+ parser = _make_parser()
219
+ store = _make_store()
220
+ node = MockNode("function_declaration", children=[])
221
+ stats = {"functions": 0, "classes": 0, "edges": 0}
222
+ parser._handle_function(node, b"", "app.ts", store, stats, class_name=None)
223
+ assert stats["functions"] == 0
224
+
225
+
226
+class TestTsHandleLexical:
227
+ def test_ingests_const_arrow_function(self):
228
+ parser = _make_parser()
229
+ store = _make_store()
230
+ source = b"const foo = () => {}"
231
+ name_node = _text_node(b"foo")
232
+ arrow = MockNode("arrow_function", start_point=(1, 0), end_point=(1, 7))
233
+ declarator = MockNode("variable_declarator")
234
+ declarator.set_field("name", name_node)
235
+ declarator.set_field("value", arrow)
236
+ node = MockNode("lexical_declaration",
237
+ children=[declarator],
238
+ start_point=(0, 0), end_point=(0, 19))
239
+ MockNode("program", children=[node])
240
+ stats = {"functions": 0, "classes": 0, "edges": 0}
241
+ parser._handle_lexical(node, source, "app.ts", store, stats)
242
+ assert stats["functions"] == 1
243
+
244
+ def test_ingests_const_function_expression(self):
245
+ parser = _make_parser()
246
+ store = _make_store()
247
+ source = b"const bar = function() {}"
248
+ name_node = _text_node(b"bar")
249
+ fn_expr = MockNode("function_expression")
250
+ declarator = MockNode("variable_declarator")
251
+ declarator.set_field("function foo() { bar() }e", name_node)
252
+ declarator.set_field("value", fn_expr)
253
+ node = MockNode("lexical_declaration",
254
+ children=[declarator],
255
+ start_point=(0, 0), end_point=(0, 24))
256
+ MockNode("program", children=[node])
257
+ stats = {"functions": 0, "classes": 0, "edges": 0}
258
+ parser._handle_lexical(node, source, "app.ts", store, stats)
259
+ assert stats["functions"] == 1
260
+
261
+ def test_skips_non_functioe_call[4]["name"] == "bar"
262
+
263
+ def test_no_calls_in_empty_body(self):
264
+ parser = _make_parser()
265
+ store = _make_store()
266
+ fn_node = MockNode("function_declaration",
267
+ children=[MockNode("statement_block")])
268
+ stats = {"functions": 0, "classes": 0, "edges": 0}
269
+ parser._extract_calls(fn_node, b"", "app.ts", "foo",
270
+ NodeLabel.Funct
--- a/tests/test_typescript_parser.py
+++ b/tests/test_typescript_parser.py
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_typescript_parser.py
+++ b/tests/test_typescript_parser.py
@@ -0,0 +1,270 @@
1 """Tests for navegador.ingestion.typescript — TypeScriptParser internal methods."""
2
3 from unittest.mock import MagicMock, patch
4
5 import pytest
6
7 from navegador.graph.schema import NodeLabel
8
9
10 class MockNode:
11 _id_counter = 0
12
13 def __init__(self, type_: str, text: bytes = b"", children: list = None,
14 start_byte: int = 0, end_byte: int = 0,
15 start_point: tuple = (0, 0), end_point: tuple = (0, 0),
16 parent=None):
17 MockNode._id_counter += 1
18 self.id = MockNode._id_counter
19 self.type = type_
20 self.children = children or []
21 self.start_byte = start_byte
22 self.end_byte = end_byte
23 self.start_point = start_point
24 self.end_point = end_point
25 self.parent = parent
26 self._fields: dict = {}
27 for child in self.children:
28 child.parent = self
29
30 def child_by_field_name(self, name: str):
31 return self._fields.get(name)
32
33 def set_field(self, name: str, node):
34 self._fields[name] = node
35 node.parent = self
36 return self
37
38
39 def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
40 return MockNode(type_, text, start_byte=0, end_byte=len(text))
41
42
43 def _make_store():
44 store = MagicMock()
45 store.query.return_value = MagicMock(result_set=[])
46 return store
47
48
49 def _make_parser(language: str = "typescript"):
50 from navegador.ingestion.typescript import TypeScriptParser
51 parser = TypeScriptParser.__new__(TypeScriptParser)
52 parser._parser = MagicMock()
53 parser._language = language
54 return parser
55
56
57 class TestTsGetLanguage:
58 def test_raises_when_ts_not_installed(self):
59 from navegador.ingestion.typescript import _get_ts_language
60 with patch.dict("sys.modules", {"tree_sitter_typescript": None, "tree_sitter": None}):
61 with pytest.raises(ImportError, match="tree-sitter-typescript"):
62 _get_ts_language("typescript")
63
64 def test_raises_when_js_not_installed(self):
65 from navegador.ingestion.typescript import _get_ts_language
66 with patch.dict("sys.modules", {"tree_sitter_javascript": None, "tree_sitter": None}):
67 with pytest.raises(ImportError, match="tree-sitter-javascript"):
68 _get_ts_language("javascript")
69
70
71 class TestTsNodeText:
72 def test_extracts_text(self):
73 from navegador.ingestion.typescript import _node_text
74 source = b"class Foo {}"
75 node = MockNode("type_identifier", start_byte=6, end_byte=9)
76 assert _node_text(node, source) == "Foo"
77
78
79 class TestTsJsdoc:
80 def test_extracts_jsdoc(self):
81 from navegador.ingestion.typescript import _jsdoc
82 source = b"/** My class */\nclass Foo {}"
83 comment = MockNode("comment", start_byte=0, end_byte=15)
84 cls_node = MockNode("class_declaration", start_byte=16, end_byte=28)
85 MockNode("program", children=[comment, cls_node])
86 result = _jsdoc(cls_node, source)
87 assert "My class" in result
88
89 def test_ignores_single_line_comment(self):
90 from navegador.ingestion.typescript import _jsdoc
91 source = b"// not jsdoc\nclass Foo {}"
92 comment = MockNode("comment", start_byte=0, end_byte=12)
93 cls_node = MockNode("class_declaration", start_byte=13, end_byte=25)
94 MockNode("program", children=[comment, cls_node])
95 result = _jsdoc(cls_node, source)
96 assert result == ""
97
98
99 class TestTsHandleClass:
100 def test_creates_class_node(self):
101 parser = _make_parser()
102 store = _make_store()
103 source = b"class Foo {}"
104 name_node = _text_node(b"Foo", "type_identifier")
105 body = MockNode("class_body")
106 node = MockNode("class_declaration",
107 children=[name_node, body],
108 start_point=(0, 0), end_point=(0, 11))
109 MockNode("program", children=[node])
110 stats = {"functions": 0, "classes": 0, "edges": 0}
111 parser._handle_class(node, source, "app.ts", store, stats)
112 assert stats["classes"] == 1
113 label = store.create_node.call_args[0][0]
114 assert label == NodeLabel.Class
115
116 def test_creates_inherits_edge(self):
117 parser = _make_parser()
118 store = _make_store()
119 source = b"class Child extends Parent {}"
120 name_node = _text_node(b"Child", "type_identifier")
121 parent_id = _text_node(b"Parent")
122 extends = MockNode("extends_clause", children=[parent_id])
123 heritage = MockNode("class_heritage", children=[extends])
124 body = MockNode("class_body")
125 node = MockNode("class_declaration",
126 children=[name_node, heritage, body],
127 start_point=(0, 0), end_point=(0, 28))
128 MockNode("program", children=[node])
129 stats = {"functions": 0, "classes": 0, "edges": 0}
130 parser._handle_class(node, source, "app.ts", store, stats)
131 assert stats["edges"] == 2 # CONTAINS + INHERITS
132
133 def test_ingests_methods_in_body(self):
134 parser = _make_parser()
135 store = _make_store()
136 source = b"class Foo { bar() {} }"
137 name_node = _text_node(b"Foo", "type_identifier")
138 method_name = _text_node(b"bar", "property_identifier")
139 method_body = MockNode("statement_block")
140 method = MockNode("method_definition",
141 children=[method_name, method_body],
142 start_point=(1, 2), end_point=(1, 9))
143 body = MockNode("class_body", children=[method])
144 node = MockNode("class_declaration",
145 children=[name_node, body],
146 start_point=(0, 0), end_point=(0, 21))
147 MockNode("program", children=[node])
148 stats = {"functions": 0, "classes": 0, "edges": 0}
149 parser._handle_class(node, source, "app.ts", store, stats)
150 assert stats["functions"] == 1
151
152 def test_skips_if_no_name(self):
153 parser = _make_parser()
154 store = _make_store()
155 node = MockNode("class_declaration", children=[])
156 stats = {"functions": 0, "classes": 0, "edges": 0}
157 parser._handle_class(node, b"", "app.ts", store, stats)
158 assert stats["classes"] == 0
159
160
161 class TestTsHandleInterface:
162 def test_creates_interface_node(self):
163 parser = _make_parser()
164 store = _make_store()
165 source = b"interface IFoo {}"
166 name_node = _text_node(b"IFoo", "type_identifier")
167 node = MockNode("interface_declaration",
168 children=[name_node],
169 start_point=(0, 0), end_point=(0, 16))
170 MockNode("program", children=[node])
171 stats = {"functions": 0, "classes": 0, "edges": 0}
172 parser._handle_interface(node, source, "app.ts", store, stats)
173 assert stats["classes"] == 1
174 props = store.create_node.call_args[0][1]
175 assert "interface" in props.get("docstring", "")
176
177 def test_skips_if_no_name(self):
178 parser = _make_parser()
179 store = _make_store()
180 node = MockNode("interface_declaration", children=[])
181 stats = {"functions": 0, "classes": 0, "edges": 0}
182 parser._handle_interface(node, b"", "app.ts", store, stats)
183 assert stats["classes"] == 0
184
185
186 class TestTsHandleFunction:
187 def test_creates_function_node(self):
188 parser = _make_parser()
189 store = _make_store()
190 source = b"function foo() {}"
191 name_node = _text_node(b"foo")
192 body = MockNode("statement_block")
193 node = MockNode("function_declaration",
194 children=[name_node, body],
195 start_point=(0, 0), end_point=(0, 16))
196 MockNode("program", children=[node])
197 stats = {"functions": 0, "classes": 0, "edges": 0}
198 parser._handle_function(node, source, "app.ts", store, stats, class_name=None)
199 assert stats["functions"] == 1
200 label = store.create_node.call_args[0][0]
201 assert label == NodeLabel.Function
202
203 def test_creates_method_when_class_name_given(self):
204 parser = _make_parser()
205 store = _make_store()
206 source = b"foo() {}"
207 name_node = _text_node(b"foo", "property_identifier")
208 body = MockNode("statement_block")
209 node = MockNode("method_definition",
210 children=[name_node, body],
211 start_point=(0, 0), end_point=(0, 7))
212 stats = {"functions": 0, "classes": 0, "edges": 0}
213 parser._handle_function(node, source, "app.ts", store, stats, class_name="Bar")
214 label = store.create_node.call_args[0][0]
215 assert label == NodeLabel.Method
216
217 def test_skips_if_no_name(self):
218 parser = _make_parser()
219 store = _make_store()
220 node = MockNode("function_declaration", children=[])
221 stats = {"functions": 0, "classes": 0, "edges": 0}
222 parser._handle_function(node, b"", "app.ts", store, stats, class_name=None)
223 assert stats["functions"] == 0
224
225
226 class TestTsHandleLexical:
227 def test_ingests_const_arrow_function(self):
228 parser = _make_parser()
229 store = _make_store()
230 source = b"const foo = () => {}"
231 name_node = _text_node(b"foo")
232 arrow = MockNode("arrow_function", start_point=(1, 0), end_point=(1, 7))
233 declarator = MockNode("variable_declarator")
234 declarator.set_field("name", name_node)
235 declarator.set_field("value", arrow)
236 node = MockNode("lexical_declaration",
237 children=[declarator],
238 start_point=(0, 0), end_point=(0, 19))
239 MockNode("program", children=[node])
240 stats = {"functions": 0, "classes": 0, "edges": 0}
241 parser._handle_lexical(node, source, "app.ts", store, stats)
242 assert stats["functions"] == 1
243
244 def test_ingests_const_function_expression(self):
245 parser = _make_parser()
246 store = _make_store()
247 source = b"const bar = function() {}"
248 name_node = _text_node(b"bar")
249 fn_expr = MockNode("function_expression")
250 declarator = MockNode("variable_declarator")
251 declarator.set_field("function foo() { bar() }e", name_node)
252 declarator.set_field("value", fn_expr)
253 node = MockNode("lexical_declaration",
254 children=[declarator],
255 start_point=(0, 0), end_point=(0, 24))
256 MockNode("program", children=[node])
257 stats = {"functions": 0, "classes": 0, "edges": 0}
258 parser._handle_lexical(node, source, "app.ts", store, stats)
259 assert stats["functions"] == 1
260
261 def test_skips_non_functioe_call[4]["name"] == "bar"
262
263 def test_no_calls_in_empty_body(self):
264 parser = _make_parser()
265 store = _make_store()
266 fn_node = MockNode("function_declaration",
267 children=[MockNode("statement_block")])
268 stats = {"functions": 0, "classes": 0, "edges": 0}
269 parser._extract_calls(fn_node, b"", "app.ts", "foo",
270 NodeLabel.Funct

Keyboard Shortcuts

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