1use super::{add_node_if_missing, make_file_node};
2use crate::error::{CodeSynapseError, Result};
3use crate::extract::{make_id, ImportNode, LanguageExtractor};
4use crate::types::{Edge, ExtractionFragment, Node};
5use std::collections::HashMap;
6use std::path::Path;
7use tree_sitter::{Node as TsNode, Parser};
8
9pub struct TsPhpExtractor;
10
11fn ph_text(source: &[u8], node: &TsNode<'_>) -> String {
12 std::str::from_utf8(&source[node.start_byte()..node.end_byte()])
13 .unwrap_or("")
14 .trim()
15 .to_string()
16}
17
18fn ph_node(id: String, label: String, file_type: &str, path: &Path) -> Node {
19 Node {
20 id,
21 label,
22 file_type: file_type.to_string(),
23 source_file: path.to_string_lossy().to_string(),
24 source_location: None,
25 community: None,
26 rationale: None,
27 docstring: None,
28 metadata: HashMap::new(),
29 }
30}
31
32fn simple_name(s: &str) -> &str {
33 s.split('\\').next_back().unwrap_or(s)
34}
35
36impl TsPhpExtractor {
37 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
38 let (file_id, _, file_node) = make_file_node(path);
39 let mut fragment = ExtractionFragment {
40 nodes: vec![file_node],
41 edges: vec![],
42 };
43
44 let lang = tree_sitter_php::LANGUAGE_PHP_ONLY.into();
45 let mut parser = Parser::new();
46 parser
47 .set_language(&lang)
48 .map_err(|e| CodeSynapseError::Parse(format!("php lang: {}", e)))?;
49
50 let tree = parser
51 .parse(source, None)
52 .ok_or_else(|| CodeSynapseError::Parse("php parse failed".to_string()))?;
53
54 let stem = file_id.clone();
55
56 Self::walk(
57 tree.root_node(),
58 source,
59 path,
60 &file_id,
61 &stem,
62 None,
63 &mut fragment,
64 );
65
66 Ok(fragment)
67 }
68
69 fn walk(
70 node: TsNode<'_>,
71 source: &[u8],
72 path: &Path,
73 file_id: &str,
74 stem: &str,
75 class_id: Option<&str>,
76 fragment: &mut ExtractionFragment,
77 ) {
78 match node.kind() {
79 "namespace_use_clause" | "namespace_use_declaration" => {
80 let text = ph_text(source, &node);
82 let leaf = text
83 .split('\\')
84 .next_back()
85 .unwrap_or(&text)
86 .trim()
87 .to_string();
88 if !leaf.is_empty() && leaf != "use" {
89 let id = make_id(&[&leaf]);
90 add_node_if_missing(fragment, ph_node(id.clone(), leaf, "module", path));
91 fragment.edges.push(Edge {
92 source: file_id.to_string(),
93 target: id,
94 relation: "imports".to_string(),
95 confidence: "EXTRACTED".to_string(),
96 source_file: Some(path.to_string_lossy().to_string()),
97 weight: 1.0,
98 context: Some("import".to_string()),
99 });
100 }
101 }
102 "class_declaration" => {
103 let name_node = node.child_by_field_name("name");
104 let class_name = match name_node {
105 Some(n) => {
106 let t = ph_text(source, &n);
107 simple_name(&t).to_string()
108 }
109 None => return,
110 };
111 let new_class_id = make_id(&[stem, &class_name]);
112 add_node_if_missing(
113 fragment,
114 ph_node(new_class_id.clone(), class_name.clone(), "class", path),
115 );
116 fragment.edges.push(Edge {
117 source: file_id.to_string(),
118 target: new_class_id.clone(),
119 relation: "contains".to_string(),
120 confidence: "EXTRACTED".to_string(),
121 source_file: Some(path.to_string_lossy().to_string()),
122 weight: 1.0,
123 context: None,
124 });
125
126 for i in 0..node.child_count() {
128 if let Some(child) = node.child(i) {
129 match child.kind() {
130 "base_clause" => {
131 Self::collect_names_as(
132 &child,
133 source,
134 path,
135 &new_class_id,
136 fragment,
137 "inherits",
138 stem,
139 );
140 }
141 "class_interface_clause" => {
142 Self::collect_names_as(
143 &child,
144 source,
145 path,
146 &new_class_id,
147 fragment,
148 "implements",
149 stem,
150 );
151 }
152 _ => {}
153 }
154 }
155 }
156
157 if let Some(body) = node.child_by_field_name("body") {
159 for i in 0..body.child_count() {
160 if let Some(child) = body.child(i) {
161 Self::walk(
162 child,
163 source,
164 path,
165 file_id,
166 stem,
167 Some(&new_class_id),
168 fragment,
169 );
170 }
171 }
172 }
173 }
174 "use_declaration" => {
175 if let Some(owner) = class_id {
177 for i in 0..node.child_count() {
178 if let Some(child) = node.child(i) {
179 if child.kind() == "name" || child.kind() == "qualified_name" {
180 let trait_name = simple_name(&ph_text(source, &child)).to_string();
181 let trait_id = make_id(&[stem, &trait_name]);
182 add_node_if_missing(
183 fragment,
184 ph_node(trait_id.clone(), trait_name, "code", path),
185 );
186 fragment.edges.push(Edge {
187 source: owner.to_string(),
188 target: trait_id,
189 relation: "mixes_in".to_string(),
190 confidence: "EXTRACTED".to_string(),
191 source_file: Some(path.to_string_lossy().to_string()),
192 weight: 1.0,
193 context: None,
194 });
195 }
196 }
197 }
198 }
199 }
200 "method_declaration" => {
201 let owner = class_id.unwrap_or(file_id);
202 Self::extract_method(node, source, path, file_id, stem, owner, fragment);
203 }
204 "function_definition" => {
205 if class_id.is_none() {
206 if let Some(name_node) = node.child_by_field_name("name") {
208 let name = ph_text(source, &name_node);
209 let id = make_id(&[stem, &name, "()"]);
210 add_node_if_missing(
211 fragment,
212 ph_node(id.clone(), format!("{}()", name), "function", path),
213 );
214 fragment.edges.push(Edge {
215 source: file_id.to_string(),
216 target: id,
217 relation: "contains".to_string(),
218 confidence: "EXTRACTED".to_string(),
219 source_file: Some(path.to_string_lossy().to_string()),
220 weight: 1.0,
221 context: None,
222 });
223 }
224 }
225 }
226 "property_declaration" => {
227 if let Some(owner) = class_id {
228 Self::extract_property(node, source, path, owner, fragment, stem);
229 }
230 }
231 _ => {
232 for i in 0..node.child_count() {
233 if let Some(child) = node.child(i) {
234 Self::walk(child, source, path, file_id, stem, class_id, fragment);
235 }
236 }
237 }
238 }
239 }
240
241 fn collect_names_as(
242 node: &TsNode<'_>,
243 source: &[u8],
244 path: &Path,
245 source_id: &str,
246 fragment: &mut ExtractionFragment,
247 relation: &str,
248 stem: &str,
249 ) {
250 for i in 0..node.child_count() {
251 if let Some(child) = node.child(i) {
252 if child.kind() == "name" || child.kind() == "qualified_name" {
253 let name = simple_name(&ph_text(source, &child)).to_string();
254 let id = make_id(&[stem, &name]);
255 add_node_if_missing(fragment, ph_node(id.clone(), name, "code", path));
256 fragment.edges.push(Edge {
257 source: source_id.to_string(),
258 target: id,
259 relation: relation.to_string(),
260 confidence: "EXTRACTED".to_string(),
261 source_file: Some(path.to_string_lossy().to_string()),
262 weight: 1.0,
263 context: None,
264 });
265 } else {
266 Self::collect_names_as(
267 &child, source, path, source_id, fragment, relation, stem,
268 );
269 }
270 }
271 }
272 }
273
274 fn extract_method(
275 node: TsNode<'_>,
276 source: &[u8],
277 path: &Path,
278 file_id: &str,
279 stem: &str,
280 owner_id: &str,
281 fragment: &mut ExtractionFragment,
282 ) {
283 let name_node = match node.child_by_field_name("name") {
284 Some(n) => n,
285 None => return,
286 };
287 let method_name = ph_text(source, &name_node);
288 let method_id = make_id(&[stem, &method_name, "()"]);
289 add_node_if_missing(
290 fragment,
291 ph_node(
292 method_id.clone(),
293 format!("{}()", method_name),
294 "method",
295 path,
296 ),
297 );
298 fragment.edges.push(Edge {
299 source: owner_id.to_string(),
300 target: method_id.clone(),
301 relation: "method".to_string(),
302 confidence: "EXTRACTED".to_string(),
303 source_file: Some(path.to_string_lossy().to_string()),
304 weight: 1.0,
305 context: None,
306 });
307 fragment.edges.push(Edge {
308 source: file_id.to_string(),
309 target: method_id.clone(),
310 relation: "contains".to_string(),
311 confidence: "EXTRACTED".to_string(),
312 source_file: Some(path.to_string_lossy().to_string()),
313 weight: 1.0,
314 context: None,
315 });
316
317 if let Some(ret) = node.child_by_field_name("return_type") {
319 Self::emit_type_ref_ph(
320 &ret,
321 source,
322 path,
323 &method_id,
324 fragment,
325 "return_type",
326 stem,
327 );
328 }
329
330 if let Some(params) = node.child_by_field_name("parameters") {
332 for i in 0..params.child_count() {
333 if let Some(param) = params.child(i) {
334 if param.kind() == "simple_parameter"
335 || param.kind() == "property_promotion_parameter"
336 {
337 if let Some(ptype) = param.child_by_field_name("type") {
338 Self::emit_type_ref_ph(
339 &ptype,
340 source,
341 path,
342 &method_id,
343 fragment,
344 "parameter_type",
345 stem,
346 );
347 }
348 }
349 }
350 }
351 }
352
353 if let Some(body) = node.child_by_field_name("body") {
355 Self::walk_calls_ph(body, source, path, &method_id, fragment, stem);
356 }
357 }
358
359 fn extract_property(
360 node: TsNode<'_>,
361 source: &[u8],
362 path: &Path,
363 class_id: &str,
364 fragment: &mut ExtractionFragment,
365 stem: &str,
366 ) {
367 if let Some(type_node) = node.child_by_field_name("type") {
368 Self::emit_type_ref_ph(&type_node, source, path, class_id, fragment, "field", stem);
369 }
370 }
371
372 fn emit_type_ref_ph(
373 type_node: &TsNode<'_>,
374 source: &[u8],
375 path: &Path,
376 owner_id: &str,
377 fragment: &mut ExtractionFragment,
378 context: &str,
379 stem: &str,
380 ) {
381 match type_node.kind() {
382 "named_type" | "name" | "qualified_name" => {
383 let name = simple_name(&ph_text(source, type_node)).to_string();
384 if !name.is_empty() {
385 let id = make_id(&[stem, &name]);
386 add_node_if_missing(fragment, ph_node(id.clone(), name, "code", path));
387 fragment.edges.push(Edge {
388 source: owner_id.to_string(),
389 target: id,
390 relation: "references".to_string(),
391 confidence: "EXTRACTED".to_string(),
392 source_file: Some(path.to_string_lossy().to_string()),
393 weight: 1.0,
394 context: Some(context.to_string()),
395 });
396 }
397 }
398 _ => {
399 for i in 0..type_node.child_count() {
400 if let Some(child) = type_node.child(i) {
401 Self::emit_type_ref_ph(
402 &child, source, path, owner_id, fragment, context, stem,
403 );
404 }
405 }
406 }
407 }
408 }
409
410 fn walk_calls_ph(
411 node: TsNode<'_>,
412 source: &[u8],
413 path: &Path,
414 caller_id: &str,
415 fragment: &mut ExtractionFragment,
416 stem: &str,
417 ) {
418 if node.kind() == "function_call_expression" || node.kind() == "member_call_expression" {
419 let func_name = node
420 .child_by_field_name("function")
421 .or_else(|| node.child_by_field_name("name"))
422 .map(|n| ph_text(source, &n));
423 if let Some(name) = func_name {
424 let callee_id = make_id(&[stem, &name, "()"]);
425 if fragment.nodes.iter().any(|n| n.id == callee_id) {
426 let already = fragment.edges.iter().any(|e| {
427 e.relation == "calls" && e.source == caller_id && e.target == callee_id
428 });
429 if !already && caller_id != callee_id.as_str() {
430 fragment.edges.push(Edge {
431 source: caller_id.to_string(),
432 target: callee_id,
433 relation: "calls".to_string(),
434 confidence: "EXTRACTED".to_string(),
435 source_file: Some(path.to_string_lossy().to_string()),
436 weight: 1.0,
437 context: Some("call".to_string()),
438 });
439 }
440 }
441 }
442 }
443 for i in 0..node.child_count() {
444 if let Some(child) = node.child(i) {
445 Self::walk_calls_ph(child, source, path, caller_id, fragment, stem);
446 }
447 }
448 }
449}
450
451impl LanguageExtractor for TsPhpExtractor {
452 fn file_extensions(&self) -> Vec<&'static str> {
453 vec!["php"]
454 }
455 fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
456 Self::extract(source, path)
457 }
458 fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
459 vec![]
460 }
461 fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
462}