Navegador

Blame History Raw 250 lines
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
raise ImportError("Install tree-sitter-php: pip install tree-sitter-php") from e
28
29
30
def _node_text(node, source: bytes) -> str:
31
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
32
33
34
class PHPParser(LanguageParser):
35
"""Parses PHP source files into the navegador graph."""
36
37
def __init__(self) -> None:
38
from tree_sitter import Parser # type: ignore[import]
39
40
self._parser = Parser(_get_php_language())
41
42
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
43
source = path.read_bytes()
44
tree = self._parser.parse(source)
45
rel_path = str(path.relative_to(repo_root))
46
47
store.create_node(
48
NodeLabel.File,
49
{
50
"name": path.name,
51
"path": rel_path,
52
"language": "php",
53
"line_count": source.count(b"\n"),
54
},
55
)
56
57
stats = {"functions": 0, "classes": 0, "edges": 0}
58
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
59
return stats
60
61
# ── AST walker ────────────────────────────────────────────────────────────
62
63
def _walk(
64
self,
65
node,
66
source: bytes,
67
file_path: str,
68
store: GraphStore,
69
stats: dict,
70
class_name: str | None,
71
) -> None:
72
if node.type in (
73
"class_declaration",
74
"interface_declaration",
75
"trait_declaration",
76
):
77
self._handle_class(node, source, file_path, store, stats)
78
return
79
if node.type == "function_definition":
80
self._handle_function(node, source, file_path, store, stats, class_name)
81
return
82
if node.type == "use_declaration":
83
self._handle_use(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((c for c in node.children if c.type == "name"), None)
96
if not name_node:
97
return
98
name = _node_text(name_node, source)
99
100
store.create_node(
101
NodeLabel.Class,
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
},
109
)
110
store.create_edge(
111
NodeLabel.File,
112
{"path": file_path},
113
EdgeType.CONTAINS,
114
NodeLabel.Class,
115
{"name": name, "file_path": file_path},
116
)
117
stats["classes"] += 1
118
stats["edges"] += 1
119
120
# Superclass
121
base_clause = node.child_by_field_name("base_clause")
122
if base_clause:
123
for child in base_clause.children:
124
if child.type in ("qualified_name", "name"):
125
parent_name = _node_text(child, source)
126
store.create_edge(
127
NodeLabel.Class,
128
{"name": name, "file_path": file_path},
129
EdgeType.INHERITS,
130
NodeLabel.Class,
131
{"name": parent_name, "file_path": file_path},
132
)
133
stats["edges"] += 1
134
break
135
136
# Walk class body for methods
137
body = node.child_by_field_name("body")
138
if not body:
139
body = next((c for c in node.children if c.type == "declaration_list"), None)
140
if body:
141
for child in body.children:
142
if child.type == "method_declaration":
143
self._handle_function(child, source, file_path, store, stats, class_name=name)
144
145
def _handle_function(
146
self,
147
node,
148
source: bytes,
149
file_path: str,
150
store: GraphStore,
151
stats: dict,
152
class_name: str | None,
153
) -> None:
154
name_node = node.child_by_field_name("name")
155
if not name_node:
156
name_node = next((c for c in node.children if c.type == "name"), None)
157
if not name_node:
158
return
159
name = _node_text(name_node, source)
160
161
label = NodeLabel.Method if class_name else NodeLabel.Function
162
store.create_node(
163
label,
164
{
165
"name": name,
166
"file_path": file_path,
167
"line_start": node.start_point[0] + 1,
168
"line_end": node.end_point[0] + 1,
169
"docstring": "",
170
"class_name": class_name or "",
171
},
172
)
173
174
container_label = NodeLabel.Class if class_name else NodeLabel.File
175
container_key = (
176
{"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
177
)
178
store.create_edge(
179
container_label,
180
container_key,
181
EdgeType.CONTAINS,
182
label,
183
{"name": name, "file_path": file_path},
184
)
185
stats["functions"] += 1
186
stats["edges"] += 1
187
188
self._extract_calls(node, source, file_path, name, label, store, stats)
189
190
def _handle_use(
191
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
192
) -> None:
193
raw = _node_text(node, source).strip()
194
module = raw.removeprefix("use").removesuffix(";").strip()
195
if not module:
196
return
197
store.create_node(
198
NodeLabel.Import,
199
{
200
"name": module,
201
"file_path": file_path,
202
"line_start": node.start_point[0] + 1,
203
"module": module,
204
},
205
)
206
store.create_edge(
207
NodeLabel.File,
208
{"path": file_path},
209
EdgeType.IMPORTS,
210
NodeLabel.Import,
211
{"name": module, "file_path": file_path},
212
)
213
stats["edges"] += 1
214
215
def _extract_calls(
216
self,
217
fn_node,
218
source: bytes,
219
file_path: str,
220
fn_name: str,
221
fn_label: str,
222
store: GraphStore,
223
stats: dict,
224
) -> None:
225
def walk(node):
226
if node.type == "function_call_expression":
227
func = node.child_by_field_name("function")
228
if not func:
229
func = next(
230
(c for c in node.children if c.type in ("name", "qualified_name")), None
231
)
232
if func:
233
callee = _node_text(func, source).split("\\")[-1]
234
store.create_edge(
235
fn_label,
236
{"name": fn_name, "file_path": file_path},
237
EdgeType.CALLS,
238
NodeLabel.Function,
239
{"name": callee, "file_path": file_path},
240
)
241
stats["edges"] += 1
242
for child in node.children:
243
walk(child)
244
245
body = fn_node.child_by_field_name("body")
246
if not body:
247
body = next((c for c in fn_node.children if c.type == "compound_statement"), None)
248
if body:
249
walk(body)
250

Keyboard Shortcuts

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