Navegador

feat: language expansion — Kotlin, C#, PHP, Ruby, Swift, C, C++ Seven new tree-sitter parsers with graceful ImportError when grammar packages aren't installed. Updated LANGUAGE_MAP and _get_parser(). Closes #1

lmata 2026-03-23 05:21 trunk
Commit fa82b9508089ab6402855bdd0bd58c62e440bbe575d4c365b7a82eb2f43e7b62
--- a/navegador/ingestion/c.py
+++ b/navegador/ingestion/c.py
@@ -0,0 +1,179 @@
1
+"""
2
+C AST parser — extracts functions, structs, typedefs, and #include directives
3
+from .c and .h 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_c_language():
17
+ try:
18
+ import tree_sitter_c as tsc # type: ignore[import]
19
+ from tree_sitter import Language
20
+
21
+ return Language(tsc.language())
22
+ except ImportError as e:
23
+ raise ImportError(
24
+es, file_pat"Install tree-sitter-crror("Install tree-sitter-c:
25
+ ) from e
26
+
27
+
28
+def _node_text(node, source: bytes) -> str:
29
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
+
31
+
32
+class CParser(LanguageParser):
33
+ """Parses C source files into the navegador graph."""
34
+
35
+ def __init__(self) -> None:
36
+ from tree_sitter import Parser # type: ignore[import]
37
+
38
+ self._parser = Parser(_get_c_language())
39
+
40
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41
+ source = path.read_bytes()
42
+ tree = self._parser.parse(source)
43
+ rel_path = str(path.relative_to(repo_root))
44
+
45
+ store.create_node(
46
+ NodeLabel.File,
47
+ {
48
+ "name": path.name,
49
+ "path": rel_path,
50
+ "language": "c",
51
+ "line_count": source.count(b"\n"),
52
+ },
53
+ )
54
+
55
+ stats = {"functions": 0, "classes": 0, "edges": 0}
56
+ self._walk(tree.root_node, source, rel_path, store, stats)
57
+ return stats
58
+
59
+ # ── AST walker ────────────────────────────────────────────────────────────
60
+
61
+ def _walk(
62
+ self,
63
+ node,
64
+ source: bytes,
65
+ file_path: str,
66
+ store: GraphStore,
67
+ stats: dict,
68
+ ) -> None:
69
+ if node.type == "function_definition":
70
+ self._handle_function(node, source, file_path, store, stats)
71
+ return
72
+ if node.type in ("struct_specifier", "union_specifier", "enum_specifier"):
73
+ self._handle_struct(node, source, file_path, store, stats)
74
+ return
75
+ if node.type == "preproc_include":
76
+ self._handle_include(node, source, file_path, store, stats)
77
+ return
78
+ for child in node.children:
79
+ self._walk(child, source, file_path, store, stats)
80
+
81
+ # ── Handlers ──────────────────────────────────────────────────────────────
82
+
83
+ def _handle_function(
84
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
85
+ ) -> None:
86
+ # function_definition: type declarator body
87
+ # declarator may be a function_declarator with a name field
88
+ declarator = node.child_by_field_name("declarator")
89
+ name = self._extract_function_name(declarator, source)
90
+ if not name:
91
+ return
92
+
93
+ store.create_node(
94
+ NodeLabel.Function,
95
+ {
96
+ "name": name,
97
+ "file_path": file_path,
98
+ "line_start": node.start_point[0] + 1,
99
+ "line_end": node.end_point[0] + 1,
100
+ "docstring": "",
101
+ "class_name": "",
102
+ },
103
+ )
104
+ store.create_edge(
105
+ NodeLabel.File,
106
+ {"path": file_path},
107
+ EdgeType.CONTAINS,
108
+ NodeLabel.Function,
109
+ {"name": name, "file_path": file_path},
110
+ )
111
+ stats["functions"] += 1
112
+ stats["edges"] += 1
113
+
114
+ self._extract_calls(node, source, file_path, name, store, stats)
115
+
116
+ def _extract_function_name(self, declarator, source: bytes) -> str | None:
117
+ """Recursively dig through declarator nodes to find the function name."""
118
+ if declarator is None:
119
+ return None
120
+ if declarator.type == "identifier":
121
+ return _node_text(declarator, source)
122
+ if declarator.type == "function_declarator":
123
+ inner = declarator.child_by_field_name("declarator")
124
+ return self._extract_function_name(inner, source)
125
+ if declarator.type == "pointer_declarator":
126
+ inner = declarator.child_by_field_name("declarator")
127
+ return self._extract_function_name(inner, source)
128
+ # Fallback: look for identifier child
129
+ for child in declarator.children:
130
+ if child.type == "identifier":
131
+ return _node_text(child, source)
132
+ return None
133
+
134
+ def _handle_struct(
135
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
136
+ ) -> None:
137
+ name_node = node.child_by_field_name("name")
138
+ if not name_node:
139
+ name_node = next((c for c in node.children if c.type == "type_identifier"), None)
140
+ if not name_node:
141
+ return
142
+ name = _node_text(name_nod"struct" if node.type == "struct_specifier" else (
143
+ : dict
144
+ "union" if node.type == "u
145
+ )
146
+ store.create_ot))
147
+
148
+ store.create_nClass,
149
+ "name": p not name_node:
150
+ �────── self, node, source: bytes "docstring": kind,
151
+ },
152
+ )
153
+ store.create_edge(
154
+ NodeLabel.File,
155
+ {"path": file_path},
156
+ EdgeType.CONTAINS,
157
+ NodeLabel.Class,
158
+ {"name": name, "file_path": file_path},
159
+ )
160
+ stats["classes"] += 1
161
+ stats["edges"] += 1
162
+
163
+ def _handle_include(
164
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
165
+ ) -> None:
166
+ path_node = node.child_by_field_name("path")
167
+ if not path_node:
168
+ path_node = next(
169
+ (c for c in node.children if c.type in ("string_literal", "system_lib_string")),
170
+ None,
171
+ )
172
+ if not path_node:
173
+ return
174
+ module = _node_text(path_node, source).strip('<>"')
175
+ store.create_node(
176
+ NodeLabel.Import,
177
+ {
178
+ "name": module,
179
+
--- a/navegador/ingestion/c.py
+++ b/navegador/ingestion/c.py
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/c.py
+++ b/navegador/ingestion/c.py
@@ -0,0 +1,179 @@
1 """
2 C AST parser — extracts functions, structs, typedefs, and #include directives
3 from .c and .h 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_c_language():
17 try:
18 import tree_sitter_c as tsc # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tsc.language())
22 except ImportError as e:
23 raise ImportError(
24 es, file_pat"Install tree-sitter-crror("Install tree-sitter-c:
25 ) from e
26
27
28 def _node_text(node, source: bytes) -> str:
29 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
31
32 class CParser(LanguageParser):
33 """Parses C source files into the navegador graph."""
34
35 def __init__(self) -> None:
36 from tree_sitter import Parser # type: ignore[import]
37
38 self._parser = Parser(_get_c_language())
39
40 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41 source = path.read_bytes()
42 tree = self._parser.parse(source)
43 rel_path = str(path.relative_to(repo_root))
44
45 store.create_node(
46 NodeLabel.File,
47 {
48 "name": path.name,
49 "path": rel_path,
50 "language": "c",
51 "line_count": source.count(b"\n"),
52 },
53 )
54
55 stats = {"functions": 0, "classes": 0, "edges": 0}
56 self._walk(tree.root_node, source, rel_path, store, stats)
57 return stats
58
59 # ── AST walker ────────────────────────────────────────────────────────────
60
61 def _walk(
62 self,
63 node,
64 source: bytes,
65 file_path: str,
66 store: GraphStore,
67 stats: dict,
68 ) -> None:
69 if node.type == "function_definition":
70 self._handle_function(node, source, file_path, store, stats)
71 return
72 if node.type in ("struct_specifier", "union_specifier", "enum_specifier"):
73 self._handle_struct(node, source, file_path, store, stats)
74 return
75 if node.type == "preproc_include":
76 self._handle_include(node, source, file_path, store, stats)
77 return
78 for child in node.children:
79 self._walk(child, source, file_path, store, stats)
80
81 # ── Handlers ──────────────────────────────────────────────────────────────
82
83 def _handle_function(
84 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
85 ) -> None:
86 # function_definition: type declarator body
87 # declarator may be a function_declarator with a name field
88 declarator = node.child_by_field_name("declarator")
89 name = self._extract_function_name(declarator, source)
90 if not name:
91 return
92
93 store.create_node(
94 NodeLabel.Function,
95 {
96 "name": name,
97 "file_path": file_path,
98 "line_start": node.start_point[0] + 1,
99 "line_end": node.end_point[0] + 1,
100 "docstring": "",
101 "class_name": "",
102 },
103 )
104 store.create_edge(
105 NodeLabel.File,
106 {"path": file_path},
107 EdgeType.CONTAINS,
108 NodeLabel.Function,
109 {"name": name, "file_path": file_path},
110 )
111 stats["functions"] += 1
112 stats["edges"] += 1
113
114 self._extract_calls(node, source, file_path, name, store, stats)
115
116 def _extract_function_name(self, declarator, source: bytes) -> str | None:
117 """Recursively dig through declarator nodes to find the function name."""
118 if declarator is None:
119 return None
120 if declarator.type == "identifier":
121 return _node_text(declarator, source)
122 if declarator.type == "function_declarator":
123 inner = declarator.child_by_field_name("declarator")
124 return self._extract_function_name(inner, source)
125 if declarator.type == "pointer_declarator":
126 inner = declarator.child_by_field_name("declarator")
127 return self._extract_function_name(inner, source)
128 # Fallback: look for identifier child
129 for child in declarator.children:
130 if child.type == "identifier":
131 return _node_text(child, source)
132 return None
133
134 def _handle_struct(
135 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
136 ) -> None:
137 name_node = node.child_by_field_name("name")
138 if not name_node:
139 name_node = next((c for c in node.children if c.type == "type_identifier"), None)
140 if not name_node:
141 return
142 name = _node_text(name_nod"struct" if node.type == "struct_specifier" else (
143 : dict
144 "union" if node.type == "u
145 )
146 store.create_ot))
147
148 store.create_nClass,
149 "name": p not name_node:
150 �────── self, node, source: bytes "docstring": kind,
151 },
152 )
153 store.create_edge(
154 NodeLabel.File,
155 {"path": file_path},
156 EdgeType.CONTAINS,
157 NodeLabel.Class,
158 {"name": name, "file_path": file_path},
159 )
160 stats["classes"] += 1
161 stats["edges"] += 1
162
163 def _handle_include(
164 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
165 ) -> None:
166 path_node = node.child_by_field_name("path")
167 if not path_node:
168 path_node = next(
169 (c for c in node.children if c.type in ("string_literal", "system_lib_string")),
170 None,
171 )
172 if not path_node:
173 return
174 module = _node_text(path_node, source).strip('<>"')
175 store.create_node(
176 NodeLabel.Import,
177 {
178 "name": module,
179
--- a/navegador/ingestion/cpp.py
+++ b/navegador/ingestion/cpp.py
@@ -0,0 +1,130 @@
1
+ ST parser — extracts clas"""
2
+C++ Aip install tree-sitter-cpp") from e
3
+
4
+
5
+def _node_text(node, source: bytes) -> str:
6
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
7
+
8
+
9
+class CppParser(LanguageParser):
10
+ """Parses C++ source files into the navegador graph."""
11
+
12
+ def __init__(self) -> None:
13
+ from tree_sitter import Parser # type: ignore[import]
14
+
15
+ self._parser = Parser(_get_cpp_language())
16
+
17
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
18
+ source = path.read_bytes()
19
+ tree = self._parser.parse(source)
20
+ rel_path = str(path.relative_to(repo_root))
21
+
22
+ store.create_node(
23
+ NodeLabel.File,
24
+ {
25
+ "name": path.name,
26
+ "path": rel_path,
27
+ "language": "cpp",
28
+ "line_count": source.count(b"\n"),
29
+ },
30
+ )
31
+
32
+ stats = {"functions": 0, "classes": 0, "edges": 0}
33
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
34
+ return stats
35
+
36
+ # ── AST walker ────────────────────────────────────────────────────────────
37
+
38
+ def _walk(
39
+ self,
40
+ node,
41
+ source: bytes,
42
+ file_path: str,
43
+ store: GraphStore,
44
+ stats: dict,
45
+ class_name: str | None,
46
+ ) -> None:
47
+ if node.type in ("class_specifier", "struct_specifier"):
48
+ self._handle_class(node, source, file_path, store, stats)
49
+ return
50
+ if node.type == "function_definition":
51
+ self._handle_function(node, source, file_path, store, stats, class_name)
52
+ return
53
+ if node.type == "preproc_include":
54
+ self._handle_include(node, source, file_path, store, stats)
55
+ return
56
+ if node.type == "namespace_definition":
57
+ # Recurse into namespace body
58
+ body = node.child_by_field_name("body")ath},
59
+ (c f._parser =
60
+ )
61
+ se_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
62
+ source = path.read_bytes()
63
+ tree = self._parser.parse(source)
64
+ rel_path = str(path.relative_to(repo_root))
65
+
66
+ store.create_node(
67
+ NodeLabel.File,
68
+ {
69
+ "name": path.name,
70
+ "path": rel_path,
71
+ "language": "cpp",
72
+ "line_count": source.count(b"\n"),
73
+ },
74
+ )
75
+
76
+ stats = {"functions": 0, "classes": 0, "edges": 0}
77
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
78
+ return stats
79
+
80
+ # ── AST walker ──────────────────�
81
+ (c ───�
82
+ )
83
+ if not ��───────────────────────
84
+
85
+ def _walk(
86
+ self,
87
+ node,
88
+ source: bytes,
89
+ file_path: str,
90
+ store: GraphStore,
91
+ stats: dict,
92
+ class_name: str | None,
93
+ ) -> None:
94
+ if node.type in ("class_specifier", "struct_specifier"):
95
+ self._handle_class(node, source, file_path, store, stats)
96
+ return
97
+ if node.type == "function_definition":
98
+ self._handle_function(node, source, file_path, store, stats, class_name)
99
+ return
100
+ if node.type == "preproc_include":
101
+ self._handle_include(node, source, file_path, store, stats)
102
+ return
103
+ if node.type == "namespace_definition":
104
+ # Recurse into namespace body
105
+
106
+ (c if c.type ==
107
+ )
108
+ if ions, methods,
109
+and #inclu"""
110
+C++ AST parser — extracts classes, structs, namespaces, functions, mec if c.type in ("identifier", "qualified_identifier", "field_expression")),
111
+ None,
112
+ )
113
+ if func:
114
+ callee = _node_text(func, source).split("::")[-1].split(".")[-1].split("->")[-1]
115
+ store.create_edge(
116
+ fn_label,
117
+ {"name": fn_name, "file_path": file_path},
118
+ EdgeType.CALLS,
119
+ NodeLabel.Function,
120
+ {"name": callee, "file_path": file_path},
121
+ )
122
+ stats["edges"] += 1
123
+ for child in node.children:
124
+ walk(child)
125
+
126
+ body = fn_node.child_by_field_name("body")
127
+ if not body:
128
+ body = next((c for c in fn_node.children if c.type == "compound_statement"), None)
129
+ if body:
130
+ walk(body)
--- a/navegador/ingestion/cpp.py
+++ b/navegador/ingestion/cpp.py
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/cpp.py
+++ b/navegador/ingestion/cpp.py
@@ -0,0 +1,130 @@
1 ST parser — extracts clas"""
2 C++ Aip install tree-sitter-cpp") from e
3
4
5 def _node_text(node, source: bytes) -> str:
6 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
7
8
9 class CppParser(LanguageParser):
10 """Parses C++ source files into the navegador graph."""
11
12 def __init__(self) -> None:
13 from tree_sitter import Parser # type: ignore[import]
14
15 self._parser = Parser(_get_cpp_language())
16
17 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
18 source = path.read_bytes()
19 tree = self._parser.parse(source)
20 rel_path = str(path.relative_to(repo_root))
21
22 store.create_node(
23 NodeLabel.File,
24 {
25 "name": path.name,
26 "path": rel_path,
27 "language": "cpp",
28 "line_count": source.count(b"\n"),
29 },
30 )
31
32 stats = {"functions": 0, "classes": 0, "edges": 0}
33 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
34 return stats
35
36 # ── AST walker ────────────────────────────────────────────────────────────
37
38 def _walk(
39 self,
40 node,
41 source: bytes,
42 file_path: str,
43 store: GraphStore,
44 stats: dict,
45 class_name: str | None,
46 ) -> None:
47 if node.type in ("class_specifier", "struct_specifier"):
48 self._handle_class(node, source, file_path, store, stats)
49 return
50 if node.type == "function_definition":
51 self._handle_function(node, source, file_path, store, stats, class_name)
52 return
53 if node.type == "preproc_include":
54 self._handle_include(node, source, file_path, store, stats)
55 return
56 if node.type == "namespace_definition":
57 # Recurse into namespace body
58 body = node.child_by_field_name("body")ath},
59 (c f._parser =
60 )
61 se_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
62 source = path.read_bytes()
63 tree = self._parser.parse(source)
64 rel_path = str(path.relative_to(repo_root))
65
66 store.create_node(
67 NodeLabel.File,
68 {
69 "name": path.name,
70 "path": rel_path,
71 "language": "cpp",
72 "line_count": source.count(b"\n"),
73 },
74 )
75
76 stats = {"functions": 0, "classes": 0, "edges": 0}
77 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
78 return stats
79
80 # ── AST walker ──────────────────�
81 (c ───�
82 )
83 if not ��───────────────────────
84
85 def _walk(
86 self,
87 node,
88 source: bytes,
89 file_path: str,
90 store: GraphStore,
91 stats: dict,
92 class_name: str | None,
93 ) -> None:
94 if node.type in ("class_specifier", "struct_specifier"):
95 self._handle_class(node, source, file_path, store, stats)
96 return
97 if node.type == "function_definition":
98 self._handle_function(node, source, file_path, store, stats, class_name)
99 return
100 if node.type == "preproc_include":
101 self._handle_include(node, source, file_path, store, stats)
102 return
103 if node.type == "namespace_definition":
104 # Recurse into namespace body
105
106 (c if c.type ==
107 )
108 if ions, methods,
109 and #inclu"""
110 C++ AST parser — extracts classes, structs, namespaces, functions, mec if c.type in ("identifier", "qualified_identifier", "field_expression")),
111 None,
112 )
113 if func:
114 callee = _node_text(func, source).split("::")[-1].split(".")[-1].split("->")[-1]
115 store.create_edge(
116 fn_label,
117 {"name": fn_name, "file_path": file_path},
118 EdgeType.CALLS,
119 NodeLabel.Function,
120 {"name": callee, "file_path": file_path},
121 )
122 stats["edges"] += 1
123 for child in node.children:
124 walk(child)
125
126 body = fn_node.child_by_field_name("body")
127 if not body:
128 body = next((c for c in fn_node.children if c.type == "compound_statement"), None)
129 if body:
130 walk(body)
--- a/navegador/ingestion/csharp.py
+++ b/navegador/ingestion/csharp.py
@@ -0,0 +1,204 @@
1
+"""
2
+C# AST parser — extracts classes, interfaces, structs, methods, and
3
+imports (using directives) from .cs 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_csharp_language():
17
+ try:
18
+ import tree_sitter_c_sharp as tscsharp # type: ignore[import]
19
+ from tree_sitter import Language
20
+
21
+ return Language(tscsharp.language())
22
+ except ImportError as e:
23
+
24
+ raise ImportError("Install
25
+ d_declanstall tree-sitter-c-sharp") from e
26
+
27
+
28
+def _node_text(node, source: bytes) -> str:
29
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
+
31
+
32
+class CSharpParser(LanguageParser):
33
+ """Parses C# source files into the navegador graph."""
34
+
35
+ def __init__(self) -> None:
36
+ from tree_sitter import Parser # type: ignore[import]
37
+
38
+ self._parser = Parser(_get_csharp_language())
39
+
40
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41
+ source = path.read_bytes()
42
+ tree = self._parser.parse(source)
43
+ rel_path = str(path.relative_to(repo_root))
44
+
45
+ store.create_node(
46
+ NodeLabel.File,
47
+ {
48
+ "name": path.name,
49
+ "path": rel_path,
50
+ "language": "csharp",
51
+ "line_count": source.count(b"\n"),
52
+ },
53
+ )
54
+
55
+ stats = {"functions": 0, "classes": 0, "edges": 0}
56
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
57
+ return stats
58
+
59
+ # ── AST walker ────────────────────────────────────────────────────────────
60
+
61
+ def _walk(
62
+ self,
63
+ node,
64
+ source: bytes,
65
+ file_path: str,
66
+ store: GraphStore,
67
+ stats: dict,
68
+ class_name: str | None,
69
+ ) -> None:
70
+ if node.type in (
71
+ "class_declaration",
72
+ "interface_declaration",
73
+ "struct_declaration",
74
+ "record_declaration",
75
+ ):
76
+ self._handle_class(node, source, file_path, store, stats)
77
+ return
78
+ if node.type in ("method_declaration", "constructor_declaration"):
79
+ self._handle_method(node, source, file_path, store, stats, class_name)
80
+ return
81
+ if node.type == "using_directive":
82
+ self._handle_using(node, source, file_path, store, stats)
83
+ return
84
+ for child in node.children:
85
+ self._walk(child, source, file_path, store, stats, class_name)
86
+
87
+ # ── Handlers ─────────────────────────────────�",
88
+ },: source.coundeclaration_list"), None
89
+ )
90
+ == "block"), None)
91
+ if body:
92
+ for child in body.children:
93
+ if child.type in ("method_declaration", "constructor_declaration"):
94
+ self._handle_method(child, source, file_path, store, stats, class_name=name)
95
+
96
+ def _handle_method(
97
+ self,
98
+ node,
99
+ source: bytes,
100
+ file_path: str,
101
+ store: GraphStore,
102
+ stats: dict,
103
+ class_name: str | None,
104
+ ) -> None:
105
+ name_node = node.child_by_field_name("name")
106
+ if not name_node:
107
+ name_node = next((c for c in node.children if c.type == "identifier"), None)
108
+ if not name_node:
109
+ return
110
+ name = _node_text(name_node, source)
111
+
112
+ label = NodeLabel.Method if class_name else NodeLabel.Function
113
+ store.create_node(
114
+ label,
115
+ {
116
+ "name": name,
117
+ "file_path": file_path,
118
+ "line_start": node.start_point[0] + 1,
119
+ "line_end": node.end_point[0] + 1,
120
+ "docstring": "",
121
+ "class_name": class_name or "",
122
+ },
123
+ )
124
+
125
+ container_label = NodeLabel.Class if class_name else NodeLabel.File
126
+ container_key = (
127
+ {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
128
+ )
129
+ store.create_edge(
130
+ container_label,
131
+ container_key,
132
+ EdgeType.CONTAINS,
133
+ label,
134
+ {"name": name, "file_path": file_path},
135
+ )
136
+ stats["functions"] += 1
137
+ stats["edges"] += 1
138
+
139
+ self._extract_calls(node, source, file_path, name, label, store, stats)
140
+
141
+ def _handle_using(
142
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
143
+ ) -> None:
144
+ raw = _node_text(node, source).strip()
145
+ # "using System.Collections.Generic;" or "using st(
146
+ method(child, source, file
147
+ "file_pa .removeprefix(" static")
148
+ "file_pa .removesuffix(";")
149
+ .strip()
150
+"name": name, "filod(
151
+ self,
152
+ node,
153
+ source: bytes,
154
+ file_path: str,
155
+ store: GraphStore,
156
+ stats: dict,
157
+ class_name: str | None,
158
+ ) -> None:
159
+ name_node = node.child_by_field_name("name")
160
+ if not name_node:
161
+ name_node = next((c for c in node.children if c.type == "identifier"), None)
162
+ if not name_node:
163
+ return
164
+ name = _node_text(name_node, source)
165
+
166
+ label = NodeLabel.Method if class_name else NodeLabel.Function
167
+ store.create_node(
168
+ label,
169
+ {
170
+ "name": name,
171
+ "file_path": file_path,
172
+ "line_start": node.start_point[0] + 1,
173
+ "line_end": node.end_point[0] + 1,
174
+ "docstring": "",
175
+ "class_name": class_name or "",
176
+ },
177
+ )
178
+
179
+ container_label = NodeLabel.Class if class_name else NodeLabel.File
180
+ container_key = (
181
+ {"nam source.count(b"\n"),
182
+ } container_),
183
+ EdgeType.CONTAINS,
184
+ label,
185
+ {"name": name, "file_path": file_path},
186
+ )
187
+ stats["functions"] += 1
188
+ stats["edges"] += 1
189
+
190
+ self._extract_calls(node, source, file_path, name, label, store, stats)
191
+
192
+ def _handle_using(
193
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
194
+ ) -> None:
195
+ raw = _node_text(node, source).strip()
196
+ # "using System.Collections.Generic;" or "using static ..."
197
+ module = raw.removeprefix("using").removeprefix(" static").removesuffix(";").strip()
198
+ if not module:
199
+ return
200
+ store.create_node(
201
+ NodeLabel.Import,
202
+ {
203
+ "name": module,
204
+
--- a/navegador/ingestion/csharp.py
+++ b/navegador/ingestion/csharp.py
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/csharp.py
+++ b/navegador/ingestion/csharp.py
@@ -0,0 +1,204 @@
1 """
2 C# AST parser — extracts classes, interfaces, structs, methods, and
3 imports (using directives) from .cs 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_csharp_language():
17 try:
18 import tree_sitter_c_sharp as tscsharp # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tscsharp.language())
22 except ImportError as e:
23
24 raise ImportError("Install
25 d_declanstall tree-sitter-c-sharp") from e
26
27
28 def _node_text(node, source: bytes) -> str:
29 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
31
32 class CSharpParser(LanguageParser):
33 """Parses C# source files into the navegador graph."""
34
35 def __init__(self) -> None:
36 from tree_sitter import Parser # type: ignore[import]
37
38 self._parser = Parser(_get_csharp_language())
39
40 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41 source = path.read_bytes()
42 tree = self._parser.parse(source)
43 rel_path = str(path.relative_to(repo_root))
44
45 store.create_node(
46 NodeLabel.File,
47 {
48 "name": path.name,
49 "path": rel_path,
50 "language": "csharp",
51 "line_count": source.count(b"\n"),
52 },
53 )
54
55 stats = {"functions": 0, "classes": 0, "edges": 0}
56 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
57 return stats
58
59 # ── AST walker ────────────────────────────────────────────────────────────
60
61 def _walk(
62 self,
63 node,
64 source: bytes,
65 file_path: str,
66 store: GraphStore,
67 stats: dict,
68 class_name: str | None,
69 ) -> None:
70 if node.type in (
71 "class_declaration",
72 "interface_declaration",
73 "struct_declaration",
74 "record_declaration",
75 ):
76 self._handle_class(node, source, file_path, store, stats)
77 return
78 if node.type in ("method_declaration", "constructor_declaration"):
79 self._handle_method(node, source, file_path, store, stats, class_name)
80 return
81 if node.type == "using_directive":
82 self._handle_using(node, source, file_path, store, stats)
83 return
84 for child in node.children:
85 self._walk(child, source, file_path, store, stats, class_name)
86
87 # ── Handlers ─────────────────────────────────�",
88 },: source.coundeclaration_list"), None
89 )
90 == "block"), None)
91 if body:
92 for child in body.children:
93 if child.type in ("method_declaration", "constructor_declaration"):
94 self._handle_method(child, source, file_path, store, stats, class_name=name)
95
96 def _handle_method(
97 self,
98 node,
99 source: bytes,
100 file_path: str,
101 store: GraphStore,
102 stats: dict,
103 class_name: str | None,
104 ) -> None:
105 name_node = node.child_by_field_name("name")
106 if not name_node:
107 name_node = next((c for c in node.children if c.type == "identifier"), None)
108 if not name_node:
109 return
110 name = _node_text(name_node, source)
111
112 label = NodeLabel.Method if class_name else NodeLabel.Function
113 store.create_node(
114 label,
115 {
116 "name": name,
117 "file_path": file_path,
118 "line_start": node.start_point[0] + 1,
119 "line_end": node.end_point[0] + 1,
120 "docstring": "",
121 "class_name": class_name or "",
122 },
123 )
124
125 container_label = NodeLabel.Class if class_name else NodeLabel.File
126 container_key = (
127 {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
128 )
129 store.create_edge(
130 container_label,
131 container_key,
132 EdgeType.CONTAINS,
133 label,
134 {"name": name, "file_path": file_path},
135 )
136 stats["functions"] += 1
137 stats["edges"] += 1
138
139 self._extract_calls(node, source, file_path, name, label, store, stats)
140
141 def _handle_using(
142 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
143 ) -> None:
144 raw = _node_text(node, source).strip()
145 # "using System.Collections.Generic;" or "using st(
146 method(child, source, file
147 "file_pa .removeprefix(" static")
148 "file_pa .removesuffix(";")
149 .strip()
150 "name": name, "filod(
151 self,
152 node,
153 source: bytes,
154 file_path: str,
155 store: GraphStore,
156 stats: dict,
157 class_name: str | None,
158 ) -> None:
159 name_node = node.child_by_field_name("name")
160 if not name_node:
161 name_node = next((c for c in node.children if c.type == "identifier"), None)
162 if not name_node:
163 return
164 name = _node_text(name_node, source)
165
166 label = NodeLabel.Method if class_name else NodeLabel.Function
167 store.create_node(
168 label,
169 {
170 "name": name,
171 "file_path": file_path,
172 "line_start": node.start_point[0] + 1,
173 "line_end": node.end_point[0] + 1,
174 "docstring": "",
175 "class_name": class_name or "",
176 },
177 )
178
179 container_label = NodeLabel.Class if class_name else NodeLabel.File
180 container_key = (
181 {"nam source.count(b"\n"),
182 } container_),
183 EdgeType.CONTAINS,
184 label,
185 {"name": name, "file_path": file_path},
186 )
187 stats["functions"] += 1
188 stats["edges"] += 1
189
190 self._extract_calls(node, source, file_path, name, label, store, stats)
191
192 def _handle_using(
193 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
194 ) -> None:
195 raw = _node_text(node, source).strip()
196 # "using System.Collections.Generic;" or "using static ..."
197 module = raw.removeprefix("using").removeprefix(" static").removesuffix(";").strip()
198 if not module:
199 return
200 store.create_node(
201 NodeLabel.Import,
202 {
203 "name": module,
204
--- a/navegador/ingestion/kotlin.py
+++ b/navegador/ingestion/kotlin.py
@@ -0,0 +1,198 @@
1
+"""
2
+Kotlin AST parser — extracts classes, objects, functions, methods, and
3
+imports from .kt and .kts files using tree-sitter.
4
+"""
5
+
6
+import logging
7
+from pathlib import Path
8
+
9
+from navegador.graph.schema import EdgeType, NodeLabel
10
+from navegador.graph.store import GraphStore
11
+from navegador.ingestion.parser import LanguageParser
12
+
13
+logger = logging.getLogger(__name__)
14
+
15
+
16
+def _get_kotlin_language():
17
+ try:
18
+ import tree_sitter_kotlin as tskotlin # type: ignore[import]
19
+ from tree_sitter import Language
20
+
21
+ return Language(tskotlin.language())
22
+ except ImportError as e:
23
+: node.end_p
24
+ raise ImportError("Insta
25
+.strip()install tree-sitter-kotlin") from e
26
+
27
+
28
+def _node_text(node, source: bytes) -> str:
29
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
+
31
+
32
+class KotlinParser(LanguageParser):
33
+ """Parses Kotlin source files into the navegador graph."""
34
+
35
+ def __init__(self) -> None:
36
+ from tree_sitter import Parser # type: ignore[import]
37
+
38
+ self._parser = Parser(_get_kotlin_language())
39
+
40
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41
+ source = path.read_bytes()
42
+ tree = self._parser.parse(source)
43
+ rel_path = str(path.relative_to(repo_root))
44
+
45
+ store.create_node(
46
+ NodeLabel.File,
47
+ {
48
+ "name": path
49
+ (c for c in node.children if c.type == "simple_identifier"), None
50
+.CONTAINS,
51
+ ount": source.count(b"\n"),
52
+ },
53
+ )
54
+
55
+ stats = {"functions": 0, "classes": 0, "edges": 0}
56
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
57
+ return stats
58
+
59
+ # ── AST walker ────────────────────────────────────────────────────────────
60
+
61
+ def _walk(
62
+ self,
63
+ node,
64
+ source: bytes,
65
+ file_path: str,
66
+ store: GraphStore,
67
+ stats: dict,
68
+ class_name: str | None,
69
+ ) -> None:
70
+ if node.type in ("class_declaration", "object_declaration", "interface_declaration"):
71
+ self._handle_class(node, source, file_path, store, stats)
72
+ return
73
+
74
+ """
75
+Kotlin Ae.CONTAINS,
76
+ if body:
77
+ for child in body.children:
78
+ if child.type == "function_declaration":
79
+ self._handle_function(child, source, file_path, store, stats, class_name=name)
80
+
81
+ def _handle_function(
82
+ self,
83
+ node,
84
+ source: bytes,
85
+ file_path: str,
86
+ store: GraphStore,
87
+ stats: dict,
88
+ class_name: str | None,
89
+ ) -> None:
90
+ name_node = node.child_by_field_name("name")
91
+ if not name_node:
92
+
93
+ (c for c in node.children if c.type == "simple_identifier"), None
94
+.CONTAINS,
95
+ ount": source.count(b"\n"),
96
+ },
97
+ )
98
+
99
+ stats = {"functions": 0, "clalabel = NodeLabel.Method if class_name else NodeLabel.Function
100
+ store.create_node(
101
+ label,
102
+ {
103
+ "name": name,
104
+ "file_path": file_path,
105
+ "line_start": node.start_point[0] + 1,
106
+ "line_end": node.end_point[0] + 1,
107
+ "docstring": "",
108
+ "class_name": class_name or "",
109
+ },
110
+ )
111
+
112
+ container_label = NodeLabel.Class if class_name else NodeLabel.File
113
+ container_key = (
114
+ {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
115
+ )
116
+ store.create_edge(
117
+ container_label,
118
+ container_key,
119
+ EdgeType.CONTAINS,
120
+ label,
121
+ {"name": name, "file_path": file_path},
122
+ )
123
+ stats["functions"] += 1
124
+ stats["edges"] += 1
125
+
126
+ self._extract_calls(node, source, file_path, name, label, store, stats)
127
+
128
+ def _handle_import(
129
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
130
+ ) -> None:
131
+ # import_header: "import" identifier ("." identifier)*
132
+ raw = _node_text(node, source).strip()
133
+ module = raw.removeprefix("import").strip()
134
+ if not module:
135
+ return
136
+ store.create_node(
137
+ NodeLabel.Import,
138
+ {
139
+ "name": module,
140
+ "file_path": file_path,
141
+ "line_start": node.start_point[0] + 1,
142
+ "module": module,
143
+ },
144
+ )
145
+ store.create_edge(
146
+ NodeLabel.File,
147
+ {"path": file_path},
148
+ EdgeType.IMPORTS,
149
+ NodeLabel.Import,
150
+ {"name": module, "file_path": file_path},
151
+ )
152
+ stats["edges"] += 1
153
+
154
+ def _extract_calls(
155
+ self,
156
+ fn_node,
157
+ source: bytes,
158
+ file_path: str,
159
+ fn_name: str,
160
+ fn_label: str,
161
+ store: GraphStore,
162
+ stats: dict,
163
+ ) -> None:
164
+ def walk(node):
165
+ if node.type == "call_expression":
166
+ func = node.child_by_field_name("calleeExpression")
167
+ if not func:
168
+ func = next(
169
+ (c for c in node.children if c.type }
170
+ )
171
+ st),
172
+ ,
173
+ container_key,
174
+ EdgeType.CONTAINS,
175
+ label,
176
+ {"name": name, "file_path": file_path},
177
+ )
178
+ stats["functions"] += 1
179
+ stats["edges"] += 1
180
+
181
+ self._extract_calls(node, source, file_path, name, label, store, stats)
182
+
183
+ def _handle_import(
184
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
185
+ ) -> None:
186
+ # import_header: "import" identifier ("." identifier)*
187
+ raw = _node_text(node, source).strip()
188
+ module = raw.removeprefix("import").strip()
189
+ if not module:
190
+ return
191
+ store.create_node(
192
+
193
+ {
194
+ "name": module,
195
+ "file_path": file_path
196
+.CONTAINS,
197
+ if body:
198
+ walk(body)
--- a/navegador/ingestion/kotlin.py
+++ b/navegador/ingestion/kotlin.py
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/kotlin.py
+++ b/navegador/ingestion/kotlin.py
@@ -0,0 +1,198 @@
1 """
2 Kotlin AST parser — extracts classes, objects, functions, methods, and
3 imports from .kt and .kts files using tree-sitter.
4 """
5
6 import logging
7 from pathlib import Path
8
9 from navegador.graph.schema import EdgeType, NodeLabel
10 from navegador.graph.store import GraphStore
11 from navegador.ingestion.parser import LanguageParser
12
13 logger = logging.getLogger(__name__)
14
15
16 def _get_kotlin_language():
17 try:
18 import tree_sitter_kotlin as tskotlin # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tskotlin.language())
22 except ImportError as e:
23 : node.end_p
24 raise ImportError("Insta
25 .strip()install tree-sitter-kotlin") from e
26
27
28 def _node_text(node, source: bytes) -> str:
29 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
31
32 class KotlinParser(LanguageParser):
33 """Parses Kotlin source files into the navegador graph."""
34
35 def __init__(self) -> None:
36 from tree_sitter import Parser # type: ignore[import]
37
38 self._parser = Parser(_get_kotlin_language())
39
40 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41 source = path.read_bytes()
42 tree = self._parser.parse(source)
43 rel_path = str(path.relative_to(repo_root))
44
45 store.create_node(
46 NodeLabel.File,
47 {
48 "name": path
49 (c for c in node.children if c.type == "simple_identifier"), None
50 .CONTAINS,
51 ount": source.count(b"\n"),
52 },
53 )
54
55 stats = {"functions": 0, "classes": 0, "edges": 0}
56 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
57 return stats
58
59 # ── AST walker ────────────────────────────────────────────────────────────
60
61 def _walk(
62 self,
63 node,
64 source: bytes,
65 file_path: str,
66 store: GraphStore,
67 stats: dict,
68 class_name: str | None,
69 ) -> None:
70 if node.type in ("class_declaration", "object_declaration", "interface_declaration"):
71 self._handle_class(node, source, file_path, store, stats)
72 return
73
74 """
75 Kotlin Ae.CONTAINS,
76 if body:
77 for child in body.children:
78 if child.type == "function_declaration":
79 self._handle_function(child, source, file_path, store, stats, class_name=name)
80
81 def _handle_function(
82 self,
83 node,
84 source: bytes,
85 file_path: str,
86 store: GraphStore,
87 stats: dict,
88 class_name: str | None,
89 ) -> None:
90 name_node = node.child_by_field_name("name")
91 if not name_node:
92
93 (c for c in node.children if c.type == "simple_identifier"), None
94 .CONTAINS,
95 ount": source.count(b"\n"),
96 },
97 )
98
99 stats = {"functions": 0, "clalabel = NodeLabel.Method if class_name else NodeLabel.Function
100 store.create_node(
101 label,
102 {
103 "name": name,
104 "file_path": file_path,
105 "line_start": node.start_point[0] + 1,
106 "line_end": node.end_point[0] + 1,
107 "docstring": "",
108 "class_name": class_name or "",
109 },
110 )
111
112 container_label = NodeLabel.Class if class_name else NodeLabel.File
113 container_key = (
114 {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
115 )
116 store.create_edge(
117 container_label,
118 container_key,
119 EdgeType.CONTAINS,
120 label,
121 {"name": name, "file_path": file_path},
122 )
123 stats["functions"] += 1
124 stats["edges"] += 1
125
126 self._extract_calls(node, source, file_path, name, label, store, stats)
127
128 def _handle_import(
129 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
130 ) -> None:
131 # import_header: "import" identifier ("." identifier)*
132 raw = _node_text(node, source).strip()
133 module = raw.removeprefix("import").strip()
134 if not module:
135 return
136 store.create_node(
137 NodeLabel.Import,
138 {
139 "name": module,
140 "file_path": file_path,
141 "line_start": node.start_point[0] + 1,
142 "module": module,
143 },
144 )
145 store.create_edge(
146 NodeLabel.File,
147 {"path": file_path},
148 EdgeType.IMPORTS,
149 NodeLabel.Import,
150 {"name": module, "file_path": file_path},
151 )
152 stats["edges"] += 1
153
154 def _extract_calls(
155 self,
156 fn_node,
157 source: bytes,
158 file_path: str,
159 fn_name: str,
160 fn_label: str,
161 store: GraphStore,
162 stats: dict,
163 ) -> None:
164 def walk(node):
165 if node.type == "call_expression":
166 func = node.child_by_field_name("calleeExpression")
167 if not func:
168 func = next(
169 (c for c in node.children if c.type }
170 )
171 st),
172 ,
173 container_key,
174 EdgeType.CONTAINS,
175 label,
176 {"name": name, "file_path": file_path},
177 )
178 stats["functions"] += 1
179 stats["edges"] += 1
180
181 self._extract_calls(node, source, file_path, name, label, store, stats)
182
183 def _handle_import(
184 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
185 ) -> None:
186 # import_header: "import" identifier ("." identifier)*
187 raw = _node_text(node, source).strip()
188 module = raw.removeprefix("import").strip()
189 if not module:
190 return
191 store.create_node(
192
193 {
194 "name": module,
195 "file_path": file_path
196 .CONTAINS,
197 if body:
198 walk(body)
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -7,10 +7,17 @@
77
TypeScript .ts .tsx
88
JavaScript .js .jsx
99
Go .go
1010
Rust .rs
1111
Java .java
12
+ Kotlin .kt .kts
13
+ C# .cs
14
+ PHP .php
15
+ Ruby .rb
16
+ Swift .swift
17
+ C .c .h
18
+ C++ .cpp .hpp .cc .cxx
1219
"""
1320
1421
import hashlib
1522
import logging
1623
import time
@@ -30,10 +37,22 @@
3037
".js": "javascript",
3138
".jsx": "javascript",
3239
".go": "go",
3340
".rs": "rust",
3441
".java": "java",
42
+ ".kt": "kotlin",
43
+ ".kts": "kotlin",
44
+ ".cs": "csharp",
45
+ ".php": "php",
46
+ ".rb": "ruby",
47
+ ".swift": "swift",
48
+ ".c": "c",
49
+ ".h": "c",
50
+ ".cpp": "cpp",
51
+ ".hpp": "cpp",
52
+ ".cc": "cpp",
53
+ ".cxx": "cpp",
3554
}
3655
3756
3857
class RepoIngester:
3958
"""
@@ -267,10 +286,38 @@
267286
self._parsers[language] = RustParser()
268287
elif language == "java":
269288
from navegador.ingestion.java import JavaParser
270289
271290
self._parsers[language] = JavaParser()
291
+ elif language == "kotlin":
292
+ from navegador.ingestion.kotlin import KotlinParser
293
+
294
+ self._parsers[language] = KotlinParser()
295
+ elif language == "csharp":
296
+ from navegador.ingestion.csharp import CSharpParser
297
+
298
+ self._parsers[language] = CSharpParser()
299
+ elif language == "php":
300
+ from navegador.ingestion.php import PHPParser
301
+
302
+ self._parsers[language] = PHPParser()
303
+ elif language == "ruby":
304
+ from navegador.ingestion.ruby import RubyParser
305
+
306
+ self._parsers[language] = RubyParser()
307
+ elif language == "swift":
308
+ from navegador.ingestion.swift import SwiftParser
309
+
310
+ self._parsers[language] = SwiftParser()
311
+ elif language == "c":
312
+ from navegador.ingestion.c import CParser
313
+
314
+ self._parsers[language] = CParser()
315
+ elif language == "cpp":
316
+ from navegador.ingestion.cpp import CppParser
317
+
318
+ self._parsers[language] = CppParser()
272319
else:
273320
raise ValueError(f"Unsupported language: {language}")
274321
return self._parsers[language]
275322
276323
277324
278325
ADDED navegador/ingestion/php.py
279326
ADDED navegador/ingestion/ruby.py
280327
ADDED navegador/ingestion/swift.py
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -7,10 +7,17 @@
7 TypeScript .ts .tsx
8 JavaScript .js .jsx
9 Go .go
10 Rust .rs
11 Java .java
 
 
 
 
 
 
 
12 """
13
14 import hashlib
15 import logging
16 import time
@@ -30,10 +37,22 @@
30 ".js": "javascript",
31 ".jsx": "javascript",
32 ".go": "go",
33 ".rs": "rust",
34 ".java": "java",
 
 
 
 
 
 
 
 
 
 
 
 
35 }
36
37
38 class RepoIngester:
39 """
@@ -267,10 +286,38 @@
267 self._parsers[language] = RustParser()
268 elif language == "java":
269 from navegador.ingestion.java import JavaParser
270
271 self._parsers[language] = JavaParser()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272 else:
273 raise ValueError(f"Unsupported language: {language}")
274 return self._parsers[language]
275
276
277
278 DDED navegador/ingestion/php.py
279 DDED navegador/ingestion/ruby.py
280 DDED navegador/ingestion/swift.py
--- navegador/ingestion/parser.py
+++ navegador/ingestion/parser.py
@@ -7,10 +7,17 @@
7 TypeScript .ts .tsx
8 JavaScript .js .jsx
9 Go .go
10 Rust .rs
11 Java .java
12 Kotlin .kt .kts
13 C# .cs
14 PHP .php
15 Ruby .rb
16 Swift .swift
17 C .c .h
18 C++ .cpp .hpp .cc .cxx
19 """
20
21 import hashlib
22 import logging
23 import time
@@ -30,10 +37,22 @@
37 ".js": "javascript",
38 ".jsx": "javascript",
39 ".go": "go",
40 ".rs": "rust",
41 ".java": "java",
42 ".kt": "kotlin",
43 ".kts": "kotlin",
44 ".cs": "csharp",
45 ".php": "php",
46 ".rb": "ruby",
47 ".swift": "swift",
48 ".c": "c",
49 ".h": "c",
50 ".cpp": "cpp",
51 ".hpp": "cpp",
52 ".cc": "cpp",
53 ".cxx": "cpp",
54 }
55
56
57 class RepoIngester:
58 """
@@ -267,10 +286,38 @@
286 self._parsers[language] = RustParser()
287 elif language == "java":
288 from navegador.ingestion.java import JavaParser
289
290 self._parsers[language] = JavaParser()
291 elif language == "kotlin":
292 from navegador.ingestion.kotlin import KotlinParser
293
294 self._parsers[language] = KotlinParser()
295 elif language == "csharp":
296 from navegador.ingestion.csharp import CSharpParser
297
298 self._parsers[language] = CSharpParser()
299 elif language == "php":
300 from navegador.ingestion.php import PHPParser
301
302 self._parsers[language] = PHPParser()
303 elif language == "ruby":
304 from navegador.ingestion.ruby import RubyParser
305
306 self._parsers[language] = RubyParser()
307 elif language == "swift":
308 from navegador.ingestion.swift import SwiftParser
309
310 self._parsers[language] = SwiftParser()
311 elif language == "c":
312 from navegador.ingestion.c import CParser
313
314 self._parsers[language] = CParser()
315 elif language == "cpp":
316 from navegador.ingestion.cpp import CppParser
317
318 self._parsers[language] = CppParser()
319 else:
320 raise ValueError(f"Unsupported language: {language}")
321 return self._parsers[language]
322
323
324
325 DDED navegador/ingestion/php.py
326 DDED navegador/ingestion/ruby.py
327 DDED navegador/ingestion/swift.py
--- a/navegador/ingestion/php.py
+++ b/navegador/ingestion/php.py
@@ -0,0 +1,197 @@
1
+"""
2
+PHP AST parser — extracts classes, interfaces, traits, functions, methods,
3
+and use statements from .php 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_php_language():
17
+ try:
18
+ import tree_sitter_php as tsphp # type: ignore[import]
19
+ from tree_sitter import Language
20
+
21
+ # tree-sitter-php exposes language_php() or language()
22
+ lang_fn = getattr(tsphp, "language_php", None) or getattr(tsphp, "language", None)
23
+ if lang_fn is None:
24
+ raise ImportError("tree_sitter_php has no language() or language_php() callable")
25
+ return Language(lang_fn())
26
+ except ImportError as e:
27
+ if c.type "Install tree-sitter-php: pip install tree-sitter-php"
28
+ ip install tree-sitter-php") from e
29
+
30
+
31
+def _node_text(node, source: bytes) -> str:
32
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
33
+
34
+
35
+class PHPParser(LanguageParser):
36
+ """Parses PHP source files into the navegador graph."""
37
+
38
+ def __init__(self) -> None:
39
+ from tree_sitter import Parser # type: ignore[import]
40
+
41
+ self._parser = Parser(_get_php_language())
42
+
43
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
44
+ source = path.read_bytes()
45
+ tree = self._parser.parse(source)
46
+ rel_path = str(path.relative_to(repo_root))
47
+
48
+ store.create_node(
49
+ NodeLabel.File,
50
+ {
51
+ "name": path.name,
52
+ "path": rel_path,
53
+ "language": "php",
54
+ "line_count": source.count(b"\n"),
55
+ },
56
+ )
57
+
58
+ stats = {"functions": 0, "classes": 0, "edges": 0}
59
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
60
+ return stats
61
+
62
+ # ── AST walker ────────────────────────────────────────────────────────────
63
+
64
+ def _walk(
65
+ self,
66
+ node,
67
+ source: bytes,
68
+ file_path: str,
69
+ store: GraphStore,
70
+ stats: dict,
71
+ class_name: str | None,
72
+ ) -> None:
73
+ if node.type in (
74
+ "class_declaration",
75
+ "interface_declaration",
76
+ "trait_declaration",
77
+ ):
78
+ self._handle_class(node, source, file_path, store, stats)
79
+ return
80
+ if node.type == "function_definition":
81
+ self._handle_function(node, source, file_path, store, stats, class_name)
82
+ return
83
+ if node.type == "use_declaration":
84
+ self._handle_use(node, source, file_path, store, stats)
85
+ return
86
+ for child in node.children:
87
+ self._walk(child, source, file_path, store, stats, class_name)
88
+
89
+ # ── Handlers ──────────────────────────────────────────────────────────────
90
+
91
+ def _handle_class(
92
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
93
+ ) -> None:
94
+ name_node = node.child_by_field_name("name")
95
+ if not name_node:
96
+ name_node = next((c for c in node.children if c.type == "name"), None)
97
+ if not name_node:
98
+ return
99
+ name = _node_text(name_node, source)
100
+
101
+ store.create_node(
102
+ NodeLabel.Class,
103
+ {
104
+ "name": name,
105
+ "file_path": file_path,
106
+ "line_start": node.start_point[0] + 1,
107
+ "line_end": node.end_point[0] + 1,
108
+ "docstring": "",
109
+ },
110
+ )
111
+ store.create_edge(
112
+ NodeLabel.File,
113
+ {"path": file_path},
114
+ EdgeType.CONTAINS,
115
+ NodeLabel.Class,
116
+ {"name": name, "file_path": file_path},
117
+ )
118
+ stats["classes"] += 1
119
+ stats["edges"] += 1
120
+
121
+ # Superclass
122
+ base_clause = node.child_by_field_name("base_clause")
123
+ if base_clause:
124
+ for child in base_clause.children:
125
+ if child.type in ("qualified_name", "name"):
126
+ parent_name = _node_text(child, source)
127
+ store.create_edge(
128
+ NodeLabel.Class,
129
+ {"name": name, "file_path": file_path},
130
+ EdgeType.INHERITS,
131
+ NodeLabel.Class,
132
+ {"name": parent_name, "file_path": file_path},
133
+ )
134
+ stats["edges"] += 1
135
+ break
136
+
137
+ # Walk class body for methods
138
+ body = node.child_by_field_name("body")
139
+ if not body:
140
+ body = next((c for c in node.children if c.type == "declaration_list"), None)
141
+ if body:
142
+ for child in body.children:
143
+ if child.type == "method_declaration":
144
+ self._handle_function(child, source, file_path, store, stats, class_name=name)
145
+
146
+ def _handle_function(
147
+ self,
148
+ node,
149
+ source: bytes,
150
+ file_path: str,
151
+ store: GraphStore,
152
+ stats: dict,
153
+ class_name: str | None,
154
+ ) -> None:
155
+ name_node = node.child_by_field_name("name")
156
+ if not name_node:
157
+ name_node = next((c for c in node.children if c.type == "name"), None)
158
+ if not name_node:
159
+ return
160
+ name = _node_text(name_node, source)
161
+
162
+ label = NodeLabel.Method if class_name else NodeLabel.Function
163
+ store.create_node(
164
+ label,
165
+ {
166
+ "name": name,
167
+ "file_path": file_path,
168
+ "line_start": node.start_point[0] + 1,
169
+ "line_end": node.end_point[0] + 1,
170
+ "docstring": "",
171
+ "class_name": class_name or "",
172
+ },
173
+ )
174
+
175
+ container_label = NodeLabel.Class if class_name else NodeLabel.File
176
+ container_key = (
177
+ {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
178
+ )
179
+ store.create_edge(
180
+ container_label,
181
+ container_key,
182
+ EdgeType.CONTAINS,
183
+ label,
184
+ {"name": name, "file_path": file_path},
185
+ )
186
+ stats["functions"] += 1
187
+ stats["edges"] += 1
188
+
189
+ self._extract_calls(node, source, file_path, name, label, store, stats)
190
+
191
+ def _handle_use(
192
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
193
+ ) -> None:
194
+ raw = _node_text(node, source).strip()
195
+ module = raw.removeprefix("use").removesuffix(";").strip()
196
+ if not module:
197
+ retu
--- a/navegador/ingestion/php.py
+++ b/navegador/ingestion/php.py
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/php.py
+++ b/navegador/ingestion/php.py
@@ -0,0 +1,197 @@
1 """
2 PHP AST parser — extracts classes, interfaces, traits, functions, methods,
3 and use statements from .php 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_php_language():
17 try:
18 import tree_sitter_php as tsphp # type: ignore[import]
19 from tree_sitter import Language
20
21 # tree-sitter-php exposes language_php() or language()
22 lang_fn = getattr(tsphp, "language_php", None) or getattr(tsphp, "language", None)
23 if lang_fn is None:
24 raise ImportError("tree_sitter_php has no language() or language_php() callable")
25 return Language(lang_fn())
26 except ImportError as e:
27 if c.type "Install tree-sitter-php: pip install tree-sitter-php"
28 ip install tree-sitter-php") from e
29
30
31 def _node_text(node, source: bytes) -> str:
32 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
33
34
35 class PHPParser(LanguageParser):
36 """Parses PHP source files into the navegador graph."""
37
38 def __init__(self) -> None:
39 from tree_sitter import Parser # type: ignore[import]
40
41 self._parser = Parser(_get_php_language())
42
43 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
44 source = path.read_bytes()
45 tree = self._parser.parse(source)
46 rel_path = str(path.relative_to(repo_root))
47
48 store.create_node(
49 NodeLabel.File,
50 {
51 "name": path.name,
52 "path": rel_path,
53 "language": "php",
54 "line_count": source.count(b"\n"),
55 },
56 )
57
58 stats = {"functions": 0, "classes": 0, "edges": 0}
59 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
60 return stats
61
62 # ── AST walker ────────────────────────────────────────────────────────────
63
64 def _walk(
65 self,
66 node,
67 source: bytes,
68 file_path: str,
69 store: GraphStore,
70 stats: dict,
71 class_name: str | None,
72 ) -> None:
73 if node.type in (
74 "class_declaration",
75 "interface_declaration",
76 "trait_declaration",
77 ):
78 self._handle_class(node, source, file_path, store, stats)
79 return
80 if node.type == "function_definition":
81 self._handle_function(node, source, file_path, store, stats, class_name)
82 return
83 if node.type == "use_declaration":
84 self._handle_use(node, source, file_path, store, stats)
85 return
86 for child in node.children:
87 self._walk(child, source, file_path, store, stats, class_name)
88
89 # ── Handlers ──────────────────────────────────────────────────────────────
90
91 def _handle_class(
92 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
93 ) -> None:
94 name_node = node.child_by_field_name("name")
95 if not name_node:
96 name_node = next((c for c in node.children if c.type == "name"), None)
97 if not name_node:
98 return
99 name = _node_text(name_node, source)
100
101 store.create_node(
102 NodeLabel.Class,
103 {
104 "name": name,
105 "file_path": file_path,
106 "line_start": node.start_point[0] + 1,
107 "line_end": node.end_point[0] + 1,
108 "docstring": "",
109 },
110 )
111 store.create_edge(
112 NodeLabel.File,
113 {"path": file_path},
114 EdgeType.CONTAINS,
115 NodeLabel.Class,
116 {"name": name, "file_path": file_path},
117 )
118 stats["classes"] += 1
119 stats["edges"] += 1
120
121 # Superclass
122 base_clause = node.child_by_field_name("base_clause")
123 if base_clause:
124 for child in base_clause.children:
125 if child.type in ("qualified_name", "name"):
126 parent_name = _node_text(child, source)
127 store.create_edge(
128 NodeLabel.Class,
129 {"name": name, "file_path": file_path},
130 EdgeType.INHERITS,
131 NodeLabel.Class,
132 {"name": parent_name, "file_path": file_path},
133 )
134 stats["edges"] += 1
135 break
136
137 # Walk class body for methods
138 body = node.child_by_field_name("body")
139 if not body:
140 body = next((c for c in node.children if c.type == "declaration_list"), None)
141 if body:
142 for child in body.children:
143 if child.type == "method_declaration":
144 self._handle_function(child, source, file_path, store, stats, class_name=name)
145
146 def _handle_function(
147 self,
148 node,
149 source: bytes,
150 file_path: str,
151 store: GraphStore,
152 stats: dict,
153 class_name: str | None,
154 ) -> None:
155 name_node = node.child_by_field_name("name")
156 if not name_node:
157 name_node = next((c for c in node.children if c.type == "name"), None)
158 if not name_node:
159 return
160 name = _node_text(name_node, source)
161
162 label = NodeLabel.Method if class_name else NodeLabel.Function
163 store.create_node(
164 label,
165 {
166 "name": name,
167 "file_path": file_path,
168 "line_start": node.start_point[0] + 1,
169 "line_end": node.end_point[0] + 1,
170 "docstring": "",
171 "class_name": class_name or "",
172 },
173 )
174
175 container_label = NodeLabel.Class if class_name else NodeLabel.File
176 container_key = (
177 {"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
178 )
179 store.create_edge(
180 container_label,
181 container_key,
182 EdgeType.CONTAINS,
183 label,
184 {"name": name, "file_path": file_path},
185 )
186 stats["functions"] += 1
187 stats["edges"] += 1
188
189 self._extract_calls(node, source, file_path, name, label, store, stats)
190
191 def _handle_use(
192 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
193 ) -> None:
194 raw = _node_text(node, source).strip()
195 module = raw.removeprefix("use").removesuffix(";").strip()
196 if not module:
197 retu
--- a/navegador/ingestion/ruby.py
+++ b/navegador/ingestion/ruby.py
@@ -0,0 +1 @@
1
+""
--- a/navegador/ingestion/ruby.py
+++ b/navegador/ingestion/ruby.py
@@ -0,0 +1 @@
 
--- a/navegador/ingestion/ruby.py
+++ b/navegador/ingestion/ruby.py
@@ -0,0 +1 @@
1 ""
--- a/navegador/ingestion/swift.py
+++ b/navegador/ingestion/swift.py
@@ -0,0 +1,263 @@
1
+"""
2
+Swift AST parser — extracts classes, structs, enums, protocols, functions,
3
+methods, and imports from .swift files using tree-sitter.
4
+"""
5
+
6
+import logging
7
+from pathlib import Path
8
+
9
+from navegador.graph.schema import EdgeType, NodeLabel
10
+from navegador.graph.store import GraphStore
11
+from navegador.ingestion.parser import LanguageParser
12
+
13
+logger = logging.getLogger(__name__)
14
+
15
+
16
+def _get_swift_language():
17
+ try:
18
+ import tree_sitter_swift as tsswift # type: ignore[import]
19
+ from tree_sitter import Language
20
+
21
+ return Language(tsswift.language())
22
+ except ImportError as e:te_node(
23
+
24
+ raise ImportError("Ins
25
+ install tree-sitter-swift") from e
26
+
27
+
28
+def _node_text(node, source: bytes) -> str:
29
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
+
31
+
32
+class SwiftParser(LanguageParser):
33
+ """Parses Swift source files into the navegador graph."""
34
+
35
+ def __init__(self) -> None:
36
+ from tree_sitter import Parser # type: ignore[import]
37
+
38
+ self._parser = Parser(_get_swift_language())
39
+
40
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41
+ source = path.read_bytes()
42
+ tree = self._parser.parse(source)
43
+ rel_path = str(path.relative_to(repo_root))
44
+
45
+ store.create_node(
46
+ NodeLabel.File,
47
+ {
48
+ "name": path.name,
49
+ "path": rel_path,
50
+ "language": "swift",
51
+ "line_count": source.count(b"\n"),
52
+ },
53
+ )
54
+
55
+ stats = {"functions": 0, "classes": 0, "edges": 0}
56
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
57
+ return stats
58
+
59
+ # ── AST walker ────────────────────────────────────────────────────────────
60
+
61
+ def _walk(
62
+ self,
63
+ node,
64
+ source: bytes,
65
+ file_path: str,
66
+ store: GraphStore,
67
+ stats: dict,
68
+ class_name: str | None,
69
+ ) -> None:
70
+ if node.type in (
71
+ "class_declaration",
72
+ "struct_declaration",
73
+ "enum_declaration",
74
+ "protocol_declaration",
75
+ "extension_declaration",
76
+ ):
77
+ self._handle_class(node, source, file_path, store, stats)
78
+ return
79
+ if node.type == "function_declaration":
80
+ self._handle_function(node, source, file_path, store, stats, class_name)
81
+ return
82
+ if node.type == "import_declaration":
83
+ self._handle_import(node, source, file_path, store, stats)
84
+ return
85
+ for child in node.children:
86
+ self._walk(child, source, file_path, store, stats, class_name)
87
+
88
+ # ── Handlers ──────────────────────────────────────────────────────────────
89
+
90
+ def _handle_class(
91
+ self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
92
+ ) -> None:
93
+ name_node = node.child_by_field_name("name")
94
+ if not name_node:
95
+ name_node = next(
96
+ (c for c in node.children if c.type in ("type_identifier", "simple_identifier")),
97
+ None,
98
+ )
99
+ if not name_node:
100
+ return
101
+ name = _node_text(name_node, source)
102
+
103
+ store.create_node(
104
+ NodeLabel.Class,
105
+ {
106
+ "name": name,
107
+ "file_path": file_path,
108
+ "line_start": node.start_point[0] + 1,
109
+ "line_end": node.end_point[0] + 1,
110
+ "docstring": "",
111
+ },
112
+ )
113
+ store.create_edge(
114
+ NodeLabel.File,
115
+ {"path": file_path},
116
+ EdgeType.CONTAINS,
117
+ NodeLabel.Class,
118
+ {"name": name, "file_path": file_path},
119
+ )
120
+ stats["classes"] += 1
121
+ stats["edges"] += 1
122
+
123
+ # Inheritance / conformance
124
+ inheritance = node.child_by_field_name("type_inheritance_clause")
125
+ if inheritance:
126
+ for child in inheritance.children:
127
+ if child.type == "type_identifier":
128
+ parent_name = _node_text(child, source)
129
+ store.create_edge(
130
+ NodeLabel.Class,
131
+ {"name": name, "file_path": file_path},
132
+ EdgeType.INHERITS,
133
+ NodeLabel.Class,
134
+ {"name": parent_name, "file_path": file_path},
135
+ )
136
+ stats["edges"] += 1
137
+
138
+ # Walk body for member functions
139
+ body = node.child_by_field_name("body")
140
+ if not body:
141
+ bodc for c in noe
142
+ in ("class_body", "struct_body", "enum_body", "protoc""
143
+Swift AST parserts clbody:
144
+ from tree_sitter imbody for child in initter-swift") from e
145
+
146
+
147
+def _node_te tion":
148
+ self._handle_function(child, source, file_path, store, stats, class_name=name)
149
+
150
+ def _handle_function(
151
+ self,
152
+ node,
153
+ source: bytes,
154
+ file_path: str,
155
+ store: GraphStore,
156
+ stats: dict,
157
+ class_name: str | None,
158
+ ) -> None:
159
+ name_node = node.child_by_field_name("name")
160
+ if not name_node:
161
+ name_node = next(
162
+ (c for c in node.children if c.type in ("simple_identifier", "identifier")), None
163
+ )
164
+ if not name_node:
165
+ return
166
+ name = _node_text(name_node, source)
167
+
168
+ label = NodeLabel.Method if class_name else NodeLabel.Function
169
+ store.create_node(
170
+ label,
171
+ {
172
+ "name": name,
173
+ "file_path": file_path,
174
+ "line_start": node.start_point[0] + 1,
175
+ "line_end": node.end_point[0] + 1,
176
+ "docstring": "",
177
+ "class_nac for c in node.children if c.typelass_name, "file_path":""
178
+Swift AST parser — extracts classes, structs, enums, protocols, functions,
179
+methods, and imports from .swift files using tree-sitter.
180
+"""
181
+
182
+import logging
183
+from pathlib import Path
184
+
185
+from navegador.graph.schema import EdgeType, NodeLabel
186
+from navegador.graph.store import GraphStore
187
+from navegador.ingestion.parser import LanguageParser
188
+
189
+logger = logging.getLogger(__name__)
190
+
191
+
192
+def _get_swift_language():
193
+ try:
194
+ import tree_sitter_swift as tsswift # type: ignore[import]
195
+ from tree_sitter import Language
196
+
197
+ return Language(tsswift.language())
198
+ except ImportError as e:
199
+ raise ImportError("Install tree-sitter-swift: pip install tree-sitter-swift") from e
200
+
201
+
202
+def _node_text(node, source: bytes) -> str:
203
+ return source[node.start_byte : node.end_byte].decots classes, structs, enums, protocols, functions,
204
+methods, and imports from .swift files using tree-sitter.
205
+"""
206
+
207
+import logging
208
+from pathlib import Path
209
+
210
+from navegador.graph.schema import EdgeType, NodeLabel
211
+from navegador.graph.store import GraphStore
212
+from navegador.ingestion.parser import LanguageParser
213
+
214
+logger = logging.getLogger(__name__)
215
+
216
+
217
+def _get_swift_language():
218
+ try:
219
+ import tree_sitter_swift as tsswift # type: ignore[import]
220
+ from tree_sitter import Language
221
+
222
+ return Language(tsswift.language())
223
+ except ImportError as e:
224
+ raise ImportError("Install tree-sitter-swift: pip install tree-sitter-swift") from e
225
+
226
+
227
+def _node_text(node, source: bytes) -> str:
228
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
229
+
230
+
231
+class SwiftParser(LanguageParser):
232
+ """Parses Swift source files into the navegador graph."""
233
+
234
+ def __init__(self) -> None:
235
+ from tree_sitter import Parser # type: ignore[import]
236
+
237
+ self._parser = Parser(_get_swift_language())
238
+
239
+ def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
240
+ source = path.read_bytes()
241
+ tree = self._parser.parse(source)
242
+ rel_path = str(path.relative_to(repo_root))
243
+
244
+ store.create_node(
245
+ NodeLabel.File,
246
+ {
247
+ "name": path.name,
248
+ "path": rel_path,
249
+ "language": "swift",
250
+ "line_count": source.count(b"\n"),
251
+ },
252
+ )
253
+
254
+ stats = {"functions": 0, "classes": 0, "edges": 0}
255
+ self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
256
+ return stats
257
+
258
+ # ── AST walker ────────────────────────────────────────────────────────────
259
+
260
+ def _walk(
261
+ self,
262
+ node,
263
+ source
--- a/navegador/ingestion/swift.py
+++ b/navegador/ingestion/swift.py
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/ingestion/swift.py
+++ b/navegador/ingestion/swift.py
@@ -0,0 +1,263 @@
1 """
2 Swift AST parser — extracts classes, structs, enums, protocols, functions,
3 methods, and imports from .swift files using tree-sitter.
4 """
5
6 import logging
7 from pathlib import Path
8
9 from navegador.graph.schema import EdgeType, NodeLabel
10 from navegador.graph.store import GraphStore
11 from navegador.ingestion.parser import LanguageParser
12
13 logger = logging.getLogger(__name__)
14
15
16 def _get_swift_language():
17 try:
18 import tree_sitter_swift as tsswift # type: ignore[import]
19 from tree_sitter import Language
20
21 return Language(tsswift.language())
22 except ImportError as e:te_node(
23
24 raise ImportError("Ins
25 install tree-sitter-swift") from e
26
27
28 def _node_text(node, source: bytes) -> str:
29 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
30
31
32 class SwiftParser(LanguageParser):
33 """Parses Swift source files into the navegador graph."""
34
35 def __init__(self) -> None:
36 from tree_sitter import Parser # type: ignore[import]
37
38 self._parser = Parser(_get_swift_language())
39
40 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
41 source = path.read_bytes()
42 tree = self._parser.parse(source)
43 rel_path = str(path.relative_to(repo_root))
44
45 store.create_node(
46 NodeLabel.File,
47 {
48 "name": path.name,
49 "path": rel_path,
50 "language": "swift",
51 "line_count": source.count(b"\n"),
52 },
53 )
54
55 stats = {"functions": 0, "classes": 0, "edges": 0}
56 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
57 return stats
58
59 # ── AST walker ────────────────────────────────────────────────────────────
60
61 def _walk(
62 self,
63 node,
64 source: bytes,
65 file_path: str,
66 store: GraphStore,
67 stats: dict,
68 class_name: str | None,
69 ) -> None:
70 if node.type in (
71 "class_declaration",
72 "struct_declaration",
73 "enum_declaration",
74 "protocol_declaration",
75 "extension_declaration",
76 ):
77 self._handle_class(node, source, file_path, store, stats)
78 return
79 if node.type == "function_declaration":
80 self._handle_function(node, source, file_path, store, stats, class_name)
81 return
82 if node.type == "import_declaration":
83 self._handle_import(node, source, file_path, store, stats)
84 return
85 for child in node.children:
86 self._walk(child, source, file_path, store, stats, class_name)
87
88 # ── Handlers ──────────────────────────────────────────────────────────────
89
90 def _handle_class(
91 self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
92 ) -> None:
93 name_node = node.child_by_field_name("name")
94 if not name_node:
95 name_node = next(
96 (c for c in node.children if c.type in ("type_identifier", "simple_identifier")),
97 None,
98 )
99 if not name_node:
100 return
101 name = _node_text(name_node, source)
102
103 store.create_node(
104 NodeLabel.Class,
105 {
106 "name": name,
107 "file_path": file_path,
108 "line_start": node.start_point[0] + 1,
109 "line_end": node.end_point[0] + 1,
110 "docstring": "",
111 },
112 )
113 store.create_edge(
114 NodeLabel.File,
115 {"path": file_path},
116 EdgeType.CONTAINS,
117 NodeLabel.Class,
118 {"name": name, "file_path": file_path},
119 )
120 stats["classes"] += 1
121 stats["edges"] += 1
122
123 # Inheritance / conformance
124 inheritance = node.child_by_field_name("type_inheritance_clause")
125 if inheritance:
126 for child in inheritance.children:
127 if child.type == "type_identifier":
128 parent_name = _node_text(child, source)
129 store.create_edge(
130 NodeLabel.Class,
131 {"name": name, "file_path": file_path},
132 EdgeType.INHERITS,
133 NodeLabel.Class,
134 {"name": parent_name, "file_path": file_path},
135 )
136 stats["edges"] += 1
137
138 # Walk body for member functions
139 body = node.child_by_field_name("body")
140 if not body:
141 bodc for c in noe
142 in ("class_body", "struct_body", "enum_body", "protoc""
143 Swift AST parserts clbody:
144 from tree_sitter imbody for child in initter-swift") from e
145
146
147 def _node_te tion":
148 self._handle_function(child, source, file_path, store, stats, class_name=name)
149
150 def _handle_function(
151 self,
152 node,
153 source: bytes,
154 file_path: str,
155 store: GraphStore,
156 stats: dict,
157 class_name: str | None,
158 ) -> None:
159 name_node = node.child_by_field_name("name")
160 if not name_node:
161 name_node = next(
162 (c for c in node.children if c.type in ("simple_identifier", "identifier")), None
163 )
164 if not name_node:
165 return
166 name = _node_text(name_node, source)
167
168 label = NodeLabel.Method if class_name else NodeLabel.Function
169 store.create_node(
170 label,
171 {
172 "name": name,
173 "file_path": file_path,
174 "line_start": node.start_point[0] + 1,
175 "line_end": node.end_point[0] + 1,
176 "docstring": "",
177 "class_nac for c in node.children if c.typelass_name, "file_path":""
178 Swift AST parser — extracts classes, structs, enums, protocols, functions,
179 methods, and imports from .swift files using tree-sitter.
180 """
181
182 import logging
183 from pathlib import Path
184
185 from navegador.graph.schema import EdgeType, NodeLabel
186 from navegador.graph.store import GraphStore
187 from navegador.ingestion.parser import LanguageParser
188
189 logger = logging.getLogger(__name__)
190
191
192 def _get_swift_language():
193 try:
194 import tree_sitter_swift as tsswift # type: ignore[import]
195 from tree_sitter import Language
196
197 return Language(tsswift.language())
198 except ImportError as e:
199 raise ImportError("Install tree-sitter-swift: pip install tree-sitter-swift") from e
200
201
202 def _node_text(node, source: bytes) -> str:
203 return source[node.start_byte : node.end_byte].decots classes, structs, enums, protocols, functions,
204 methods, and imports from .swift files using tree-sitter.
205 """
206
207 import logging
208 from pathlib import Path
209
210 from navegador.graph.schema import EdgeType, NodeLabel
211 from navegador.graph.store import GraphStore
212 from navegador.ingestion.parser import LanguageParser
213
214 logger = logging.getLogger(__name__)
215
216
217 def _get_swift_language():
218 try:
219 import tree_sitter_swift as tsswift # type: ignore[import]
220 from tree_sitter import Language
221
222 return Language(tsswift.language())
223 except ImportError as e:
224 raise ImportError("Install tree-sitter-swift: pip install tree-sitter-swift") from e
225
226
227 def _node_text(node, source: bytes) -> str:
228 return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
229
230
231 class SwiftParser(LanguageParser):
232 """Parses Swift source files into the navegador graph."""
233
234 def __init__(self) -> None:
235 from tree_sitter import Parser # type: ignore[import]
236
237 self._parser = Parser(_get_swift_language())
238
239 def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
240 source = path.read_bytes()
241 tree = self._parser.parse(source)
242 rel_path = str(path.relative_to(repo_root))
243
244 store.create_node(
245 NodeLabel.File,
246 {
247 "name": path.name,
248 "path": rel_path,
249 "language": "swift",
250 "line_count": source.count(b"\n"),
251 },
252 )
253
254 stats = {"functions": 0, "classes": 0, "edges": 0}
255 self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
256 return stats
257
258 # ── AST walker ────────────────────────────────────────────────────────────
259
260 def _walk(
261 self,
262 node,
263 source
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -34,12 +34,12 @@
3434
assert LANGUAGE_MAP[".go"] == "go"
3535
assert LANGUAGE_MAP[".rs"] == "rust"
3636
assert LANGUAGE_MAP[".java"] == "java"
3737
3838
def test_no_entry_for_unknown(self):
39
- assert ".rb" not in LANGUAGE_MAP
40
- assert ".php" not in LANGUAGE_MAP
39
+ assert ".txt" not in LANGUAGE_MAP
40
+ assert ".md" not in LANGUAGE_MAP
4141
4242
4343
# ── ingest() ─────────────────────────────────────────────────────────────────
4444
4545
class TestRepoIngester:
@@ -259,11 +259,11 @@
259259
260260
def test_raises_for_unknown_language(self):
261261
store = _make_store()
262262
ingester = RepoIngester(store)
263263
with pytest.raises(ValueError, match="Unsupported language"):
264
- ingester._get_parser("ruby")
264
+ ingester._get_parser("brainfuck")
265265
266266
def test_creates_python_parser_via_lazy_import(self):
267267
store = _make_store()
268268
ingester = RepoIngester(store)
269269
mock_py_parser = MagicMock()
@@ -329,20 +329,20 @@
329329
class TestIngesterContinueBranch:
330330
def test_skips_file_when_language_not_in_map(self):
331331
"""
332332
_iter_source_files filters to LANGUAGE_MAP extensions, but ingest()
333333
has a defensive `if not language: continue`. Test it by patching
334
- _iter_source_files to yield a .rb path.
334
+ _iter_source_files to yield a .txt path.
335335
"""
336336
import tempfile
337337
from pathlib import Path
338338
from unittest.mock import patch
339339
store = _make_store()
340340
ingester = RepoIngester(store)
341341
with tempfile.TemporaryDirectory() as tmpdir:
342
- rb_file = Path(tmpdir) / "script.rb"
343
- rb_file.write_text("puts 'hello'")
342
+ rb_file = Path(tmpdir) / "notes.txt"
343
+ rb_file.write_text("just a text file")
344344
with patch.object(ingester, "_iter_source_files", return_value=[rb_file]):
345345
stats = ingester.ingest(tmpdir)
346346
assert stats["files"] == 0
347347
348348
349349
350350
ADDED tests/test_new_language_parsers.py
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -34,12 +34,12 @@
34 assert LANGUAGE_MAP[".go"] == "go"
35 assert LANGUAGE_MAP[".rs"] == "rust"
36 assert LANGUAGE_MAP[".java"] == "java"
37
38 def test_no_entry_for_unknown(self):
39 assert ".rb" not in LANGUAGE_MAP
40 assert ".php" not in LANGUAGE_MAP
41
42
43 # ── ingest() ─────────────────────────────────────────────────────────────────
44
45 class TestRepoIngester:
@@ -259,11 +259,11 @@
259
260 def test_raises_for_unknown_language(self):
261 store = _make_store()
262 ingester = RepoIngester(store)
263 with pytest.raises(ValueError, match="Unsupported language"):
264 ingester._get_parser("ruby")
265
266 def test_creates_python_parser_via_lazy_import(self):
267 store = _make_store()
268 ingester = RepoIngester(store)
269 mock_py_parser = MagicMock()
@@ -329,20 +329,20 @@
329 class TestIngesterContinueBranch:
330 def test_skips_file_when_language_not_in_map(self):
331 """
332 _iter_source_files filters to LANGUAGE_MAP extensions, but ingest()
333 has a defensive `if not language: continue`. Test it by patching
334 _iter_source_files to yield a .rb path.
335 """
336 import tempfile
337 from pathlib import Path
338 from unittest.mock import patch
339 store = _make_store()
340 ingester = RepoIngester(store)
341 with tempfile.TemporaryDirectory() as tmpdir:
342 rb_file = Path(tmpdir) / "script.rb"
343 rb_file.write_text("puts 'hello'")
344 with patch.object(ingester, "_iter_source_files", return_value=[rb_file]):
345 stats = ingester.ingest(tmpdir)
346 assert stats["files"] == 0
347
348
349
350 DDED tests/test_new_language_parsers.py
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -34,12 +34,12 @@
34 assert LANGUAGE_MAP[".go"] == "go"
35 assert LANGUAGE_MAP[".rs"] == "rust"
36 assert LANGUAGE_MAP[".java"] == "java"
37
38 def test_no_entry_for_unknown(self):
39 assert ".txt" not in LANGUAGE_MAP
40 assert ".md" not in LANGUAGE_MAP
41
42
43 # ── ingest() ─────────────────────────────────────────────────────────────────
44
45 class TestRepoIngester:
@@ -259,11 +259,11 @@
259
260 def test_raises_for_unknown_language(self):
261 store = _make_store()
262 ingester = RepoIngester(store)
263 with pytest.raises(ValueError, match="Unsupported language"):
264 ingester._get_parser("brainfuck")
265
266 def test_creates_python_parser_via_lazy_import(self):
267 store = _make_store()
268 ingester = RepoIngester(store)
269 mock_py_parser = MagicMock()
@@ -329,20 +329,20 @@
329 class TestIngesterContinueBranch:
330 def test_skips_file_when_language_not_in_map(self):
331 """
332 _iter_source_files filters to LANGUAGE_MAP extensions, but ingest()
333 has a defensive `if not language: continue`. Test it by patching
334 _iter_source_files to yield a .txt path.
335 """
336 import tempfile
337 from pathlib import Path
338 from unittest.mock import patch
339 store = _make_store()
340 ingester = RepoIngester(store)
341 with tempfile.TemporaryDirectory() as tmpdir:
342 rb_file = Path(tmpdir) / "notes.txt"
343 rb_file.write_text("just a text file")
344 with patch.object(ingester, "_iter_source_files", return_value=[rb_file]):
345 stats = ingester.ingest(tmpdir)
346 assert stats["files"] == 0
347
348
349
350 DDED tests/test_new_language_parsers.py
--- a/tests/test_new_language_parsers.py
+++ b/tests/test_new_language_parsers.py
@@ -0,0 +1,1033 @@
1
+"""
2
+Tests for the 7 new language parsers:
3
+ KotlinParser, CSharpParser, PHPParser, RubyParser, SwiftParser, CParser, CppParser
4
+
5
+All tree-sitter grammar imports are mocked so no grammars need to be installed.
6
+"""
7
+
8
+import tempfile
9
+from pathlib import Path
10
+from unittest.mock import MagicMock, patch
11
+
12
+import pytest
13
+
14
+from navegador.graph.schema import NodeLabel
15
+from navegador.ingestion.parser import LANGUAGE_MAP, RepoIngester
16
+
17
+
18
+# ── Shared helpers ────────────────────────────────────────────────────────────
19
+
20
+
21
+class MockNode:
22
+ _id_counter = 0
23
+
24
+ def __init__(
25
+ self,
26
+ type_: str,
27
+ text: bytes = b"",
28
+ children: list = None,
29
+ start_byte: int = 0,
30
+ end_byte: int = 0,
31
+ start_point: tuple = (0, 0),
32
+ end_point: tuple = (0, 0),
33
+ parent=None,
34
+ ):
35
+ MockNode._id_counter += 1
36
+ self.id = MockNode._id_counter
37
+ self.type = type_
38
+ self.children = children or []
39
+ self.start_byte = start_byte
40
+ self.end_byte = end_byte
41
+ self.start_point = start_point
42
+ self.end_point = end_point
43
+ self.parent = parent
44
+ self._fields: dict = {}
45
+ for child in self.children:
46
+ child.parent = self
47
+
48
+ def child_by_field_name(self, name: str):
49
+ return self._fields.get(name)
50
+
51
+ def set_field(self, name: str, node):
52
+ self._fields[name] = node
53
+ node.parent = self
54
+ return self
55
+
56
+
57
+def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
58
+ return MockNode(type_, text, start_byte=0, end_byte=len(text))
59
+
60
+
61
+def _make_store():
62
+ store = MagicMock()
63
+ store.query.return_value = MagicMock(result_set=[])
64
+ return store
65
+
66
+
67
+def _make_mock_tree(root_node: MockNode):
68
+ tree = MagicMock()
69
+ tree.root_node = root_node
70
+ return tree
71
+
72
+
73
+def _mock_ts_modules(lang_module_name: str):
74
+ """Return a patch.dict context that mocks tree_sitter and the given grammar module."""
75
+ mock_lang_module = MagicMock()
76
+ mock_ts = MagicMock()
77
+ return patch.dict("sys.modules", {lang_module_name: mock_lang_module, "tree_sitter": mock_ts})
78
+
79
+
80
+# ── LANGUAGE_MAP coverage ─────────────────────────────────────────────────────
81
+
82
+
83
+class TestLanguageMapExtensions:
84
+ def test_kotlin_kt(self):
85
+ assert LANGUAGE_MAP[".kt"] == "kotlin"
86
+
87
+ def test_kotlin_kts(self):
88
+ assert LANGUAGE_MAP[".kts"] == "kotlin"
89
+
90
+ def test_csharp_cs(self):
91
+ assert LANGUAGE_MAP[".cs"] == "csharp"
92
+
93
+ def test_php(self):
94
+ assert LANGUAGE_MAP[".php"] == "php"
95
+
96
+ def test_ruby_rb(self):
97
+ assert LANGUAGE_MAP[".rb"] == "ruby"
98
+
99
+ def test_swift(self):
100
+ assert LANGUAGE_MAP[".swift"] == "swift"
101
+
102
+ def test_c_c(self):
103
+ assert LANGUAGE_MAP[".c"] == "c"
104
+
105
+ def test_c_h(self):
106
+ assert LANGUAGE_MAP[".h"] == "c"
107
+
108
+ def test_cpp_cpp(self):
109
+ assert LANGUAGE_MAP[".cpp"] == "cpp"
110
+
111
+ def test_cpp_hpp(self):
112
+ assert LANGUAGE_MAP[".hpp"] == "cpp"
113
+
114
+ def test_cpp_cc(self):
115
+ assert LANGUAGE_MAP[".cc"] == "cpp"
116
+
117
+ def test_cpp_cxx(self):
118
+ assert LANGUAGE_MAP[".cxx"] == "cpp"
119
+
120
+
121
+# ── _get_parser dispatch ──────────────────────────────────────────────────────
122
+
123
+
124
+class TestGetParserDispatch:
125
+ def _make_ingester(self):
126
+ store = _make_store()
127
+ ingester = RepoIngester.__new__(RepoIngester)
128
+ ingester.store = store
129
+ ingester.redact = False
130
+ ingester._detector = None
131
+ ingester._parsers = {}
132
+ return ingester
133
+
134
+ def _test_parser_type(self, language: str, grammar_module: str, parser_cls_name: str):
135
+ ingester = self._make_ingester()
136
+ with _mock_ts_modules(grammar_module):
137
+ # Also need to force re-import of the parser module
138
+ import sys
139
+ mod_name = f"navegador.ingestion.{language}"
140
+ if mod_name in sys.modules:
141
+ del sys.modules[mod_name]
142
+ parser = ingester._get_parser(language)
143
+ assert type(parser).__name__ == parser_cls_name
144
+
145
+ def test_kotlin_parser(self):
146
+ self._test_parser_type("kotlin", "tree_sitter_kotlin", "KotlinParser")
147
+
148
+ def test_csharp_parser(self):
149
+ self._test_parser_type("csharp", "tree_sitter_c_sharp", "CSharpParser")
150
+
151
+ def test_php_parser(self):
152
+ self._test_parser_type("php", "tree_sitter_php", "PHPParser")
153
+
154
+ def test_ruby_parser(self):
155
+ self._test_parser_type("ruby", "tree_sitter_ruby", "RubyParser")
156
+
157
+ def test_swift_parser(self):
158
+ self._test_parser_type("swift", "tree_sitter_swift", "SwiftParser")
159
+
160
+ def test_c_parser(self):
161
+ self._test_parser_type("c", "tree_sitter_c", "CParser")
162
+
163
+ def test_cpp_parser(self):
164
+ self._test_parser_type("cpp", "tree_sitter_cpp", "CppParser")
165
+
166
+
167
+# ── _get_*_language ImportError ────────────────────────────────────────────────
168
+
169
+
170
+class TestMissingGrammars:
171
+ def _assert_import_error(self, module_path: str, fn_name: str, grammar_pkg: str, grammar_module: str):
172
+ import importlib
173
+ import sys
174
+ # Remove cached module if present
175
+ if module_path in sys.modules:
176
+ del sys.modules[module_path]
177
+ with patch.dict("sys.modules", {grammar_module: None, "tree_sitter": None}):
178
+ mod = importlib.import_module(module_path)
179
+ fn = getattr(mod, fn_name)
180
+ with pytest.raises(ImportError, match=grammar_pkg):
181
+ fn()
182
+
183
+ def test_kotlin_missing(self):
184
+ self._assert_import_error(
185
+ "navegador.ingestion.kotlin", "_get_kotlin_language",
186
+ "tree-sitter-kotlin", "tree_sitter_kotlin",
187
+ )
188
+
189
+ def test_csharp_missing(self):
190
+ self._assert_import_error(
191
+ "navegador.ingestion.csharp", "_get_csharp_language",
192
+ "tree-sitter-c-sharp", "tree_sitter_c_sharp",
193
+ )
194
+
195
+ def test_php_missing(self):
196
+ self._assert_import_error(
197
+ "navegador.ingestion.php", "_get_php_language",
198
+ "tree-sitter-php", "tree_sitter_php",
199
+ )
200
+
201
+ def test_ruby_missing(self):
202
+ self._assert_import_error(
203
+ "navegador.ingestion.ruby", "_get_ruby_language",
204
+ "tree-sitter-ruby", "tree_sitter_ruby",
205
+ )
206
+
207
+ def test_swift_missing(self):
208
+ self._assert_import_error(
209
+ "navegador.ingestion.swift", "_get_swift_language",
210
+ "tree-sitter-swift", "tree_sitter_swift",
211
+ )
212
+
213
+ def test_c_missing(self):
214
+ self._assert_import_error(
215
+ "navegador.ingestion.c", "_get_c_language",
216
+ "tree-sitter-c", "tree_sitter_c",
217
+ )
218
+
219
+ def test_cpp_missing(self):
220
+ self._assert_import_error(
221
+ "navegador.ingestion.cpp", "_get_cpp_language",
222
+ "tree-sitter-cpp", "tree_sitter_cpp",
223
+ )
224
+
225
+
226
+# ── KotlinParser ──────────────────────────────────────────────────────────────
227
+
228
+
229
+def _make_kotlin_parser():
230
+ from navegador.ingestion.kotlin import KotlinParser
231
+ p = KotlinParser.__new__(KotlinParser)
232
+ p._parser = MagicMock()
233
+ return p
234
+
235
+
236
+class TestKotlinParserFileNode:
237
+ def test_parse_file_creates_file_node(self):
238
+ parser = _make_kotlin_parser()
239
+ store = _make_store()
240
+ root = MockNode("source_file")
241
+ parser._parser.parse.return_value = _make_mock_tree(root)
242
+ with tempfile.NamedTemporaryFile(suffix=".kt", delete=False) as f:
243
+ f.write(b"fun main() {}\n")
244
+ fpath = Path(f.name)
245
+ try:
246
+ parser.parse_file(fpath, fpath.parent, store)
247
+ assert store.create_node.call_args[0][0] == NodeLabel.File
248
+ assert store.create_node.call_args[0][1]["language"] == "kotlin"
249
+ finally:
250
+ fpath.unlink()
251
+
252
+
253
+class TestKotlinHandleClass:
254
+ def test_creates_class_node(self):
255
+ parser = _make_kotlin_parser()
256
+ store = _make_store()
257
+ source = b"class Foo {}"
258
+ name_node = _text_node(b"Foo", "simple_identifier")
259
+ body = MockNode("class_body")
260
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
261
+ node.set_field("name", name_node)
262
+ node.set_field("body", body)
263
+ stats = {"functions": 0, "classes": 0, "edges": 0}
264
+ parser._handle_class(node, source, "Foo.kt", store, stats)
265
+ assert stats["classes"] == 1
266
+ assert store.create_node.call_args[0][0] == NodeLabel.Class
267
+
268
+ def test_skips_if_no_name(self):
269
+ parser = _make_kotlin_parser()
270
+ store = _make_store()
271
+ node = MockNode("class_declaration")
272
+ stats = {"functions": 0, "classes": 0, "edges": 0}
273
+ parser._handle_class(node, b"", "Foo.kt", store, stats)
274
+ assert stats["classes"] == 0
275
+
276
+ def test_walks_member_functions(self):
277
+ parser = _make_kotlin_parser()
278
+ store = _make_store()
279
+ source = b"class Foo { fun bar() {} }"
280
+ class_name = _text_node(b"Foo", "simple_identifier")
281
+ fn_name = _text_node(b"bar", "simple_identifier")
282
+ fn_node = MockNode("function_declaration", start_point=(0, 12), end_point=(0, 24))
283
+ fn_node.set_field("name", fn_name)
284
+ body = MockNode("class_body", children=[fn_node])
285
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 25))
286
+ node.set_field("name", class_name)
287
+ node.set_field("body", body)
288
+ stats = {"functions": 0, "classes": 0, "edges": 0}
289
+ parser._handle_class(node, source, "Foo.kt", store, stats)
290
+ assert stats["classes"] == 1
291
+ assert stats["functions"] == 1
292
+
293
+
294
+class TestKotlinHandleFunction:
295
+ def test_creates_function_node(self):
296
+ parser = _make_kotlin_parser()
297
+ store = _make_store()
298
+ source = b"fun greet() {}"
299
+ name_node = _text_node(b"greet", "simple_identifier")
300
+ node = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 13))
301
+ node.set_field("name", name_node)
302
+ stats = {"functions": 0, "classes": 0, "edges": 0}
303
+ parser._handle_function(node, source, "Foo.kt", store, stats, class_name=None)
304
+ assert stats["functions"] == 1
305
+ assert store.create_node.call_args[0][0] == NodeLabel.Function
306
+
307
+ def test_creates_method_node_in_class(self):
308
+ parser = _make_kotlin_parser()
309
+ store = _make_store()
310
+ source = b"fun run() {}"
311
+ name_node = _text_node(b"run", "simple_identifier")
312
+ node = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 11))
313
+ node.set_field("name", name_node)
314
+ stats = {"functions": 0, "classes": 0, "edges": 0}
315
+ parser._handle_function(node, source, "Foo.kt", store, stats, class_name="Foo")
316
+ assert store.create_node.call_args[0][0] == NodeLabel.Method
317
+
318
+ def test_skips_if_no_name(self):
319
+ parser = _make_kotlin_parser()
320
+ store = _make_store()
321
+ node = MockNode("function_declaration")
322
+ stats = {"functions": 0, "classes": 0, "edges": 0}
323
+ parser._handle_function(node, b"", "Foo.kt", store, stats, class_name=None)
324
+ assert stats["functions"] == 0
325
+
326
+
327
+class TestKotlinHandleImport:
328
+ def test_creates_import_node(self):
329
+ parser = _make_kotlin_parser()
330
+ store = _make_store()
331
+ source = b"import kotlin.collections.List"
332
+ node = MockNode("import_header", start_byte=0, end_byte=len(source), start_point=(0, 0))
333
+ stats = {"functions": 0, "classes": 0, "edges": 0}
334
+ parser._handle_import(node, source, "Foo.kt", store, stats)
335
+ assert stats["edges"] == 1
336
+ props = store.create_node.call_args[0][1]
337
+ assert "kotlin.collections.List" in props["name"]
338
+
339
+
340
+# ── CSharpParser ───────────────────────────────────────────────────────────────
341
+
342
+
343
+def _make_csharp_parser():
344
+ from navegador.ingestion.csharp import CSharpParser
345
+ p = CSharpParser.__new__(CSharpParser)
346
+ p._parser = MagicMock()
347
+ return p
348
+
349
+
350
+class TestCSharpParserFileNode:
351
+ def test_parse_file_creates_file_node(self):
352
+ parser = _make_csharp_parser()
353
+ store = _make_store()
354
+ root = MockNode("compilation_unit")
355
+ parser._parser.parse.return_value = _make_mock_tree(root)
356
+ with tempfile.NamedTemporaryFile(suffix=".cs", delete=False) as f:
357
+ f.write(b"class Foo {}\n")
358
+ fpath = Path(f.name)
359
+ try:
360
+ parser.parse_file(fpath, fpath.parent, store)
361
+ assert store.create_node.call_args[0][0] == NodeLabel.File
362
+ assert store.create_node.call_args[0][1]["language"] == "csharp"
363
+ finally:
364
+ fpath.unlink()
365
+
366
+
367
+class TestCSharpHandleClass:
368
+ def test_creates_class_node(self):
369
+ parser = _make_csharp_parser()
370
+ store = _make_store()
371
+ source = b"class Foo {}"
372
+ name_node = _text_node(b"Foo")
373
+ body = MockNode("declaration_list")
374
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
375
+ node.set_field("name", name_node)
376
+ node.set_field("body", body)
377
+ stats = {"functions": 0, "classes": 0, "edges": 0}
378
+ parser._handle_class(node, source, "Foo.cs", store, stats)
379
+ assert stats["classes"] == 1
380
+ assert store.create_node.call_args[0][0] == NodeLabel.Class
381
+
382
+ def test_skips_if_no_name(self):
383
+ parser = _make_csharp_parser()
384
+ store = _make_store()
385
+ node = MockNode("class_declaration")
386
+ stats = {"functions": 0, "classes": 0, "edges": 0}
387
+ parser._handle_class(node, b"", "Foo.cs", store, stats)
388
+ assert stats["classes"] == 0
389
+
390
+ def test_creates_inherits_edge(self):
391
+ parser = _make_csharp_parser()
392
+ store = _make_store()
393
+ source = b"class Child : Parent {}"
394
+ name_node = _text_node(b"Child")
395
+ parent_id = _text_node(b"Parent")
396
+ bases = MockNode("base_list", children=[parent_id])
397
+ body = MockNode("declaration_list")
398
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 22))
399
+ node.set_field("name", name_node)
400
+ node.set_field("bases", bases)
401
+ node.set_field("body", body)
402
+ stats = {"functions": 0, "classes": 0, "edges": 0}
403
+ parser._handle_class(node, source, "Child.cs", store, stats)
404
+ assert stats["edges"] == 2 # CONTAINS + INHERITS
405
+
406
+ def test_walks_methods(self):
407
+ parser = _make_csharp_parser()
408
+ store = _make_store()
409
+ source = b"class Foo { void Save() {} }"
410
+ class_name_node = _text_node(b"Foo")
411
+ method_name_node = _text_node(b"Save")
412
+ method = MockNode("method_declaration", start_point=(0, 12), end_point=(0, 25))
413
+ method.set_field("name", method_name_node)
414
+ body = MockNode("declaration_list", children=[method])
415
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 27))
416
+ node.set_field("name", class_name_node)
417
+ node.set_field("body", body)
418
+ stats = {"functions": 0, "classes": 0, "edges": 0}
419
+ parser._handle_class(node, source, "Foo.cs", store, stats)
420
+ assert stats["functions"] == 1
421
+
422
+
423
+class TestCSharpHandleUsing:
424
+ def test_creates_import_node(self):
425
+ parser = _make_csharp_parser()
426
+ store = _make_store()
427
+ source = b"using System.Collections.Generic;"
428
+ node = MockNode("using_directive", start_byte=0, end_byte=len(source), start_point=(0, 0))
429
+ stats = {"functions": 0, "classes": 0, "edges": 0}
430
+ parser._handle_using(node, source, "Foo.cs", store, stats)
431
+ assert stats["edges"] == 1
432
+ props = store.create_node.call_args[0][1]
433
+ assert "System.Collections.Generic" in props["name"]
434
+
435
+
436
+# ── PHPParser ─────────────────────────────────────────────────────────────────
437
+
438
+
439
+def _make_php_parser():
440
+ from navegador.ingestion.php import PHPParser
441
+ p = PHPParser.__new__(PHPParser)
442
+ p._parser = MagicMock()
443
+ return p
444
+
445
+
446
+class TestPHPParserFileNode:
447
+ def test_parse_file_creates_file_node(self):
448
+ parser = _make_php_parser()
449
+ store = _make_store()
450
+ root = MockNode("program")
451
+ parser._parser.parse.return_value = _make_mock_tree(root)
452
+ with tempfile.NamedTemporaryFile(suffix=".php", delete=False) as f:
453
+ f.write(b"<?php class Foo {} ?>\n")
454
+ fpath = Path(f.name)
455
+ try:
456
+ parser.parse_file(fpath, fpath.parent, store)
457
+ assert store.create_node.call_args[0][0] == NodeLabel.File
458
+ assert store.create_node.call_args[0][1]["language"] == "php"
459
+ finally:
460
+ fpath.unlink()
461
+
462
+
463
+class TestPHPHandleClass:
464
+ def test_creates_class_node(self):
465
+ parser = _make_php_parser()
466
+ store = _make_store()
467
+ source = b"class Foo {}"
468
+ name_node = _text_node(b"Foo", "name")
469
+ body = MockNode("declaration_list")
470
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
471
+ node.set_field("name", name_node)
472
+ node.set_field("body", body)
473
+ stats = {"functions": 0, "classes": 0, "edges": 0}
474
+ parser._handle_class(node, source, "Foo.php", store, stats)
475
+ assert stats["classes"] == 1
476
+
477
+ def test_skips_if_no_name(self):
478
+ parser = _make_php_parser()
479
+ store = _make_store()
480
+ node = MockNode("class_declaration")
481
+ stats = {"functions": 0, "classes": 0, "edges": 0}
482
+ parser._handle_class(node, b"", "Foo.php", store, stats)
483
+ assert stats["classes"] == 0
484
+
485
+ def test_creates_inherits_edge(self):
486
+ parser = _make_php_parser()
487
+ store = _make_store()
488
+ source = b"class Child extends Parent {}"
489
+ name_node = _text_node(b"Child", "name")
490
+ parent_name_node = _text_node(b"Parent", "qualified_name")
491
+ base_clause = MockNode("base_clause", children=[parent_name_node])
492
+ body = MockNode("declaration_list")
493
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 28))
494
+ node.set_field("name", name_node)
495
+ node.set_field("base_clause", base_clause)
496
+ node.set_field("body", body)
497
+ stats = {"functions": 0, "classes": 0, "edges": 0}
498
+ parser._handle_class(node, source, "Child.php", store, stats)
499
+ assert stats["edges"] == 2 # CONTAINS + INHERITS
500
+
501
+
502
+class TestPHPHandleFunction:
503
+ def test_creates_function_node(self):
504
+ parser = _make_php_parser()
505
+ store = _make_store()
506
+ source = b"function save() {}"
507
+ name_node = _text_node(b"save", "name")
508
+ node = MockNode("function_definition", start_point=(0, 0), end_point=(0, 17))
509
+ node.set_field("name", name_node)
510
+ stats = {"functions": 0, "classes": 0, "edges": 0}
511
+ parser._handle_function(node, source, "Foo.php", store, stats, class_name=None)
512
+ assert stats["functions"] == 1
513
+ assert store.create_node.call_args[0][0] == NodeLabel.Function
514
+
515
+ def test_skips_if_no_name(self):
516
+ parser = _make_php_parser()
517
+ store = _make_store()
518
+ node = MockNode("function_definition")
519
+ stats = {"functions": 0, "classes": 0, "edges": 0}
520
+ parser._handle_function(node, b"", "Foo.php", store, stats, class_name=None)
521
+ assert stats["functions"] == 0
522
+
523
+
524
+class TestPHPHandleUse:
525
+ def test_creates_import_node(self):
526
+ parser = _make_php_parser()
527
+ store = _make_store()
528
+ source = b"use App\\Http\\Controllers\\Controller;"
529
+ node = MockNode("use_declaration", start_byte=0, end_byte=len(source), start_point=(0, 0))
530
+ stats = {"functions": 0, "classes": 0, "edges": 0}
531
+ parser._handle_use(node, source, "Foo.php", store, stats)
532
+ assert stats["edges"] == 1
533
+ props = store.create_node.call_args[0][1]
534
+ assert "Controller" in props["name"]
535
+
536
+
537
+# ── RubyParser ────────────────────────────────────────────────────────────────
538
+
539
+
540
+def _make_ruby_parser():
541
+ from navegador.ingestion.ruby import RubyParser
542
+ p = RubyParser.__new__(RubyParser)
543
+ p._parser = MagicMock()
544
+ return p
545
+
546
+
547
+class TestRubyParserFileNode:
548
+ def test_parse_file_creates_file_node(self):
549
+ parser = _make_ruby_parser()
550
+ store = _make_store()
551
+ root = MockNode("program")
552
+ parser._parser.parse.return_value = _make_mock_tree(root)
553
+ with tempfile.NamedTemporaryFile(suffix=".rb", delete=False) as f:
554
+ f.write(b"class Foo; end\n")
555
+ fpath = Path(f.name)
556
+ try:
557
+ parser.parse_file(fpath, fpath.parent, store)
558
+ assert store.create_node.call_args[0][0] == NodeLabel.File
559
+ assert store.create_node.call_args[0][1]["language"] == "ruby"
560
+ finally:
561
+ fpath.unlink()
562
+
563
+
564
+class TestRubyHandleClass:
565
+ def test_creates_class_node(self):
566
+ parser = _make_ruby_parser()
567
+ store = _make_store()
568
+ source = b"class Foo; end"
569
+ name_node = _text_node(b"Foo", "constant")
570
+ body = MockNode("body_statement")
571
+ node = MockNode("class", start_point=(0, 0), end_point=(0, 13))
572
+ node.set_field("name", name_node)
573
+ node.set_field("body", body)
574
+ stats = {"functions": 0, "classes": 0, "edges": 0}
575
+ parser._handle_class(node, source, "foo.rb", store, stats)
576
+ assert stats["classes"] == 1
577
+
578
+ def test_skips_if_no_name(self):
579
+ parser = _make_ruby_parser()
580
+ store = _make_store()
581
+ node = MockNode("class")
582
+ stats = {"functions": 0, "classes": 0, "edges": 0}
583
+ parser._handle_class(node, b"", "foo.rb", store, stats)
584
+ assert stats["classes"] == 0
585
+
586
+ def test_creates_inherits_edge(self):
587
+ parser = _make_ruby_parser()
588
+ store = _make_store()
589
+ source = b"class Child < Parent; end"
590
+ name_node = _text_node(b"Child", "constant")
591
+ superclass_node = _text_node(b"Parent", "constant")
592
+ body = MockNode("body_statement")
593
+ node = MockNode("class", start_point=(0, 0), end_point=(0, 24))
594
+ node.set_field("name", name_node)
595
+ node.set_field("superclass", superclass_node)
596
+ node.set_field("body", body)
597
+ stats = {"functions": 0, "classes": 0, "edges": 0}
598
+ parser._handle_class(node, source, "child.rb", store, stats)
599
+ assert stats["edges"] >= 2 # CONTAINS + INHERITS
600
+
601
+ def test_walks_body_methods(self):
602
+ parser = _make_ruby_parser()
603
+ store = _make_store()
604
+ source = b"class Foo; def run; end; end"
605
+ class_name_node = _text_node(b"Foo", "constant")
606
+ method_name_node = _text_node(b"run")
607
+ method_node = MockNode("method", start_point=(0, 11), end_point=(0, 22))
608
+ method_node.set_field("name", method_name_node)
609
+ body = MockNode("body_statement", children=[method_node])
610
+ node = MockNode("class", start_point=(0, 0), end_point=(0, 26))
611
+ node.set_field("name", class_name_node)
612
+ node.set_field("body", body)
613
+ stats = {"functions": 0, "classes": 0, "edges": 0}
614
+ parser._handle_class(node, source, "foo.rb", store, stats)
615
+ assert stats["functions"] == 1
616
+
617
+
618
+class TestRubyHandleMethod:
619
+ def test_creates_function_node(self):
620
+ parser = _make_ruby_parser()
621
+ store = _make_store()
622
+ source = b"def run; end"
623
+ name_node = _text_node(b"run")
624
+ node = MockNode("method", start_point=(0, 0), end_point=(0, 11))
625
+ node.set_field("name", name_node)
626
+ stats = {"functions": 0, "classes": 0, "edges": 0}
627
+ parser._handle_method(node, source, "foo.rb", store, stats, class_name=None)
628
+ assert stats["functions"] == 1
629
+ assert store.create_node.call_args[0][0] == NodeLabel.Function
630
+
631
+ def test_creates_method_node_in_class(self):
632
+ parser = _make_ruby_parser()
633
+ store = _make_store()
634
+ source = b"def run; end"
635
+ name_node = _text_node(b"run")
636
+ node = MockNode("method", start_point=(0, 0), end_point=(0, 11))
637
+ node.set_field("name", name_node)
638
+ stats = {"functions": 0, "classes": 0, "edges": 0}
639
+ parser._handle_method(node, source, "foo.rb", store, stats, class_name="Foo")
640
+ assert store.create_node.call_args[0][0] == NodeLabel.Method
641
+
642
+ def test_skips_if_no_name(self):
643
+ parser = _make_ruby_parser()
644
+ store = _make_store()
645
+ node = MockNode("method")
646
+ stats = {"functions": 0, "classes": 0, "edges": 0}
647
+ parser._handle_method(node, b"", "foo.rb", store, stats, class_name=None)
648
+ assert stats["functions"] == 0
649
+
650
+
651
+class TestRubyHandleModule:
652
+ def test_creates_module_node(self):
653
+ parser = _make_ruby_parser()
654
+ store = _make_store()
655
+ source = b"module Concerns; end"
656
+ name_node = _text_node(b"Concerns", "constant")
657
+ body = MockNode("body_statement")
658
+ node = MockNode("module", start_point=(0, 0), end_point=(0, 19))
659
+ node.set_field("name", name_node)
660
+ node.set_field("body", body)
661
+ stats = {"functions": 0, "classes": 0, "edges": 0}
662
+ parser._handle_module(node, source, "concerns.rb", store, stats)
663
+ assert stats["classes"] == 1
664
+ props = store.create_node.call_args[0][1]
665
+ assert props.get("docstring") == "module"
666
+
667
+
668
+# ── SwiftParser ───────────────────────────────────────────────────────────────
669
+
670
+
671
+def _make_swift_parser():
672
+ from navegador.ingestion.swift import SwiftParser
673
+ p = SwiftParser.__new__(SwiftParser)
674
+ p._parser = MagicMock()
675
+ return p
676
+
677
+
678
+class TestSwiftParserFileNode:
679
+ def test_parse_file_creates_file_node(self):
680
+ parser = _make_swift_parser()
681
+ store = _make_store()
682
+ root = MockNode("source_file")
683
+ parser._parser.parse.return_value = _make_mock_tree(root)
684
+ with tempfile.NamedTemporaryFile(suffix=".swift", delete=False) as f:
685
+ f.write(b"class Foo {}\n")
686
+ fpath = Path(f.name)
687
+ try:
688
+ parser.parse_file(fpath, fpath.parent, store)
689
+ assert store.create_node.call_args[0][0] == NodeLabel.File
690
+ assert store.create_node.call_args[0][1]["language"] == "swift"
691
+ finally:
692
+ fpath.unlink()
693
+
694
+
695
+class TestSwiftHandleClass:
696
+ def test_creates_class_node(self):
697
+ parser = _make_swift_parser()
698
+ store = _make_store()
699
+ source = b"class Foo {}"
700
+ name_node = _text_node(b"Foo", "type_identifier")
701
+ body = MockNode("class_body")
702
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
703
+ node.set_field("name", name_node)
704
+ node.set_field("body", body)
705
+ stats = {"functions": 0, "classes": 0, "edges": 0}
706
+ parser._handle_class(node, source, "Foo.swift", store, stats)
707
+ assert stats["classes"] == 1
708
+
709
+ def test_skips_if_no_name(self):
710
+ parser = _make_swift_parser()
711
+ store = _make_store()
712
+ node = MockNode("class_declaration")
713
+ stats = {"functions": 0, "classes": 0, "edges": 0}
714
+ parser._handle_class(node, b"", "Foo.swift", store, stats)
715
+ assert stats["classes"] == 0
716
+
717
+ def test_creates_inherits_edge(self):
718
+ parser = _make_swift_parser()
719
+ store = _make_store()
720
+ source = b"class Child: Parent {}"
721
+ name_node = _text_node(b"Child", "type_identifier")
722
+ parent_id = _text_node(b"Parent", "type_identifier")
723
+ inheritance = MockNode("type_inheritance_clause", children=[parent_id])
724
+ body = MockNode("class_body")
725
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 21))
726
+ node.set_field("name", name_node)
727
+ node.set_field("type_inheritance_clause", inheritance)
728
+ node.set_field("body", body)
729
+ stats = {"functions": 0, "classes": 0, "edges": 0}
730
+ parser._handle_class(node, source, "Child.swift", store, stats)
731
+ assert stats["edges"] == 2 # CONTAINS + INHERITS
732
+
733
+ def test_walks_body_functions(self):
734
+ parser = _make_swift_parser()
735
+ store = _make_store()
736
+ source = b"class Foo { func run() {} }"
737
+ class_name = _text_node(b"Foo", "type_identifier")
738
+ fn_name = _text_node(b"run", "simple_identifier")
739
+ fn_node = MockNode("function_declaration", start_point=(0, 12), end_point=(0, 24))
740
+ fn_node.set_field("name", fn_name)
741
+ body = MockNode("class_body", children=[fn_node])
742
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 26))
743
+ node.set_field("name", class_name)
744
+ node.set_field("body", body)
745
+ stats = {"functions": 0, "classes": 0, "edges": 0}
746
+ parser._handle_class(node, source, "Foo.swift", store, stats)
747
+ assert stats["functions"] == 1
748
+
749
+
750
+class TestSwiftHandleImport:
751
+ def test_creates_import_node(self):
752
+ parser = _make_swift_parser()
753
+ store = _make_store()
754
+ source = b"import Foundation"
755
+ node = MockNode("import_declaration", start_byte=0, end_byte=len(source), start_point=(0, 0))
756
+ stats = {"functions": 0, "classes": 0, "edges": 0}
757
+ parser._handle_import(node, source, "Foo.swift", store, stats)
758
+ assert stats["edges"] == 1
759
+ props = store.create_node.call_args[0][1]
760
+ assert "Foundation" in props["name"]
761
+
762
+
763
+# ── CParser ───────────────────────────────────────────────────────────────────
764
+
765
+
766
+def _make_c_parser():
767
+ from navegador.ingestion.c import CParser
768
+ p = CParser.__new__(CParser)
769
+ p._parser = MagicMock()
770
+ return p
771
+
772
+
773
+class TestCParserFileNode:
774
+ def test_parse_file_creates_file_node(self):
775
+ parser = _make_c_parser()
776
+ store = _make_store()
777
+ root = MockNode("translation_unit")
778
+ parser._parser.parse.return_value = _make_mock_tree(root)
779
+ with tempfile.NamedTemporaryFile(suffix=".c", delete=False) as f:
780
+ f.write(b"void foo() {}\n")
781
+ fpath = Path(f.name)
782
+ try:
783
+ parser.parse_file(fpath, fpath.parent, store)
784
+ assert store.create_node.call_args[0][0] == NodeLabel.File
785
+ assert store.create_node.call_args[0][1]["language"] == "c"
786
+ finally:
787
+ fpath.unlink()
788
+
789
+
790
+class TestCHandleFunction:
791
+ def _make_function_node(self, fn_name: bytes) -> tuple[MockNode, bytes]:
792
+ source = fn_name + b"(void) {}"
793
+ fn_id = _text_node(fn_name)
794
+ fn_decl = MockNode("function_declarator")
795
+ fn_decl.set_field("declarator", fn_id)
796
+ body = MockNode("compound_statement")
797
+ node = MockNode("function_definition", start_point=(0, 0), end_point=(0, len(source)))
798
+ node.set_field("declarator", fn_decl)
799
+ node.set_field("body", body)
800
+ return node, source
801
+
802
+ def test_creates_function_node(self):
803
+ parser = _make_c_parser()
804
+ store = _make_store()
805
+ node, source = self._make_function_node(b"main")
806
+ stats = {"functions": 0, "classes": 0, "edges": 0}
807
+ parser._handle_function(node, source, "main.c", store, stats)
808
+ assert stats["functions"] == 1
809
+ assert store.create_node.call_args[0][0] == NodeLabel.Function
810
+
811
+ def test_skips_if_no_name(self):
812
+ parser = _make_c_parser()
813
+ store = _make_store()
814
+ node = MockNode("function_definition")
815
+ stats = {"functions": 0, "classes": 0, "edges": 0}
816
+ parser._handle_function(node, b"", "main.c", store, stats)
817
+ assert stats["functions"] == 0
818
+
819
+
820
+class TestCHandleStruct:
821
+ def test_creates_struct_node(self):
822
+ parser = _make_c_parser()
823
+ store = _make_store()
824
+ source = b"struct Point { int x; int y; };"
825
+ name_node = _text_node(b"Point", "type_identifier")
826
+ node = MockNode("struct_specifier", start_point=(0, 0), end_point=(0, 30))
827
+ node.set_field("name", name_node)
828
+ stats = {"functions": 0, "classes": 0, "edges": 0}
829
+ parser._handle_struct(node, source, "point.c", store, stats)
830
+ assert stats["classes"] == 1
831
+ props = store.create_node.call_args[0][1]
832
+ assert props["docstring"] == "struct"
833
+
834
+ def test_skips_if_no_name(self):
835
+ parser = _make_c_parser()
836
+ store = _make_store()
837
+ node = MockNode("struct_specifier")
838
+ stats = {"functions": 0, "classes": 0, "edges": 0}
839
+ parser._handle_struct(node, b"", "point.c", store, stats)
840
+ assert stats["classes"] == 0
841
+
842
+
843
+class TestCHandleInclude:
844
+ def test_creates_import_node_angle_bracket(self):
845
+ parser = _make_c_parser()
846
+ store = _make_store()
847
+ source = b"#include <stdio.h>"
848
+ path_node = MockNode("system_lib_string", start_byte=9, end_byte=18)
849
+ node = MockNode("preproc_include", start_byte=0, end_byte=18, start_point=(0, 0))
850
+ node.set_field("path", path_node)
851
+ stats = {"functions": 0, "classes": 0, "edges": 0}
852
+ parser._handle_include(node, source, "main.c", store, stats)
853
+ assert stats["edges"] == 1
854
+ props = store.create_node.call_args[0][1]
855
+ assert "stdio.h" in props["name"]
856
+
857
+ def test_creates_import_node_quoted(self):
858
+ parser = _make_c_parser()
859
+ store = _make_store()
860
+ source = b'#include "utils.h"'
861
+ path_node = MockNode("string_literal", start_byte=9, end_byte=18)
862
+ node = MockNode("preproc_include", start_byte=0, end_byte=18, start_point=(0, 0))
863
+ node.set_field("path", path_node)
864
+ stats = {"functions": 0, "classes": 0, "edges": 0}
865
+ parser._handle_include(node, source, "main.c", store, stats)
866
+ assert stats["edges"] == 1
867
+ props = store.create_node.call_args[0][1]
868
+ assert "utils.h" in props["name"]
869
+
870
+
871
+# ── CppParser ─────────────────────────────────────────────────────────────────
872
+
873
+
874
+def _make_cpp_parser():
875
+ from navegador.ingestion.cpp import CppParser
876
+ p = CppParser.__new__(CppParser)
877
+ p._parser = MagicMock()
878
+ return p
879
+
880
+
881
+class TestCppParserFileNode:
882
+ def test_parse_file_creates_file_node(self):
883
+ parser = _make_cpp_parser()
884
+ store = _make_store()
885
+ root = MockNode("translation_unit")
886
+ parser._parser.parse.return_value = _make_mock_tree(root)
887
+ with tempfile.NamedTemporaryFile(suffix=".cpp", delete=False) as f:
888
+ f.write(b"class Foo {};\n")
889
+ fpath = Path(f.name)
890
+ try:
891
+ parser.parse_file(fpath, fpath.parent, store)
892
+ assert store.create_node.call_args[0][0] == NodeLabel.File
893
+ assert store.create_node.call_args[0][1]["language"] == "cpp"
894
+ finally:
895
+ fpath.unlink()
896
+
897
+
898
+class TestCppHandleClass:
899
+ def test_creates_class_node(self):
900
+ parser = _make_cpp_parser()
901
+ store = _make_store()
902
+ source = b"class Foo {};"
903
+ name_node = _text_node(b"Foo", "type_identifier")
904
+ body = MockNode("field_declaration_list")
905
+ node = MockNode("class_specifier", start_point=(0, 0), end_point=(0, 12))
906
+ node.set_field("name", name_node)
907
+ node.set_field("body", body)
908
+ stats = {"functions": 0, "classes": 0, "edges": 0}
909
+ parser._handle_class(node, source, "Foo.cpp", store, stats)
910
+ assert stats["classes"] == 1
911
+
912
+ def test_skips_if_no_name(self):
913
+ parser = _make_cpp_parser()
914
+ store = _make_store()
915
+ node = MockNode("class_specifier")
916
+ stats = {"functions": 0, "classes": 0, "edges": 0}
917
+ parser._handle_class(node, b"", "Foo.cpp", store, stats)
918
+ assert stats["classes"] == 0
919
+
920
+ def test_creates_inherits_edge(self):
921
+ parser = _make_cpp_parser()
922
+ store = _make_store()
923
+ source = b"class Child : public Parent {};"
924
+ name_node = _text_node(b"Child", "type_identifier")
925
+ parent_id = _text_node(b"Parent", "type_identifier")
926
+ base_clause = MockNode("base_class_clause", children=[parent_id])
927
+ body = MockNode("field_declaration_list")
928
+ node = MockNode("class_specifier", start_point=(0, 0), end_point=(0, 30))
929
+ node.set_field("name", name_node)
930
+ node.set_field("base_clause", base_clause)
931
+ node.set_field("body", body)
932
+ stats = {"functions": 0, "classes": 0, "edges": 0}
933
+ parser._handle_class(node, source, "Child.cpp", store, stats)
934
+ assert stats["edges"] == 2 # CONTAINS + INHERITS
935
+
936
+ def test_walks_member_functions(self):
937
+ parser = _make_cpp_parser()
938
+ store = _make_store()
939
+ source = b"class Foo { void run() {} };"
940
+ class_name = _text_node(b"Foo", "type_identifier")
941
+ fn_id = _text_node(b"run")
942
+ fn_decl = MockNode("function_declarator")
943
+ fn_decl.set_field("declarator", fn_id)
944
+ fn_body = MockNode("compound_statement")
945
+ fn_node = MockNode("function_definition", start_point=(0, 12), end_point=(0, 24))
946
+ fn_node.set_field("declarator", fn_decl)
947
+ fn_node.set_field("body", fn_body)
948
+ body = MockNode("field_declaration_list", children=[fn_node])
949
+ node = MockNode("class_specifier", start_point=(0, 0), end_point=(0, 27))
950
+ node.set_field("name", class_name)
951
+ node.set_field("body", body)
952
+ stats = {"functions": 0, "classes": 0, "edges": 0}
953
+ parser._handle_class(node, source, "Foo.cpp", store, stats)
954
+ assert stats["functions"] == 1
955
+
956
+
957
+class TestCppHandleFunction:
958
+ def test_creates_function_node(self):
959
+ parser = _make_cpp_parser()
960
+ store = _make_store()
961
+ source = b"void main() {}"
962
+ fn_id = _text_node(b"main")
963
+ fn_decl = MockNode("function_declarator")
964
+ fn_decl.set_field("declarator", fn_id)
965
+ body = MockNode("compound_statement")
966
+ node = MockNode("function_definition", start_point=(0, 0), end_point=(0, 13))
967
+ node.set_field("declarator", fn_decl)
968
+ node.set_field("body", body)
969
+ stats = {"functions": 0, "classes": 0, "edges": 0}
970
+ parser._handle_function(node, source, "main.cpp", store, stats, class_name=None)
971
+ assert stats["functions"] == 1
972
+ assert store.create_node.call_args[0][0] == NodeLabel.Function
973
+
974
+ def test_creates_method_node_in_class(self):
975
+ parser = _make_cpp_parser()
976
+ store = _make_store()
977
+ source = b"void run() {}"
978
+ fn_id = _text_node(b"run")
979
+ fn_decl = MockNode("function_declarator")
980
+ fn_decl.set_field("declarator", fn_id)
981
+ body = MockNode("compound_statement")
982
+ node = MockNode("function_definition", start_point=(0, 0), end_point=(0, 12))
983
+ node.set_field("declarator", fn_decl)
984
+ node.set_field("body", body)
985
+ stats = {"functions": 0, "classes": 0, "edges": 0}
986
+ parser._handle_function(node, source, "Foo.cpp", store, stats, class_name="Foo")
987
+ assert store.create_node.call_args[0][0] == NodeLabel.Method
988
+
989
+ def test_skips_if_no_name(self):
990
+ parser = _make_cpp_parser()
991
+ store = _make_store()
992
+ node = MockNode("function_definition")
993
+ stats = {"functions": 0, "classes": 0, "edges": 0}
994
+ parser._handle_function(node, b"", "main.cpp", store, stats, class_name=None)
995
+ assert stats["functions"] == 0
996
+
997
+
998
+class TestCppHandleInclude:
999
+ def test_creates_import_node(self):
1000
+ parser = _make_cpp_parser()
1001
+ store = _make_store()
1002
+ source = b"#include <vector>"
1003
+ path_node = MockNode("system_lib_string", start_byte=9, end_byte=17)
1004
+ node = MockNode("preproc_include", start_byte=0, end_byte=17, start_point=(0, 0))
1005
+ node.set_field("path", path_node)
1006
+ stats = {"functions": 0, "classes": 0, "edges": 0}
1007
+ parser._handle_include(node, source, "Foo.cpp", store, stats)
1008
+ assert stats["edges"] == 1
1009
+ props = store.create_node.call_args[0][1]
1010
+ assert "vector" in props["name"]
1011
+
1012
+
1013
+class TestCppExtractFunctionName:
1014
+ def test_simple_identifier(self):
1015
+ from navegador.ingestion.cpp import CppParser
1016
+ parser = CppParser.__new__(CppParser)
1017
+ source = b"foo"
1018
+ node = _text_node(b"foo")
1019
+ assert parser._extract_function_name(node, source) == "foo"
1020
+
1021
+ def test_function_declarator(self):
1022
+ from navegador.ingestion.cpp import CppParser
1023
+ parser = CppParser.__new__(CppParser)
1024
+ source = b"foo"
1025
+ fn_id = _text_node(b"foo")
1026
+ fn_decl = MockNode("function_declarator")
1027
+ fn_decl.set_field("declarator", fn_id)
1028
+ assert parser._extract_function_name(fn_decl, source) == "foo"
1029
+
1030
+ def test_none_input(self):
1031
+ from navegador.ingestion.cpp import CppParser
1032
+ parser = CppParser.__new__(CppParser)
1033
+ assert parser._extract_function_name(None, b"") is None
--- a/tests/test_new_language_parsers.py
+++ b/tests/test_new_language_parsers.py
@@ -0,0 +1,1033 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_new_language_parsers.py
+++ b/tests/test_new_language_parsers.py
@@ -0,0 +1,1033 @@
1 """
2 Tests for the 7 new language parsers:
3 KotlinParser, CSharpParser, PHPParser, RubyParser, SwiftParser, CParser, CppParser
4
5 All tree-sitter grammar imports are mocked so no grammars need to be installed.
6 """
7
8 import tempfile
9 from pathlib import Path
10 from unittest.mock import MagicMock, patch
11
12 import pytest
13
14 from navegador.graph.schema import NodeLabel
15 from navegador.ingestion.parser import LANGUAGE_MAP, RepoIngester
16
17
18 # ── Shared helpers ────────────────────────────────────────────────────────────
19
20
21 class MockNode:
22 _id_counter = 0
23
24 def __init__(
25 self,
26 type_: str,
27 text: bytes = b"",
28 children: list = None,
29 start_byte: int = 0,
30 end_byte: int = 0,
31 start_point: tuple = (0, 0),
32 end_point: tuple = (0, 0),
33 parent=None,
34 ):
35 MockNode._id_counter += 1
36 self.id = MockNode._id_counter
37 self.type = type_
38 self.children = children or []
39 self.start_byte = start_byte
40 self.end_byte = end_byte
41 self.start_point = start_point
42 self.end_point = end_point
43 self.parent = parent
44 self._fields: dict = {}
45 for child in self.children:
46 child.parent = self
47
48 def child_by_field_name(self, name: str):
49 return self._fields.get(name)
50
51 def set_field(self, name: str, node):
52 self._fields[name] = node
53 node.parent = self
54 return self
55
56
57 def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
58 return MockNode(type_, text, start_byte=0, end_byte=len(text))
59
60
61 def _make_store():
62 store = MagicMock()
63 store.query.return_value = MagicMock(result_set=[])
64 return store
65
66
67 def _make_mock_tree(root_node: MockNode):
68 tree = MagicMock()
69 tree.root_node = root_node
70 return tree
71
72
73 def _mock_ts_modules(lang_module_name: str):
74 """Return a patch.dict context that mocks tree_sitter and the given grammar module."""
75 mock_lang_module = MagicMock()
76 mock_ts = MagicMock()
77 return patch.dict("sys.modules", {lang_module_name: mock_lang_module, "tree_sitter": mock_ts})
78
79
80 # ── LANGUAGE_MAP coverage ─────────────────────────────────────────────────────
81
82
83 class TestLanguageMapExtensions:
84 def test_kotlin_kt(self):
85 assert LANGUAGE_MAP[".kt"] == "kotlin"
86
87 def test_kotlin_kts(self):
88 assert LANGUAGE_MAP[".kts"] == "kotlin"
89
90 def test_csharp_cs(self):
91 assert LANGUAGE_MAP[".cs"] == "csharp"
92
93 def test_php(self):
94 assert LANGUAGE_MAP[".php"] == "php"
95
96 def test_ruby_rb(self):
97 assert LANGUAGE_MAP[".rb"] == "ruby"
98
99 def test_swift(self):
100 assert LANGUAGE_MAP[".swift"] == "swift"
101
102 def test_c_c(self):
103 assert LANGUAGE_MAP[".c"] == "c"
104
105 def test_c_h(self):
106 assert LANGUAGE_MAP[".h"] == "c"
107
108 def test_cpp_cpp(self):
109 assert LANGUAGE_MAP[".cpp"] == "cpp"
110
111 def test_cpp_hpp(self):
112 assert LANGUAGE_MAP[".hpp"] == "cpp"
113
114 def test_cpp_cc(self):
115 assert LANGUAGE_MAP[".cc"] == "cpp"
116
117 def test_cpp_cxx(self):
118 assert LANGUAGE_MAP[".cxx"] == "cpp"
119
120
121 # ── _get_parser dispatch ──────────────────────────────────────────────────────
122
123
124 class TestGetParserDispatch:
125 def _make_ingester(self):
126 store = _make_store()
127 ingester = RepoIngester.__new__(RepoIngester)
128 ingester.store = store
129 ingester.redact = False
130 ingester._detector = None
131 ingester._parsers = {}
132 return ingester
133
134 def _test_parser_type(self, language: str, grammar_module: str, parser_cls_name: str):
135 ingester = self._make_ingester()
136 with _mock_ts_modules(grammar_module):
137 # Also need to force re-import of the parser module
138 import sys
139 mod_name = f"navegador.ingestion.{language}"
140 if mod_name in sys.modules:
141 del sys.modules[mod_name]
142 parser = ingester._get_parser(language)
143 assert type(parser).__name__ == parser_cls_name
144
145 def test_kotlin_parser(self):
146 self._test_parser_type("kotlin", "tree_sitter_kotlin", "KotlinParser")
147
148 def test_csharp_parser(self):
149 self._test_parser_type("csharp", "tree_sitter_c_sharp", "CSharpParser")
150
151 def test_php_parser(self):
152 self._test_parser_type("php", "tree_sitter_php", "PHPParser")
153
154 def test_ruby_parser(self):
155 self._test_parser_type("ruby", "tree_sitter_ruby", "RubyParser")
156
157 def test_swift_parser(self):
158 self._test_parser_type("swift", "tree_sitter_swift", "SwiftParser")
159
160 def test_c_parser(self):
161 self._test_parser_type("c", "tree_sitter_c", "CParser")
162
163 def test_cpp_parser(self):
164 self._test_parser_type("cpp", "tree_sitter_cpp", "CppParser")
165
166
167 # ── _get_*_language ImportError ────────────────────────────────────────────────
168
169
170 class TestMissingGrammars:
171 def _assert_import_error(self, module_path: str, fn_name: str, grammar_pkg: str, grammar_module: str):
172 import importlib
173 import sys
174 # Remove cached module if present
175 if module_path in sys.modules:
176 del sys.modules[module_path]
177 with patch.dict("sys.modules", {grammar_module: None, "tree_sitter": None}):
178 mod = importlib.import_module(module_path)
179 fn = getattr(mod, fn_name)
180 with pytest.raises(ImportError, match=grammar_pkg):
181 fn()
182
183 def test_kotlin_missing(self):
184 self._assert_import_error(
185 "navegador.ingestion.kotlin", "_get_kotlin_language",
186 "tree-sitter-kotlin", "tree_sitter_kotlin",
187 )
188
189 def test_csharp_missing(self):
190 self._assert_import_error(
191 "navegador.ingestion.csharp", "_get_csharp_language",
192 "tree-sitter-c-sharp", "tree_sitter_c_sharp",
193 )
194
195 def test_php_missing(self):
196 self._assert_import_error(
197 "navegador.ingestion.php", "_get_php_language",
198 "tree-sitter-php", "tree_sitter_php",
199 )
200
201 def test_ruby_missing(self):
202 self._assert_import_error(
203 "navegador.ingestion.ruby", "_get_ruby_language",
204 "tree-sitter-ruby", "tree_sitter_ruby",
205 )
206
207 def test_swift_missing(self):
208 self._assert_import_error(
209 "navegador.ingestion.swift", "_get_swift_language",
210 "tree-sitter-swift", "tree_sitter_swift",
211 )
212
213 def test_c_missing(self):
214 self._assert_import_error(
215 "navegador.ingestion.c", "_get_c_language",
216 "tree-sitter-c", "tree_sitter_c",
217 )
218
219 def test_cpp_missing(self):
220 self._assert_import_error(
221 "navegador.ingestion.cpp", "_get_cpp_language",
222 "tree-sitter-cpp", "tree_sitter_cpp",
223 )
224
225
226 # ── KotlinParser ──────────────────────────────────────────────────────────────
227
228
229 def _make_kotlin_parser():
230 from navegador.ingestion.kotlin import KotlinParser
231 p = KotlinParser.__new__(KotlinParser)
232 p._parser = MagicMock()
233 return p
234
235
236 class TestKotlinParserFileNode:
237 def test_parse_file_creates_file_node(self):
238 parser = _make_kotlin_parser()
239 store = _make_store()
240 root = MockNode("source_file")
241 parser._parser.parse.return_value = _make_mock_tree(root)
242 with tempfile.NamedTemporaryFile(suffix=".kt", delete=False) as f:
243 f.write(b"fun main() {}\n")
244 fpath = Path(f.name)
245 try:
246 parser.parse_file(fpath, fpath.parent, store)
247 assert store.create_node.call_args[0][0] == NodeLabel.File
248 assert store.create_node.call_args[0][1]["language"] == "kotlin"
249 finally:
250 fpath.unlink()
251
252
253 class TestKotlinHandleClass:
254 def test_creates_class_node(self):
255 parser = _make_kotlin_parser()
256 store = _make_store()
257 source = b"class Foo {}"
258 name_node = _text_node(b"Foo", "simple_identifier")
259 body = MockNode("class_body")
260 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
261 node.set_field("name", name_node)
262 node.set_field("body", body)
263 stats = {"functions": 0, "classes": 0, "edges": 0}
264 parser._handle_class(node, source, "Foo.kt", store, stats)
265 assert stats["classes"] == 1
266 assert store.create_node.call_args[0][0] == NodeLabel.Class
267
268 def test_skips_if_no_name(self):
269 parser = _make_kotlin_parser()
270 store = _make_store()
271 node = MockNode("class_declaration")
272 stats = {"functions": 0, "classes": 0, "edges": 0}
273 parser._handle_class(node, b"", "Foo.kt", store, stats)
274 assert stats["classes"] == 0
275
276 def test_walks_member_functions(self):
277 parser = _make_kotlin_parser()
278 store = _make_store()
279 source = b"class Foo { fun bar() {} }"
280 class_name = _text_node(b"Foo", "simple_identifier")
281 fn_name = _text_node(b"bar", "simple_identifier")
282 fn_node = MockNode("function_declaration", start_point=(0, 12), end_point=(0, 24))
283 fn_node.set_field("name", fn_name)
284 body = MockNode("class_body", children=[fn_node])
285 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 25))
286 node.set_field("name", class_name)
287 node.set_field("body", body)
288 stats = {"functions": 0, "classes": 0, "edges": 0}
289 parser._handle_class(node, source, "Foo.kt", store, stats)
290 assert stats["classes"] == 1
291 assert stats["functions"] == 1
292
293
294 class TestKotlinHandleFunction:
295 def test_creates_function_node(self):
296 parser = _make_kotlin_parser()
297 store = _make_store()
298 source = b"fun greet() {}"
299 name_node = _text_node(b"greet", "simple_identifier")
300 node = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 13))
301 node.set_field("name", name_node)
302 stats = {"functions": 0, "classes": 0, "edges": 0}
303 parser._handle_function(node, source, "Foo.kt", store, stats, class_name=None)
304 assert stats["functions"] == 1
305 assert store.create_node.call_args[0][0] == NodeLabel.Function
306
307 def test_creates_method_node_in_class(self):
308 parser = _make_kotlin_parser()
309 store = _make_store()
310 source = b"fun run() {}"
311 name_node = _text_node(b"run", "simple_identifier")
312 node = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 11))
313 node.set_field("name", name_node)
314 stats = {"functions": 0, "classes": 0, "edges": 0}
315 parser._handle_function(node, source, "Foo.kt", store, stats, class_name="Foo")
316 assert store.create_node.call_args[0][0] == NodeLabel.Method
317
318 def test_skips_if_no_name(self):
319 parser = _make_kotlin_parser()
320 store = _make_store()
321 node = MockNode("function_declaration")
322 stats = {"functions": 0, "classes": 0, "edges": 0}
323 parser._handle_function(node, b"", "Foo.kt", store, stats, class_name=None)
324 assert stats["functions"] == 0
325
326
327 class TestKotlinHandleImport:
328 def test_creates_import_node(self):
329 parser = _make_kotlin_parser()
330 store = _make_store()
331 source = b"import kotlin.collections.List"
332 node = MockNode("import_header", start_byte=0, end_byte=len(source), start_point=(0, 0))
333 stats = {"functions": 0, "classes": 0, "edges": 0}
334 parser._handle_import(node, source, "Foo.kt", store, stats)
335 assert stats["edges"] == 1
336 props = store.create_node.call_args[0][1]
337 assert "kotlin.collections.List" in props["name"]
338
339
340 # ── CSharpParser ───────────────────────────────────────────────────────────────
341
342
343 def _make_csharp_parser():
344 from navegador.ingestion.csharp import CSharpParser
345 p = CSharpParser.__new__(CSharpParser)
346 p._parser = MagicMock()
347 return p
348
349
350 class TestCSharpParserFileNode:
351 def test_parse_file_creates_file_node(self):
352 parser = _make_csharp_parser()
353 store = _make_store()
354 root = MockNode("compilation_unit")
355 parser._parser.parse.return_value = _make_mock_tree(root)
356 with tempfile.NamedTemporaryFile(suffix=".cs", delete=False) as f:
357 f.write(b"class Foo {}\n")
358 fpath = Path(f.name)
359 try:
360 parser.parse_file(fpath, fpath.parent, store)
361 assert store.create_node.call_args[0][0] == NodeLabel.File
362 assert store.create_node.call_args[0][1]["language"] == "csharp"
363 finally:
364 fpath.unlink()
365
366
367 class TestCSharpHandleClass:
368 def test_creates_class_node(self):
369 parser = _make_csharp_parser()
370 store = _make_store()
371 source = b"class Foo {}"
372 name_node = _text_node(b"Foo")
373 body = MockNode("declaration_list")
374 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
375 node.set_field("name", name_node)
376 node.set_field("body", body)
377 stats = {"functions": 0, "classes": 0, "edges": 0}
378 parser._handle_class(node, source, "Foo.cs", store, stats)
379 assert stats["classes"] == 1
380 assert store.create_node.call_args[0][0] == NodeLabel.Class
381
382 def test_skips_if_no_name(self):
383 parser = _make_csharp_parser()
384 store = _make_store()
385 node = MockNode("class_declaration")
386 stats = {"functions": 0, "classes": 0, "edges": 0}
387 parser._handle_class(node, b"", "Foo.cs", store, stats)
388 assert stats["classes"] == 0
389
390 def test_creates_inherits_edge(self):
391 parser = _make_csharp_parser()
392 store = _make_store()
393 source = b"class Child : Parent {}"
394 name_node = _text_node(b"Child")
395 parent_id = _text_node(b"Parent")
396 bases = MockNode("base_list", children=[parent_id])
397 body = MockNode("declaration_list")
398 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 22))
399 node.set_field("name", name_node)
400 node.set_field("bases", bases)
401 node.set_field("body", body)
402 stats = {"functions": 0, "classes": 0, "edges": 0}
403 parser._handle_class(node, source, "Child.cs", store, stats)
404 assert stats["edges"] == 2 # CONTAINS + INHERITS
405
406 def test_walks_methods(self):
407 parser = _make_csharp_parser()
408 store = _make_store()
409 source = b"class Foo { void Save() {} }"
410 class_name_node = _text_node(b"Foo")
411 method_name_node = _text_node(b"Save")
412 method = MockNode("method_declaration", start_point=(0, 12), end_point=(0, 25))
413 method.set_field("name", method_name_node)
414 body = MockNode("declaration_list", children=[method])
415 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 27))
416 node.set_field("name", class_name_node)
417 node.set_field("body", body)
418 stats = {"functions": 0, "classes": 0, "edges": 0}
419 parser._handle_class(node, source, "Foo.cs", store, stats)
420 assert stats["functions"] == 1
421
422
423 class TestCSharpHandleUsing:
424 def test_creates_import_node(self):
425 parser = _make_csharp_parser()
426 store = _make_store()
427 source = b"using System.Collections.Generic;"
428 node = MockNode("using_directive", start_byte=0, end_byte=len(source), start_point=(0, 0))
429 stats = {"functions": 0, "classes": 0, "edges": 0}
430 parser._handle_using(node, source, "Foo.cs", store, stats)
431 assert stats["edges"] == 1
432 props = store.create_node.call_args[0][1]
433 assert "System.Collections.Generic" in props["name"]
434
435
436 # ── PHPParser ─────────────────────────────────────────────────────────────────
437
438
439 def _make_php_parser():
440 from navegador.ingestion.php import PHPParser
441 p = PHPParser.__new__(PHPParser)
442 p._parser = MagicMock()
443 return p
444
445
446 class TestPHPParserFileNode:
447 def test_parse_file_creates_file_node(self):
448 parser = _make_php_parser()
449 store = _make_store()
450 root = MockNode("program")
451 parser._parser.parse.return_value = _make_mock_tree(root)
452 with tempfile.NamedTemporaryFile(suffix=".php", delete=False) as f:
453 f.write(b"<?php class Foo {} ?>\n")
454 fpath = Path(f.name)
455 try:
456 parser.parse_file(fpath, fpath.parent, store)
457 assert store.create_node.call_args[0][0] == NodeLabel.File
458 assert store.create_node.call_args[0][1]["language"] == "php"
459 finally:
460 fpath.unlink()
461
462
463 class TestPHPHandleClass:
464 def test_creates_class_node(self):
465 parser = _make_php_parser()
466 store = _make_store()
467 source = b"class Foo {}"
468 name_node = _text_node(b"Foo", "name")
469 body = MockNode("declaration_list")
470 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
471 node.set_field("name", name_node)
472 node.set_field("body", body)
473 stats = {"functions": 0, "classes": 0, "edges": 0}
474 parser._handle_class(node, source, "Foo.php", store, stats)
475 assert stats["classes"] == 1
476
477 def test_skips_if_no_name(self):
478 parser = _make_php_parser()
479 store = _make_store()
480 node = MockNode("class_declaration")
481 stats = {"functions": 0, "classes": 0, "edges": 0}
482 parser._handle_class(node, b"", "Foo.php", store, stats)
483 assert stats["classes"] == 0
484
485 def test_creates_inherits_edge(self):
486 parser = _make_php_parser()
487 store = _make_store()
488 source = b"class Child extends Parent {}"
489 name_node = _text_node(b"Child", "name")
490 parent_name_node = _text_node(b"Parent", "qualified_name")
491 base_clause = MockNode("base_clause", children=[parent_name_node])
492 body = MockNode("declaration_list")
493 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 28))
494 node.set_field("name", name_node)
495 node.set_field("base_clause", base_clause)
496 node.set_field("body", body)
497 stats = {"functions": 0, "classes": 0, "edges": 0}
498 parser._handle_class(node, source, "Child.php", store, stats)
499 assert stats["edges"] == 2 # CONTAINS + INHERITS
500
501
502 class TestPHPHandleFunction:
503 def test_creates_function_node(self):
504 parser = _make_php_parser()
505 store = _make_store()
506 source = b"function save() {}"
507 name_node = _text_node(b"save", "name")
508 node = MockNode("function_definition", start_point=(0, 0), end_point=(0, 17))
509 node.set_field("name", name_node)
510 stats = {"functions": 0, "classes": 0, "edges": 0}
511 parser._handle_function(node, source, "Foo.php", store, stats, class_name=None)
512 assert stats["functions"] == 1
513 assert store.create_node.call_args[0][0] == NodeLabel.Function
514
515 def test_skips_if_no_name(self):
516 parser = _make_php_parser()
517 store = _make_store()
518 node = MockNode("function_definition")
519 stats = {"functions": 0, "classes": 0, "edges": 0}
520 parser._handle_function(node, b"", "Foo.php", store, stats, class_name=None)
521 assert stats["functions"] == 0
522
523
524 class TestPHPHandleUse:
525 def test_creates_import_node(self):
526 parser = _make_php_parser()
527 store = _make_store()
528 source = b"use App\\Http\\Controllers\\Controller;"
529 node = MockNode("use_declaration", start_byte=0, end_byte=len(source), start_point=(0, 0))
530 stats = {"functions": 0, "classes": 0, "edges": 0}
531 parser._handle_use(node, source, "Foo.php", store, stats)
532 assert stats["edges"] == 1
533 props = store.create_node.call_args[0][1]
534 assert "Controller" in props["name"]
535
536
537 # ── RubyParser ────────────────────────────────────────────────────────────────
538
539
540 def _make_ruby_parser():
541 from navegador.ingestion.ruby import RubyParser
542 p = RubyParser.__new__(RubyParser)
543 p._parser = MagicMock()
544 return p
545
546
547 class TestRubyParserFileNode:
548 def test_parse_file_creates_file_node(self):
549 parser = _make_ruby_parser()
550 store = _make_store()
551 root = MockNode("program")
552 parser._parser.parse.return_value = _make_mock_tree(root)
553 with tempfile.NamedTemporaryFile(suffix=".rb", delete=False) as f:
554 f.write(b"class Foo; end\n")
555 fpath = Path(f.name)
556 try:
557 parser.parse_file(fpath, fpath.parent, store)
558 assert store.create_node.call_args[0][0] == NodeLabel.File
559 assert store.create_node.call_args[0][1]["language"] == "ruby"
560 finally:
561 fpath.unlink()
562
563
564 class TestRubyHandleClass:
565 def test_creates_class_node(self):
566 parser = _make_ruby_parser()
567 store = _make_store()
568 source = b"class Foo; end"
569 name_node = _text_node(b"Foo", "constant")
570 body = MockNode("body_statement")
571 node = MockNode("class", start_point=(0, 0), end_point=(0, 13))
572 node.set_field("name", name_node)
573 node.set_field("body", body)
574 stats = {"functions": 0, "classes": 0, "edges": 0}
575 parser._handle_class(node, source, "foo.rb", store, stats)
576 assert stats["classes"] == 1
577
578 def test_skips_if_no_name(self):
579 parser = _make_ruby_parser()
580 store = _make_store()
581 node = MockNode("class")
582 stats = {"functions": 0, "classes": 0, "edges": 0}
583 parser._handle_class(node, b"", "foo.rb", store, stats)
584 assert stats["classes"] == 0
585
586 def test_creates_inherits_edge(self):
587 parser = _make_ruby_parser()
588 store = _make_store()
589 source = b"class Child < Parent; end"
590 name_node = _text_node(b"Child", "constant")
591 superclass_node = _text_node(b"Parent", "constant")
592 body = MockNode("body_statement")
593 node = MockNode("class", start_point=(0, 0), end_point=(0, 24))
594 node.set_field("name", name_node)
595 node.set_field("superclass", superclass_node)
596 node.set_field("body", body)
597 stats = {"functions": 0, "classes": 0, "edges": 0}
598 parser._handle_class(node, source, "child.rb", store, stats)
599 assert stats["edges"] >= 2 # CONTAINS + INHERITS
600
601 def test_walks_body_methods(self):
602 parser = _make_ruby_parser()
603 store = _make_store()
604 source = b"class Foo; def run; end; end"
605 class_name_node = _text_node(b"Foo", "constant")
606 method_name_node = _text_node(b"run")
607 method_node = MockNode("method", start_point=(0, 11), end_point=(0, 22))
608 method_node.set_field("name", method_name_node)
609 body = MockNode("body_statement", children=[method_node])
610 node = MockNode("class", start_point=(0, 0), end_point=(0, 26))
611 node.set_field("name", class_name_node)
612 node.set_field("body", body)
613 stats = {"functions": 0, "classes": 0, "edges": 0}
614 parser._handle_class(node, source, "foo.rb", store, stats)
615 assert stats["functions"] == 1
616
617
618 class TestRubyHandleMethod:
619 def test_creates_function_node(self):
620 parser = _make_ruby_parser()
621 store = _make_store()
622 source = b"def run; end"
623 name_node = _text_node(b"run")
624 node = MockNode("method", start_point=(0, 0), end_point=(0, 11))
625 node.set_field("name", name_node)
626 stats = {"functions": 0, "classes": 0, "edges": 0}
627 parser._handle_method(node, source, "foo.rb", store, stats, class_name=None)
628 assert stats["functions"] == 1
629 assert store.create_node.call_args[0][0] == NodeLabel.Function
630
631 def test_creates_method_node_in_class(self):
632 parser = _make_ruby_parser()
633 store = _make_store()
634 source = b"def run; end"
635 name_node = _text_node(b"run")
636 node = MockNode("method", start_point=(0, 0), end_point=(0, 11))
637 node.set_field("name", name_node)
638 stats = {"functions": 0, "classes": 0, "edges": 0}
639 parser._handle_method(node, source, "foo.rb", store, stats, class_name="Foo")
640 assert store.create_node.call_args[0][0] == NodeLabel.Method
641
642 def test_skips_if_no_name(self):
643 parser = _make_ruby_parser()
644 store = _make_store()
645 node = MockNode("method")
646 stats = {"functions": 0, "classes": 0, "edges": 0}
647 parser._handle_method(node, b"", "foo.rb", store, stats, class_name=None)
648 assert stats["functions"] == 0
649
650
651 class TestRubyHandleModule:
652 def test_creates_module_node(self):
653 parser = _make_ruby_parser()
654 store = _make_store()
655 source = b"module Concerns; end"
656 name_node = _text_node(b"Concerns", "constant")
657 body = MockNode("body_statement")
658 node = MockNode("module", start_point=(0, 0), end_point=(0, 19))
659 node.set_field("name", name_node)
660 node.set_field("body", body)
661 stats = {"functions": 0, "classes": 0, "edges": 0}
662 parser._handle_module(node, source, "concerns.rb", store, stats)
663 assert stats["classes"] == 1
664 props = store.create_node.call_args[0][1]
665 assert props.get("docstring") == "module"
666
667
668 # ── SwiftParser ───────────────────────────────────────────────────────────────
669
670
671 def _make_swift_parser():
672 from navegador.ingestion.swift import SwiftParser
673 p = SwiftParser.__new__(SwiftParser)
674 p._parser = MagicMock()
675 return p
676
677
678 class TestSwiftParserFileNode:
679 def test_parse_file_creates_file_node(self):
680 parser = _make_swift_parser()
681 store = _make_store()
682 root = MockNode("source_file")
683 parser._parser.parse.return_value = _make_mock_tree(root)
684 with tempfile.NamedTemporaryFile(suffix=".swift", delete=False) as f:
685 f.write(b"class Foo {}\n")
686 fpath = Path(f.name)
687 try:
688 parser.parse_file(fpath, fpath.parent, store)
689 assert store.create_node.call_args[0][0] == NodeLabel.File
690 assert store.create_node.call_args[0][1]["language"] == "swift"
691 finally:
692 fpath.unlink()
693
694
695 class TestSwiftHandleClass:
696 def test_creates_class_node(self):
697 parser = _make_swift_parser()
698 store = _make_store()
699 source = b"class Foo {}"
700 name_node = _text_node(b"Foo", "type_identifier")
701 body = MockNode("class_body")
702 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 11))
703 node.set_field("name", name_node)
704 node.set_field("body", body)
705 stats = {"functions": 0, "classes": 0, "edges": 0}
706 parser._handle_class(node, source, "Foo.swift", store, stats)
707 assert stats["classes"] == 1
708
709 def test_skips_if_no_name(self):
710 parser = _make_swift_parser()
711 store = _make_store()
712 node = MockNode("class_declaration")
713 stats = {"functions": 0, "classes": 0, "edges": 0}
714 parser._handle_class(node, b"", "Foo.swift", store, stats)
715 assert stats["classes"] == 0
716
717 def test_creates_inherits_edge(self):
718 parser = _make_swift_parser()
719 store = _make_store()
720 source = b"class Child: Parent {}"
721 name_node = _text_node(b"Child", "type_identifier")
722 parent_id = _text_node(b"Parent", "type_identifier")
723 inheritance = MockNode("type_inheritance_clause", children=[parent_id])
724 body = MockNode("class_body")
725 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 21))
726 node.set_field("name", name_node)
727 node.set_field("type_inheritance_clause", inheritance)
728 node.set_field("body", body)
729 stats = {"functions": 0, "classes": 0, "edges": 0}
730 parser._handle_class(node, source, "Child.swift", store, stats)
731 assert stats["edges"] == 2 # CONTAINS + INHERITS
732
733 def test_walks_body_functions(self):
734 parser = _make_swift_parser()
735 store = _make_store()
736 source = b"class Foo { func run() {} }"
737 class_name = _text_node(b"Foo", "type_identifier")
738 fn_name = _text_node(b"run", "simple_identifier")
739 fn_node = MockNode("function_declaration", start_point=(0, 12), end_point=(0, 24))
740 fn_node.set_field("name", fn_name)
741 body = MockNode("class_body", children=[fn_node])
742 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 26))
743 node.set_field("name", class_name)
744 node.set_field("body", body)
745 stats = {"functions": 0, "classes": 0, "edges": 0}
746 parser._handle_class(node, source, "Foo.swift", store, stats)
747 assert stats["functions"] == 1
748
749
750 class TestSwiftHandleImport:
751 def test_creates_import_node(self):
752 parser = _make_swift_parser()
753 store = _make_store()
754 source = b"import Foundation"
755 node = MockNode("import_declaration", start_byte=0, end_byte=len(source), start_point=(0, 0))
756 stats = {"functions": 0, "classes": 0, "edges": 0}
757 parser._handle_import(node, source, "Foo.swift", store, stats)
758 assert stats["edges"] == 1
759 props = store.create_node.call_args[0][1]
760 assert "Foundation" in props["name"]
761
762
763 # ── CParser ───────────────────────────────────────────────────────────────────
764
765
766 def _make_c_parser():
767 from navegador.ingestion.c import CParser
768 p = CParser.__new__(CParser)
769 p._parser = MagicMock()
770 return p
771
772
773 class TestCParserFileNode:
774 def test_parse_file_creates_file_node(self):
775 parser = _make_c_parser()
776 store = _make_store()
777 root = MockNode("translation_unit")
778 parser._parser.parse.return_value = _make_mock_tree(root)
779 with tempfile.NamedTemporaryFile(suffix=".c", delete=False) as f:
780 f.write(b"void foo() {}\n")
781 fpath = Path(f.name)
782 try:
783 parser.parse_file(fpath, fpath.parent, store)
784 assert store.create_node.call_args[0][0] == NodeLabel.File
785 assert store.create_node.call_args[0][1]["language"] == "c"
786 finally:
787 fpath.unlink()
788
789
790 class TestCHandleFunction:
791 def _make_function_node(self, fn_name: bytes) -> tuple[MockNode, bytes]:
792 source = fn_name + b"(void) {}"
793 fn_id = _text_node(fn_name)
794 fn_decl = MockNode("function_declarator")
795 fn_decl.set_field("declarator", fn_id)
796 body = MockNode("compound_statement")
797 node = MockNode("function_definition", start_point=(0, 0), end_point=(0, len(source)))
798 node.set_field("declarator", fn_decl)
799 node.set_field("body", body)
800 return node, source
801
802 def test_creates_function_node(self):
803 parser = _make_c_parser()
804 store = _make_store()
805 node, source = self._make_function_node(b"main")
806 stats = {"functions": 0, "classes": 0, "edges": 0}
807 parser._handle_function(node, source, "main.c", store, stats)
808 assert stats["functions"] == 1
809 assert store.create_node.call_args[0][0] == NodeLabel.Function
810
811 def test_skips_if_no_name(self):
812 parser = _make_c_parser()
813 store = _make_store()
814 node = MockNode("function_definition")
815 stats = {"functions": 0, "classes": 0, "edges": 0}
816 parser._handle_function(node, b"", "main.c", store, stats)
817 assert stats["functions"] == 0
818
819
820 class TestCHandleStruct:
821 def test_creates_struct_node(self):
822 parser = _make_c_parser()
823 store = _make_store()
824 source = b"struct Point { int x; int y; };"
825 name_node = _text_node(b"Point", "type_identifier")
826 node = MockNode("struct_specifier", start_point=(0, 0), end_point=(0, 30))
827 node.set_field("name", name_node)
828 stats = {"functions": 0, "classes": 0, "edges": 0}
829 parser._handle_struct(node, source, "point.c", store, stats)
830 assert stats["classes"] == 1
831 props = store.create_node.call_args[0][1]
832 assert props["docstring"] == "struct"
833
834 def test_skips_if_no_name(self):
835 parser = _make_c_parser()
836 store = _make_store()
837 node = MockNode("struct_specifier")
838 stats = {"functions": 0, "classes": 0, "edges": 0}
839 parser._handle_struct(node, b"", "point.c", store, stats)
840 assert stats["classes"] == 0
841
842
843 class TestCHandleInclude:
844 def test_creates_import_node_angle_bracket(self):
845 parser = _make_c_parser()
846 store = _make_store()
847 source = b"#include <stdio.h>"
848 path_node = MockNode("system_lib_string", start_byte=9, end_byte=18)
849 node = MockNode("preproc_include", start_byte=0, end_byte=18, start_point=(0, 0))
850 node.set_field("path", path_node)
851 stats = {"functions": 0, "classes": 0, "edges": 0}
852 parser._handle_include(node, source, "main.c", store, stats)
853 assert stats["edges"] == 1
854 props = store.create_node.call_args[0][1]
855 assert "stdio.h" in props["name"]
856
857 def test_creates_import_node_quoted(self):
858 parser = _make_c_parser()
859 store = _make_store()
860 source = b'#include "utils.h"'
861 path_node = MockNode("string_literal", start_byte=9, end_byte=18)
862 node = MockNode("preproc_include", start_byte=0, end_byte=18, start_point=(0, 0))
863 node.set_field("path", path_node)
864 stats = {"functions": 0, "classes": 0, "edges": 0}
865 parser._handle_include(node, source, "main.c", store, stats)
866 assert stats["edges"] == 1
867 props = store.create_node.call_args[0][1]
868 assert "utils.h" in props["name"]
869
870
871 # ── CppParser ─────────────────────────────────────────────────────────────────
872
873
874 def _make_cpp_parser():
875 from navegador.ingestion.cpp import CppParser
876 p = CppParser.__new__(CppParser)
877 p._parser = MagicMock()
878 return p
879
880
881 class TestCppParserFileNode:
882 def test_parse_file_creates_file_node(self):
883 parser = _make_cpp_parser()
884 store = _make_store()
885 root = MockNode("translation_unit")
886 parser._parser.parse.return_value = _make_mock_tree(root)
887 with tempfile.NamedTemporaryFile(suffix=".cpp", delete=False) as f:
888 f.write(b"class Foo {};\n")
889 fpath = Path(f.name)
890 try:
891 parser.parse_file(fpath, fpath.parent, store)
892 assert store.create_node.call_args[0][0] == NodeLabel.File
893 assert store.create_node.call_args[0][1]["language"] == "cpp"
894 finally:
895 fpath.unlink()
896
897
898 class TestCppHandleClass:
899 def test_creates_class_node(self):
900 parser = _make_cpp_parser()
901 store = _make_store()
902 source = b"class Foo {};"
903 name_node = _text_node(b"Foo", "type_identifier")
904 body = MockNode("field_declaration_list")
905 node = MockNode("class_specifier", start_point=(0, 0), end_point=(0, 12))
906 node.set_field("name", name_node)
907 node.set_field("body", body)
908 stats = {"functions": 0, "classes": 0, "edges": 0}
909 parser._handle_class(node, source, "Foo.cpp", store, stats)
910 assert stats["classes"] == 1
911
912 def test_skips_if_no_name(self):
913 parser = _make_cpp_parser()
914 store = _make_store()
915 node = MockNode("class_specifier")
916 stats = {"functions": 0, "classes": 0, "edges": 0}
917 parser._handle_class(node, b"", "Foo.cpp", store, stats)
918 assert stats["classes"] == 0
919
920 def test_creates_inherits_edge(self):
921 parser = _make_cpp_parser()
922 store = _make_store()
923 source = b"class Child : public Parent {};"
924 name_node = _text_node(b"Child", "type_identifier")
925 parent_id = _text_node(b"Parent", "type_identifier")
926 base_clause = MockNode("base_class_clause", children=[parent_id])
927 body = MockNode("field_declaration_list")
928 node = MockNode("class_specifier", start_point=(0, 0), end_point=(0, 30))
929 node.set_field("name", name_node)
930 node.set_field("base_clause", base_clause)
931 node.set_field("body", body)
932 stats = {"functions": 0, "classes": 0, "edges": 0}
933 parser._handle_class(node, source, "Child.cpp", store, stats)
934 assert stats["edges"] == 2 # CONTAINS + INHERITS
935
936 def test_walks_member_functions(self):
937 parser = _make_cpp_parser()
938 store = _make_store()
939 source = b"class Foo { void run() {} };"
940 class_name = _text_node(b"Foo", "type_identifier")
941 fn_id = _text_node(b"run")
942 fn_decl = MockNode("function_declarator")
943 fn_decl.set_field("declarator", fn_id)
944 fn_body = MockNode("compound_statement")
945 fn_node = MockNode("function_definition", start_point=(0, 12), end_point=(0, 24))
946 fn_node.set_field("declarator", fn_decl)
947 fn_node.set_field("body", fn_body)
948 body = MockNode("field_declaration_list", children=[fn_node])
949 node = MockNode("class_specifier", start_point=(0, 0), end_point=(0, 27))
950 node.set_field("name", class_name)
951 node.set_field("body", body)
952 stats = {"functions": 0, "classes": 0, "edges": 0}
953 parser._handle_class(node, source, "Foo.cpp", store, stats)
954 assert stats["functions"] == 1
955
956
957 class TestCppHandleFunction:
958 def test_creates_function_node(self):
959 parser = _make_cpp_parser()
960 store = _make_store()
961 source = b"void main() {}"
962 fn_id = _text_node(b"main")
963 fn_decl = MockNode("function_declarator")
964 fn_decl.set_field("declarator", fn_id)
965 body = MockNode("compound_statement")
966 node = MockNode("function_definition", start_point=(0, 0), end_point=(0, 13))
967 node.set_field("declarator", fn_decl)
968 node.set_field("body", body)
969 stats = {"functions": 0, "classes": 0, "edges": 0}
970 parser._handle_function(node, source, "main.cpp", store, stats, class_name=None)
971 assert stats["functions"] == 1
972 assert store.create_node.call_args[0][0] == NodeLabel.Function
973
974 def test_creates_method_node_in_class(self):
975 parser = _make_cpp_parser()
976 store = _make_store()
977 source = b"void run() {}"
978 fn_id = _text_node(b"run")
979 fn_decl = MockNode("function_declarator")
980 fn_decl.set_field("declarator", fn_id)
981 body = MockNode("compound_statement")
982 node = MockNode("function_definition", start_point=(0, 0), end_point=(0, 12))
983 node.set_field("declarator", fn_decl)
984 node.set_field("body", body)
985 stats = {"functions": 0, "classes": 0, "edges": 0}
986 parser._handle_function(node, source, "Foo.cpp", store, stats, class_name="Foo")
987 assert store.create_node.call_args[0][0] == NodeLabel.Method
988
989 def test_skips_if_no_name(self):
990 parser = _make_cpp_parser()
991 store = _make_store()
992 node = MockNode("function_definition")
993 stats = {"functions": 0, "classes": 0, "edges": 0}
994 parser._handle_function(node, b"", "main.cpp", store, stats, class_name=None)
995 assert stats["functions"] == 0
996
997
998 class TestCppHandleInclude:
999 def test_creates_import_node(self):
1000 parser = _make_cpp_parser()
1001 store = _make_store()
1002 source = b"#include <vector>"
1003 path_node = MockNode("system_lib_string", start_byte=9, end_byte=17)
1004 node = MockNode("preproc_include", start_byte=0, end_byte=17, start_point=(0, 0))
1005 node.set_field("path", path_node)
1006 stats = {"functions": 0, "classes": 0, "edges": 0}
1007 parser._handle_include(node, source, "Foo.cpp", store, stats)
1008 assert stats["edges"] == 1
1009 props = store.create_node.call_args[0][1]
1010 assert "vector" in props["name"]
1011
1012
1013 class TestCppExtractFunctionName:
1014 def test_simple_identifier(self):
1015 from navegador.ingestion.cpp import CppParser
1016 parser = CppParser.__new__(CppParser)
1017 source = b"foo"
1018 node = _text_node(b"foo")
1019 assert parser._extract_function_name(node, source) == "foo"
1020
1021 def test_function_declarator(self):
1022 from navegador.ingestion.cpp import CppParser
1023 parser = CppParser.__new__(CppParser)
1024 source = b"foo"
1025 fn_id = _text_node(b"foo")
1026 fn_decl = MockNode("function_declarator")
1027 fn_decl.set_field("declarator", fn_id)
1028 assert parser._extract_function_name(fn_decl, source) == "foo"
1029
1030 def test_none_input(self):
1031 from navegador.ingestion.cpp import CppParser
1032 parser = CppParser.__new__(CppParser)
1033 assert parser._extract_function_name(None, b"") is None

Keyboard Shortcuts

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