1use std::collections::HashMap;
34use std::path::Path;
35
36use sqry_core::graph::unified::edge::kind::{FfiConvention, TypeOfContext};
37use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
38use sqry_core::graph::{
39 GraphBuilder, GraphBuilderError, GraphResult, GraphSnapshot, Language, Span,
40};
41use tree_sitter::{Node, Tree};
42
43use super::phpdoc_parser::{extract_phpdoc_comment, parse_phpdoc_tags};
44use super::type_extractor::{canonical_type_string, extract_type_names};
45
46const DEFAULT_MAX_SCOPE_DEPTH: usize = 5;
48
49#[derive(Debug)]
51pub struct PhpGraphBuilder {
52 pub max_scope_depth: usize,
53}
54
55impl Default for PhpGraphBuilder {
56 fn default() -> Self {
57 Self {
58 max_scope_depth: DEFAULT_MAX_SCOPE_DEPTH,
59 }
60 }
61}
62
63impl GraphBuilder for PhpGraphBuilder {
64 fn build_graph(
65 &self,
66 tree: &Tree,
67 content: &[u8],
68 file: &Path,
69 staging: &mut StagingGraph,
70 ) -> GraphResult<()> {
71 let mut helper = GraphBuildHelper::new(staging, file, Language::Php);
72
73 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
75 GraphBuilderError::ParseError {
76 span: Span::default(),
77 reason: e,
78 }
79 })?;
80
81 let mut node_map = HashMap::new();
83
84 for context in ast_graph.contexts() {
86 let qualified_name = &context.qualified_name;
87 let span = Span::from_bytes(context.span.0, context.span.1);
88
89 let node_id = match &context.kind {
90 ContextKind::Function { is_async } => helper.add_function_with_signature(
91 qualified_name,
92 Some(span),
93 *is_async,
94 false, None, context.return_type.as_deref(),
97 ),
98 ContextKind::Method {
99 is_async,
100 is_static,
101 visibility: _,
102 } => {
103 helper.add_method_with_signature(
107 qualified_name,
108 Some(span),
109 *is_async,
110 *is_static,
111 None, context.return_type.as_deref(),
113 )
114 }
115 ContextKind::Class => helper.add_class(qualified_name, Some(span)),
116 };
117 node_map.insert(qualified_name.clone(), node_id);
118 }
119
120 let root = tree.root_node();
122 walk_tree_for_edges(root, content, &ast_graph, &mut helper, &mut node_map)?;
123
124 process_oop_relationships(root, content, &mut helper, &mut node_map);
126
127 process_exports(root, content, &mut helper, &mut node_map);
130
131 process_phpdoc_annotations(root, content, &mut helper)?;
133
134 Ok(())
135 }
136
137 fn language(&self) -> Language {
138 Language::Php
139 }
140
141 fn detect_cross_language_edges(
142 &self,
143 _snapshot: &GraphSnapshot,
144 ) -> GraphResult<Vec<sqry_core::graph::CodeEdge>> {
145 Ok(vec![])
148 }
149}
150
151#[derive(Debug, Clone)]
156enum ContextKind {
157 Function {
158 is_async: bool,
159 },
160 Method {
161 is_async: bool,
162 is_static: bool,
163 #[allow(dead_code)] visibility: Option<String>,
165 },
166 Class,
167}
168
169#[derive(Debug, Clone)]
170struct CallContext {
171 qualified_name: String,
172 span: (usize, usize),
173 kind: ContextKind,
174 class_name: Option<String>,
175 return_type: Option<String>,
176}
177
178struct ASTGraph {
179 contexts: Vec<CallContext>,
180 node_to_context: HashMap<usize, usize>,
181}
182
183impl ASTGraph {
184 fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Result<Self, String> {
185 let mut contexts = Vec::new();
186 let mut node_to_context = HashMap::new();
187 let mut scope_stack: Vec<String> = Vec::new();
188 let mut class_stack: Vec<String> = Vec::new();
189
190 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
192 .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
193 let file_ops_depth = recursion_limits
194 .effective_file_ops_depth()
195 .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
196 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
197 .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
198
199 let mut walk_ctx = WalkContext {
200 contexts: &mut contexts,
201 node_to_context: &mut node_to_context,
202 scope_stack: &mut scope_stack,
203 class_stack: &mut class_stack,
204 max_depth,
205 };
206
207 walk_ast(tree.root_node(), content, &mut walk_ctx, &mut guard)?;
208
209 Ok(Self {
210 contexts,
211 node_to_context,
212 })
213 }
214
215 fn contexts(&self) -> &[CallContext] {
216 &self.contexts
217 }
218
219 fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
220 self.node_to_context
221 .get(&node_id)
222 .and_then(|idx| self.contexts.get(*idx))
223 }
224}
225
226#[allow(
227 clippy::too_many_lines,
228 reason = "PHP namespace and scope handling requires a large, unified traversal."
229)]
230struct WalkContext<'a> {
235 contexts: &'a mut Vec<CallContext>,
236 node_to_context: &'a mut HashMap<usize, usize>,
237 scope_stack: &'a mut Vec<String>,
238 class_stack: &'a mut Vec<String>,
239 max_depth: usize,
240}
241
242#[allow(clippy::too_many_lines)]
243fn walk_ast(
244 node: Node,
245 content: &[u8],
246 ctx: &mut WalkContext,
247 guard: &mut sqry_core::query::security::RecursionGuard,
248) -> Result<(), String> {
249 guard
250 .enter()
251 .map_err(|e| format!("Recursion limit exceeded: {e}"))?;
252
253 if ctx.scope_stack.len() > ctx.max_depth {
254 guard.exit();
255 return Ok(());
256 }
257
258 match node.kind() {
259 "program" => {
260 let mut active_namespace_parts: Vec<String> = Vec::new();
263
264 let mut cursor = node.walk();
265 for child in node.children(&mut cursor) {
266 if child.kind() == "namespace_definition" {
267 let has_body = child
269 .children(&mut child.walk())
270 .any(|c| matches!(c.kind(), "compound_statement" | "declaration_list"));
271
272 let ns_name = child
273 .child_by_field_name("name")
274 .and_then(|n| n.utf8_text(content).ok())
275 .map(|s| s.trim().to_string())
276 .unwrap_or_default();
277
278 if has_body {
279 for _ in 0..active_namespace_parts.len() {
286 ctx.scope_stack.pop();
287 }
288 active_namespace_parts.clear();
289
290 let ns_parts: Vec<String> = if ns_name.is_empty() {
291 Vec::new()
292 } else {
293 ns_name.split('\\').map(ToString::to_string).collect()
294 };
295
296 for part in &ns_parts {
297 ctx.scope_stack.push(part.clone());
298 }
299
300 for ns_child in child.children(&mut child.walk()) {
302 if matches!(ns_child.kind(), "compound_statement" | "declaration_list")
303 {
304 for body_child in ns_child.children(&mut ns_child.walk()) {
305 walk_ast(body_child, content, ctx, guard)?;
306 }
307 }
308 }
309
310 for _ in 0..ns_parts.len() {
311 ctx.scope_stack.pop();
312 }
313 } else {
314 for _ in 0..active_namespace_parts.len() {
317 ctx.scope_stack.pop();
318 }
319
320 active_namespace_parts = if ns_name.is_empty() {
322 Vec::new()
323 } else {
324 ns_name.split('\\').map(ToString::to_string).collect()
325 };
326
327 for part in &active_namespace_parts {
329 ctx.scope_stack.push(part.clone());
330 }
331 }
332 } else {
333 walk_ast(child, content, ctx, guard)?;
335 }
336 }
337
338 for _ in 0..active_namespace_parts.len() {
340 ctx.scope_stack.pop();
341 }
342
343 guard.exit();
344 return Ok(());
345 }
346 "namespace_definition" => {
347 let namespace_name = node
350 .child_by_field_name("name")
351 .and_then(|n| n.utf8_text(content).ok())
352 .map(|s| s.trim().to_string())
353 .unwrap_or_default();
354
355 let namespace_parts: Vec<String> = if namespace_name.is_empty() {
356 Vec::new()
357 } else {
358 namespace_name
359 .split('\\')
360 .map(ToString::to_string)
361 .collect()
362 };
363
364 let parts_count = namespace_parts.len();
365 for part in &namespace_parts {
366 ctx.scope_stack.push(part.clone());
367 }
368
369 let mut cursor = node.walk();
371 for child in node.children(&mut cursor) {
372 if matches!(child.kind(), "compound_statement" | "declaration_list") {
373 let mut body_cursor = child.walk();
374 for body_child in child.children(&mut body_cursor) {
375 walk_ast(body_child, content, ctx, guard)?;
376 }
377 }
378 }
379
380 for _ in 0..parts_count {
382 ctx.scope_stack.pop();
383 }
384 }
385 "class_declaration" => {
386 let name_node = node
387 .child_by_field_name("name")
388 .ok_or_else(|| "class_declaration missing name".to_string())?;
389 let class_name = name_node
390 .utf8_text(content)
391 .map_err(|_| "failed to read class name".to_string())?;
392
393 let qualified_class = if ctx.scope_stack.is_empty() {
395 class_name.to_string()
396 } else {
397 format!("{}\\{}", ctx.scope_stack.join("\\"), class_name)
398 };
399
400 ctx.class_stack.push(qualified_class.clone());
401 ctx.scope_stack.push(class_name.to_string());
402
403 let _context_idx = ctx.contexts.len();
405 ctx.contexts.push(CallContext {
406 qualified_name: qualified_class.clone(),
407 span: (node.start_byte(), node.end_byte()),
408 kind: ContextKind::Class,
409 class_name: Some(qualified_class),
410 return_type: None, });
412
413 let mut cursor = node.walk();
415 for child in node.children(&mut cursor) {
416 if child.kind() == "declaration_list" {
417 let mut body_cursor = child.walk();
418 for body_child in child.children(&mut body_cursor) {
419 walk_ast(body_child, content, ctx, guard)?;
420 }
421 }
422 }
423
424 ctx.class_stack.pop();
425 ctx.scope_stack.pop();
426 }
427 "function_definition" | "method_declaration" => {
428 let name_node = node
429 .child_by_field_name("name")
430 .ok_or_else(|| format!("{} missing name", node.kind()).to_string())?;
431 let func_name = name_node
432 .utf8_text(content)
433 .map_err(|_| "failed to read function name".to_string())?;
434
435 let is_async = false; let is_static = node
440 .children(&mut node.walk())
441 .any(|child| child.kind() == "static_modifier");
442
443 let visibility = extract_visibility(&node, content);
445
446 let return_type = extract_return_type(&node, content);
448
449 let is_method = !ctx.class_stack.is_empty();
451 let class_name = ctx.class_stack.last().cloned();
452
453 let qualified_func = if is_method {
457 if let Some(ref class) = class_name {
459 format!("{class}::{func_name}")
460 } else {
461 func_name.to_string()
462 }
463 } else {
464 if ctx.scope_stack.is_empty() {
466 func_name.to_string()
467 } else {
468 format!("{}\\{}", ctx.scope_stack.join("\\"), func_name)
469 }
470 };
471
472 let kind = if is_method {
473 ContextKind::Method {
474 is_async,
475 is_static,
476 visibility: visibility.clone(),
477 }
478 } else {
479 ContextKind::Function { is_async }
480 };
481
482 let context_idx = ctx.contexts.len();
483 ctx.contexts.push(CallContext {
484 qualified_name: qualified_func.clone(),
485 span: (node.start_byte(), node.end_byte()),
486 kind,
487 class_name,
488 return_type,
489 });
490
491 if let Some(body) = node.child_by_field_name("body") {
493 associate_descendants(body, context_idx, ctx.node_to_context);
494 }
495
496 ctx.scope_stack.push(func_name.to_string());
497
498 if let Some(body) = node.child_by_field_name("body") {
500 let mut cursor = body.walk();
501 for child in body.children(&mut cursor) {
502 walk_ast(child, content, ctx, guard)?;
503 }
504 }
505
506 ctx.scope_stack.pop();
507 }
508 _ => {
509 let mut cursor = node.walk();
511 for child in node.children(&mut cursor) {
512 walk_ast(child, content, ctx, guard)?;
513 }
514 }
515 }
516
517 guard.exit();
518 Ok(())
519}
520
521fn associate_descendants(
522 node: Node,
523 context_idx: usize,
524 node_to_context: &mut HashMap<usize, usize>,
525) {
526 node_to_context.insert(node.id(), context_idx);
527
528 let mut stack = vec![node];
529 while let Some(current) = stack.pop() {
530 node_to_context.insert(current.id(), context_idx);
531
532 let mut cursor = current.walk();
533 for child in current.children(&mut cursor) {
534 stack.push(child);
535 }
536 }
537}
538
539#[allow(clippy::only_used_in_recursion)]
545fn walk_tree_for_edges(
546 node: Node,
547 content: &[u8],
548 ast_graph: &ASTGraph,
549 helper: &mut GraphBuildHelper,
550 node_map: &mut HashMap<String, NodeId>,
551) -> GraphResult<()> {
552 match node.kind() {
553 "function_call_expression" => {
554 process_function_call(node, content, ast_graph, helper, node_map);
555 }
556 "member_call_expression" | "nullsafe_member_call_expression" => {
557 process_member_call(node, content, ast_graph, helper, node_map);
558 }
559 "scoped_call_expression" => {
560 process_static_call(node, content, ast_graph, helper, node_map);
561 }
562 "namespace_use_declaration" => {
564 process_namespace_use(node, content, helper);
565 }
566 "expression_statement" => {
568 let mut cursor = node.walk();
570 for child in node.children(&mut cursor) {
571 match child.kind() {
572 "require_expression"
573 | "require_once_expression"
574 | "include_expression"
575 | "include_once_expression" => {
576 process_file_include(child, content, helper);
577 }
578 _ => {}
579 }
580 }
581 }
582 _ => {}
583 }
584
585 let mut cursor = node.walk();
587 for child in node.children(&mut cursor) {
588 walk_tree_for_edges(child, content, ast_graph, helper, node_map)?;
589 }
590
591 Ok(())
592}
593
594fn process_function_call(
595 node: Node,
596 content: &[u8],
597 ast_graph: &ASTGraph,
598 helper: &mut GraphBuildHelper,
599 node_map: &mut HashMap<String, NodeId>,
600) {
601 let Some(function_node) = node.child_by_field_name("function") else {
602 return;
603 };
604
605 let Ok(callee_name) = function_node.utf8_text(content) else {
606 return;
607 };
608
609 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
611 return;
612 };
613
614 let source_id = *node_map
616 .entry(call_context.qualified_name.clone())
617 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
618
619 let target_id = *node_map
621 .entry(callee_name.to_string())
622 .or_insert_with(|| helper.add_function(callee_name, None, false, false));
623
624 let argument_count = count_call_arguments(node);
625 let call_span = span_from_node(node);
626 helper.add_call_edge_full_with_span(
627 source_id,
628 target_id,
629 argument_count,
630 false,
631 vec![call_span],
632 );
633}
634
635fn process_member_call(
636 node: Node,
637 content: &[u8],
638 ast_graph: &ASTGraph,
639 helper: &mut GraphBuildHelper,
640 node_map: &mut HashMap<String, NodeId>,
641) {
642 let Some(method_node) = node.child_by_field_name("name") else {
643 return;
644 };
645
646 let Ok(method_name) = method_node.utf8_text(content) else {
647 return;
648 };
649
650 if let Some(object_node) = node.child_by_field_name("object")
652 && is_php_ffi_call(object_node, content)
653 {
654 process_ffi_member_call(node, method_name, ast_graph, helper, node_map);
655 return;
656 }
657
658 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
660 return;
661 };
662
663 let callee_qualified = if let Some(class_name) = &call_context.class_name {
665 format!("{class_name}::{method_name}")
666 } else {
667 method_name.to_string()
668 };
669
670 let source_id = *node_map
672 .entry(call_context.qualified_name.clone())
673 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
674
675 let target_id = *node_map
677 .entry(callee_qualified.clone())
678 .or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
679
680 let argument_count = count_call_arguments(node);
681 let call_span = span_from_node(node);
682 helper.add_call_edge_full_with_span(
683 source_id,
684 target_id,
685 argument_count,
686 false,
687 vec![call_span],
688 );
689}
690
691fn process_static_call(
692 node: Node,
693 content: &[u8],
694 ast_graph: &ASTGraph,
695 helper: &mut GraphBuildHelper,
696 node_map: &mut HashMap<String, NodeId>,
697) {
698 let Some(scope_node) = node.child_by_field_name("scope") else {
699 return;
700 };
701 let Some(name_node) = node.child_by_field_name("name") else {
702 return;
703 };
704
705 let Ok(class_name) = scope_node.utf8_text(content) else {
706 return;
707 };
708 let Ok(method_name) = name_node.utf8_text(content) else {
709 return;
710 };
711
712 if is_ffi_static_call(class_name, method_name) {
714 process_ffi_static_call(node, method_name, ast_graph, helper, node_map, content);
715 return;
716 }
717
718 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
720 return;
721 };
722
723 let callee_qualified = format!("{class_name}::{method_name}");
725
726 let source_id = *node_map
728 .entry(call_context.qualified_name.clone())
729 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
730
731 let target_id = *node_map.entry(callee_qualified.clone()).or_insert_with(|| {
733 helper.add_method(&callee_qualified, None, false, true) });
735
736 let argument_count = count_call_arguments(node);
737 let call_span = span_from_node(node);
738 helper.add_call_edge_full_with_span(
739 source_id,
740 target_id,
741 argument_count,
742 false,
743 vec![call_span],
744 );
745}
746
747fn process_namespace_use(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
760 let file_path = helper.file_path().to_string();
762 let importer_id = helper.add_module(&file_path, None);
763
764 let mut prefix = String::new();
767 let mut cursor = node.walk();
768 for child in node.children(&mut cursor) {
769 if child.kind() == "namespace_name"
770 && let Ok(ns) = child.utf8_text(content)
771 {
772 prefix = ns.trim().to_string();
773 break;
774 }
775 }
776
777 cursor = node.walk();
779 for child in node.children(&mut cursor) {
780 match child.kind() {
781 "namespace_use_clause" => {
782 process_use_clause(child, content, helper, importer_id);
784 }
785 "namespace_use_group" => {
786 process_use_group(child, content, helper, importer_id, &prefix);
789 }
790 _ => {}
791 }
792 }
793}
794
795fn process_use_clause(
807 node: Node,
808 content: &[u8],
809 helper: &mut GraphBuildHelper,
810 import_source_id: NodeId,
811) {
812 process_use_clause_with_prefix(node, content, helper, import_source_id, None);
813}
814
815fn process_use_clause_with_prefix(
817 node: Node,
818 content: &[u8],
819 helper: &mut GraphBuildHelper,
820 import_source_id: NodeId,
821 prefix: Option<&str>,
822) {
823 let mut qualified_name = None;
825 let mut alias = None;
826 let mut found_as = false;
827
828 let mut cursor = node.walk();
829 for child in node.children(&mut cursor) {
830 match child.kind() {
831 "qualified_name" => {
832 if let Ok(name) = child.utf8_text(content) {
834 qualified_name = Some(name.trim().to_string());
835 }
836 }
837 "namespace_name" => {
838 if qualified_name.is_none()
840 && let Ok(name) = child.utf8_text(content)
841 {
842 qualified_name = Some(name.trim().to_string());
843 }
844 }
845 "name" => {
846 if found_as {
848 if let Ok(alias_text) = child.utf8_text(content) {
850 alias = Some(alias_text.trim().to_string());
851 }
852 } else if qualified_name.is_none() {
853 if let Ok(name) = child.utf8_text(content) {
855 qualified_name = Some(name.trim().to_string());
856 }
857 }
858 }
859 "as" => {
860 found_as = true;
862 }
863 _ => {}
864 }
865 }
866
867 if let Some(name) = qualified_name
868 && !name.is_empty()
869 {
870 let full_name = if let Some(pfx) = prefix {
872 format!("{pfx}\\{name}")
873 } else {
874 name
875 };
876
877 let span = span_from_node(node);
879 let import_node_id = helper.add_import(&full_name, Some(span));
880
881 if let Some(alias_str) = alias {
883 helper.add_import_edge_full(import_source_id, import_node_id, Some(&alias_str), false);
884 } else {
885 helper.add_import_edge(import_source_id, import_node_id);
886 }
887 }
888}
889
890fn process_use_group(
909 node: Node,
910 content: &[u8],
911 helper: &mut GraphBuildHelper,
912 import_source_id: NodeId,
913 prefix: &str,
914) {
915 let mut cursor = node.walk();
917 for child in node.children(&mut cursor) {
918 if child.kind() == "namespace_use_clause" {
920 process_use_clause_with_prefix(child, content, helper, import_source_id, Some(prefix));
922 }
923 }
924}
925
926fn process_file_include(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
928 let file_path = helper.file_path().to_string();
930 let import_source_id = helper.add_module(&file_path, None);
931
932 let mut cursor = node.walk();
935 for child in node.children(&mut cursor) {
936 if child.kind() == "string"
937 || child.kind() == "encapsed_string"
938 || child.kind() == "binary_expression"
939 {
940 if let Ok(path_text) = child.utf8_text(content) {
941 let cleaned_path = path_text
943 .trim()
944 .trim_start_matches(['\'', '"'])
945 .trim_end_matches(['\'', '"'])
946 .to_string();
947
948 if !cleaned_path.is_empty() {
949 let span = span_from_node(node);
950 let import_node_id = helper.add_import(&cleaned_path, Some(span));
951 helper.add_import_edge(import_source_id, import_node_id);
952 }
953 }
954 break;
955 }
956 }
957}
958
959fn process_oop_relationships(
965 node: Node,
966 content: &[u8],
967 helper: &mut GraphBuildHelper,
968 node_map: &mut HashMap<String, NodeId>,
969) {
970 let kind = node.kind();
971 if kind == "class_declaration" {
972 process_class_oop(node, content, helper, node_map);
973 } else if kind == "interface_declaration" {
974 process_interface_inheritance(node, content, helper, node_map);
975 }
976
977 let mut cursor = node.walk();
979 for child in node.children(&mut cursor) {
980 process_oop_relationships(child, content, helper, node_map);
981 }
982}
983
984fn process_class_oop(
986 node: Node,
987 content: &[u8],
988 helper: &mut GraphBuildHelper,
989 node_map: &mut HashMap<String, NodeId>,
990) {
991 let Some(name_node) = node.child_by_field_name("name") else {
993 return;
994 };
995 let Ok(class_name) = name_node.utf8_text(content) else {
996 return;
997 };
998 let class_name = class_name.trim();
999
1000 let span = span_from_node(node);
1002 let class_id = *node_map
1003 .entry(class_name.to_string())
1004 .or_insert_with(|| helper.add_class(class_name, Some(span)));
1005
1006 let mut cursor = node.walk();
1008 for child in node.children(&mut cursor) {
1009 match child.kind() {
1010 "base_clause" => {
1011 process_extends_clause(child, content, helper, node_map, class_id);
1013 }
1014 "class_interface_clause" => {
1015 process_implements_clause(child, content, helper, node_map, class_id);
1017 }
1018 "declaration_list" => {
1019 process_class_body_traits(child, content, helper, node_map, class_id);
1021 }
1022 _ => {}
1023 }
1024 }
1025}
1026
1027fn process_extends_clause(
1029 node: Node,
1030 content: &[u8],
1031 helper: &mut GraphBuildHelper,
1032 node_map: &mut HashMap<String, NodeId>,
1033 class_id: NodeId,
1034) {
1035 let mut cursor = node.walk();
1037 for child in node.children(&mut cursor) {
1038 if child.kind() == "name"
1039 || child.kind() == "qualified_name"
1040 || child.kind() == "namespace_name"
1041 {
1042 if let Ok(parent_name) = child.utf8_text(content) {
1043 let parent_name = parent_name.trim();
1044 if !parent_name.is_empty() {
1045 let span = span_from_node(child);
1046 let parent_id = *node_map
1047 .entry(parent_name.to_string())
1048 .or_insert_with(|| helper.add_class(parent_name, Some(span)));
1049
1050 helper.add_inherits_edge(class_id, parent_id);
1051 }
1052 }
1053 break;
1054 }
1055 }
1056}
1057
1058fn process_implements_clause(
1060 node: Node,
1061 content: &[u8],
1062 helper: &mut GraphBuildHelper,
1063 node_map: &mut HashMap<String, NodeId>,
1064 class_id: NodeId,
1065) {
1066 let mut cursor = node.walk();
1068 for child in node.children(&mut cursor) {
1069 if matches!(child.kind(), "name" | "qualified_name" | "namespace_name")
1070 && let Ok(interface_name) = child.utf8_text(content)
1071 {
1072 let interface_name = interface_name.trim();
1073 if !interface_name.is_empty() {
1074 let span = span_from_node(child);
1075 let interface_id = *node_map
1076 .entry(interface_name.to_string())
1077 .or_insert_with(|| helper.add_interface(interface_name, Some(span)));
1078
1079 helper.add_implements_edge(class_id, interface_id);
1080 }
1081 }
1082 }
1083}
1084
1085fn process_class_body_traits(
1087 declaration_list: Node,
1088 content: &[u8],
1089 helper: &mut GraphBuildHelper,
1090 node_map: &mut HashMap<String, NodeId>,
1091 class_id: NodeId,
1092) {
1093 let mut cursor = declaration_list.walk();
1094 for child in declaration_list.children(&mut cursor) {
1095 if child.kind() == "use_declaration" {
1096 process_trait_use(child, content, helper, node_map, class_id);
1098 }
1099 }
1100}
1101
1102fn process_trait_use(
1104 node: Node,
1105 content: &[u8],
1106 helper: &mut GraphBuildHelper,
1107 node_map: &mut HashMap<String, NodeId>,
1108 class_id: NodeId,
1109) {
1110 let mut cursor = node.walk();
1112 for child in node.children(&mut cursor) {
1113 if matches!(child.kind(), "name" | "qualified_name" | "namespace_name")
1114 && let Ok(trait_name) = child.utf8_text(content)
1115 {
1116 let trait_name = trait_name.trim();
1117 if !trait_name.is_empty() {
1118 let span = span_from_node(child);
1119 let trait_id = *node_map.entry(trait_name.to_string()).or_insert_with(|| {
1122 helper.add_node(
1123 trait_name,
1124 Some(span),
1125 sqry_core::graph::unified::node::NodeKind::Trait,
1126 )
1127 });
1128
1129 helper.add_implements_edge(class_id, trait_id);
1132 }
1133 }
1134 }
1135}
1136
1137fn process_interface_inheritance(
1139 node: Node,
1140 content: &[u8],
1141 helper: &mut GraphBuildHelper,
1142 node_map: &mut HashMap<String, NodeId>,
1143) {
1144 let Some(name_node) = node.child_by_field_name("name") else {
1146 return;
1147 };
1148 let Ok(interface_name) = name_node.utf8_text(content) else {
1149 return;
1150 };
1151 let interface_name = interface_name.trim();
1152
1153 let span = span_from_node(node);
1155 let interface_id = *node_map
1156 .entry(interface_name.to_string())
1157 .or_insert_with(|| helper.add_interface(interface_name, Some(span)));
1158
1159 let mut cursor = node.walk();
1161 for child in node.children(&mut cursor) {
1162 if child.kind() == "base_clause" {
1163 let mut base_cursor = child.walk();
1165 for base_child in child.children(&mut base_cursor) {
1166 if matches!(
1167 base_child.kind(),
1168 "name" | "qualified_name" | "namespace_name"
1169 ) && let Ok(parent_name) = base_child.utf8_text(content)
1170 {
1171 let parent_name = parent_name.trim();
1172 if !parent_name.is_empty() {
1173 let span = span_from_node(base_child);
1174 let parent_id = *node_map
1175 .entry(parent_name.to_string())
1176 .or_insert_with(|| helper.add_interface(parent_name, Some(span)));
1177
1178 helper.add_inherits_edge(interface_id, parent_id);
1180 }
1181 }
1182 }
1183 }
1184 }
1185}
1186
1187fn process_exports(
1206 node: Node,
1207 content: &[u8],
1208 helper: &mut GraphBuildHelper,
1209 node_map: &mut HashMap<String, NodeId>,
1210) {
1211 let file_path = helper.file_path().to_string();
1213 let module_id = helper.add_module(&file_path, None);
1214
1215 if node.kind() != "program" {
1217 return;
1218 }
1219
1220 let mut active_namespace = String::new();
1222
1223 let mut cursor = node.walk();
1225 for child in node.children(&mut cursor) {
1226 process_top_level_for_export(
1227 child,
1228 content,
1229 helper,
1230 node_map,
1231 module_id,
1232 &mut active_namespace,
1233 );
1234 }
1235}
1236
1237fn process_top_level_for_export(
1247 node: Node,
1248 content: &[u8],
1249 helper: &mut GraphBuildHelper,
1250 node_map: &mut HashMap<String, NodeId>,
1251 module_id: NodeId,
1252 active_namespace: &mut String,
1253) {
1254 match node.kind() {
1255 "namespace_definition" => {
1256 let ns_name = node
1258 .child_by_field_name("name")
1259 .and_then(|n| n.utf8_text(content).ok())
1260 .map(|s| s.trim().to_string())
1261 .unwrap_or_default();
1262
1263 let has_body = node
1265 .children(&mut node.walk())
1266 .any(|c| matches!(c.kind(), "compound_statement" | "declaration_list"));
1267
1268 if has_body {
1269 active_namespace.clear();
1275
1276 let mut cursor = node.walk();
1278 for child in node.children(&mut cursor) {
1279 if matches!(child.kind(), "compound_statement" | "declaration_list") {
1280 let mut body_cursor = child.walk();
1281 for body_child in child.children(&mut body_cursor) {
1282 export_declaration_if_exportable(
1283 body_child, content, helper, node_map, module_id, &ns_name,
1284 );
1285 }
1286 }
1287 }
1288 } else {
1289 *active_namespace = ns_name;
1292 }
1293 }
1294 "class_declaration"
1296 | "interface_declaration"
1297 | "trait_declaration"
1298 | "enum_declaration"
1299 | "function_definition" => {
1300 export_declaration_if_exportable(
1301 node,
1302 content,
1303 helper,
1304 node_map,
1305 module_id,
1306 active_namespace,
1307 );
1308 }
1309 _ => {
1310 }
1313 }
1314}
1315
1316fn lookup_or_create_node<F>(
1323 node_map: &mut HashMap<String, NodeId>,
1324 qualified_name: &str,
1325 simple_name: &str,
1326 namespace_prefix: &str,
1327 create_fn: F,
1328) -> NodeId
1329where
1330 F: FnOnce() -> NodeId,
1331{
1332 if let Some(&id) = node_map.get(qualified_name) {
1334 return id;
1335 }
1336
1337 if namespace_prefix.is_empty()
1340 && let Some(&id) = node_map.get(simple_name)
1341 {
1342 return id;
1343 }
1344
1345 let id = create_fn();
1347 node_map.insert(qualified_name.to_string(), id);
1348 id
1349}
1350
1351#[allow(clippy::too_many_lines)] fn export_declaration_if_exportable(
1366 node: Node,
1367 content: &[u8],
1368 helper: &mut GraphBuildHelper,
1369 node_map: &mut HashMap<String, NodeId>,
1370 module_id: NodeId,
1371 namespace_prefix: &str,
1372) {
1373 match node.kind() {
1374 "class_declaration" => {
1375 if let Some(name_node) = node.child_by_field_name("name")
1376 && let Ok(class_name) = name_node.utf8_text(content)
1377 {
1378 let simple_name = class_name.trim().to_string();
1379 let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
1380 let span = span_from_node(node);
1381
1382 let class_id = lookup_or_create_node(
1383 node_map,
1384 &qualified_name,
1385 &simple_name,
1386 namespace_prefix,
1387 || helper.add_class(&qualified_name, Some(span)),
1388 );
1389
1390 helper.add_export_edge(module_id, class_id);
1391
1392 export_public_methods_from_class(
1394 node,
1395 content,
1396 helper,
1397 node_map,
1398 module_id,
1399 &qualified_name,
1400 );
1401 }
1402 }
1403 "interface_declaration" => {
1404 if let Some(name_node) = node.child_by_field_name("name")
1405 && let Ok(interface_name) = name_node.utf8_text(content)
1406 {
1407 let simple_name = interface_name.trim().to_string();
1408 let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
1409 let span = span_from_node(node);
1410
1411 let interface_id = lookup_or_create_node(
1412 node_map,
1413 &qualified_name,
1414 &simple_name,
1415 namespace_prefix,
1416 || helper.add_interface(&qualified_name, Some(span)),
1417 );
1418
1419 helper.add_export_edge(module_id, interface_id);
1420 }
1421 }
1422 "trait_declaration" => {
1423 if let Some(name_node) = node.child_by_field_name("name")
1424 && let Ok(trait_name) = name_node.utf8_text(content)
1425 {
1426 let simple_name = trait_name.trim().to_string();
1427 let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
1428 let span = span_from_node(node);
1429
1430 let trait_id = lookup_or_create_node(
1431 node_map,
1432 &qualified_name,
1433 &simple_name,
1434 namespace_prefix,
1435 || {
1436 helper.add_node(
1437 &qualified_name,
1438 Some(span),
1439 sqry_core::graph::unified::node::NodeKind::Trait,
1440 )
1441 },
1442 );
1443
1444 helper.add_export_edge(module_id, trait_id);
1445 }
1446 }
1447 "enum_declaration" => {
1448 if let Some(name_node) = node.child_by_field_name("name")
1450 && let Ok(enum_name) = name_node.utf8_text(content)
1451 {
1452 let simple_name = enum_name.trim().to_string();
1453 let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
1454 let span = span_from_node(node);
1455
1456 let enum_id = lookup_or_create_node(
1457 node_map,
1458 &qualified_name,
1459 &simple_name,
1460 namespace_prefix,
1461 || helper.add_enum(&qualified_name, Some(span)),
1462 );
1463
1464 helper.add_export_edge(module_id, enum_id);
1465 }
1466 }
1467 "function_definition" => {
1468 if let Some(name_node) = node.child_by_field_name("name")
1470 && let Ok(func_name) = name_node.utf8_text(content)
1471 {
1472 let simple_name = func_name.trim().to_string();
1473 let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
1474 let span = span_from_node(node);
1475
1476 let func_id = lookup_or_create_node(
1477 node_map,
1478 &qualified_name,
1479 &simple_name,
1480 namespace_prefix,
1481 || helper.add_function(&qualified_name, Some(span), false, false),
1482 );
1483
1484 helper.add_export_edge(module_id, func_id);
1485 }
1486 }
1487 _ => {
1488 }
1490 }
1491}
1492
1493fn build_qualified_name(namespace_prefix: &str, name: &str) -> String {
1495 if namespace_prefix.is_empty() {
1496 name.to_string()
1497 } else {
1498 format!("{namespace_prefix}\\{name}")
1499 }
1500}
1501
1502fn span_from_node(node: Node<'_>) -> Span {
1504 let start = node.start_position();
1505 let end = node.end_position();
1506 Span::new(
1507 sqry_core::graph::node::Position::new(start.row, start.column),
1508 sqry_core::graph::node::Position::new(end.row, end.column),
1509 )
1510}
1511
1512fn count_call_arguments(call_node: Node<'_>) -> u8 {
1513 let args_node = call_node
1514 .child_by_field_name("arguments")
1515 .or_else(|| call_node.child_by_field_name("argument_list"))
1516 .or_else(|| {
1517 let mut cursor = call_node.walk();
1518 call_node
1519 .children(&mut cursor)
1520 .find(|child| child.kind() == "argument_list")
1521 });
1522
1523 let Some(args_node) = args_node else {
1524 return 255;
1525 };
1526 let count = args_node.named_child_count();
1527 if count <= 254 {
1528 u8::try_from(count).unwrap_or(u8::MAX)
1529 } else {
1530 255
1531 }
1532}
1533
1534fn extract_visibility(node: &Node, content: &[u8]) -> Option<String> {
1539 let mut cursor = node.walk();
1541 for child in node.children(&mut cursor) {
1542 match child.kind() {
1543 "visibility_modifier" => {
1544 if let Ok(vis_text) = child.utf8_text(content) {
1546 return Some(vis_text.trim().to_string());
1547 }
1548 }
1549 "public" | "private" | "protected" => {
1550 if let Ok(vis_text) = child.utf8_text(content) {
1552 return Some(vis_text.trim().to_string());
1553 }
1554 }
1555 _ => {}
1556 }
1557 }
1558
1559 None
1563}
1564
1565fn export_public_methods_from_class(
1571 class_node: Node,
1572 content: &[u8],
1573 helper: &mut GraphBuildHelper,
1574 node_map: &mut HashMap<String, NodeId>,
1575 module_id: NodeId,
1576 class_qualified_name: &str,
1577) {
1578 let mut cursor = class_node.walk();
1580 for child in class_node.children(&mut cursor) {
1581 if child.kind() == "declaration_list" {
1582 let mut body_cursor = child.walk();
1584 for body_child in child.children(&mut body_cursor) {
1585 if body_child.kind() == "method_declaration" {
1586 let visibility = extract_visibility(&body_child, content);
1588
1589 let is_public = visibility.as_deref() == Some("public") || visibility.is_none();
1591
1592 if is_public {
1593 if let Some(name_node) = body_child.child_by_field_name("name")
1595 && let Ok(method_name) = name_node.utf8_text(content)
1596 {
1597 let method_name = method_name.trim();
1598 let qualified_method_name =
1599 format!("{class_qualified_name}::{method_name}");
1600
1601 if let Some(&method_id) = node_map.get(&qualified_method_name) {
1603 helper.add_export_edge(module_id, method_id);
1604 }
1605 }
1606 }
1607 }
1608 }
1609 break;
1610 }
1611 }
1612}
1613
1614fn extract_return_type(node: &Node, content: &[u8]) -> Option<String> {
1633 let mut found_colon = false;
1635 let mut cursor = node.walk();
1636 for child in node.children(&mut cursor) {
1637 if found_colon && child.is_named() {
1638 return extract_type_from_node(&child, content);
1640 }
1641 if child.kind() == ":" {
1642 found_colon = true;
1643 }
1644 }
1645 None
1646}
1647
1648fn extract_type_from_node(type_node: &Node, content: &[u8]) -> Option<String> {
1662 match type_node.kind() {
1663 "primitive_type" => {
1664 type_node
1666 .utf8_text(content)
1667 .ok()
1668 .map(|s| s.trim().to_string())
1669 }
1670 "optional_type" => {
1671 let mut cursor = type_node.walk();
1674 for child in type_node.children(&mut cursor) {
1675 if child.kind() != "?" && child.is_named() {
1676 return extract_type_from_node(&child, content);
1677 }
1678 }
1679 None
1680 }
1681 "union_type" => {
1682 type_node
1685 .named_child(0)
1686 .and_then(|first_type| extract_type_from_node(&first_type, content))
1687 }
1688 "named_type" | "qualified_name" => {
1689 type_node
1691 .utf8_text(content)
1692 .ok()
1693 .map(|s| s.trim().to_string())
1694 }
1695 "intersection_type" => {
1696 type_node
1699 .named_child(0)
1700 .and_then(|first_type| extract_type_from_node(&first_type, content))
1701 }
1702 _ => {
1703 type_node
1708 .utf8_text(content)
1709 .ok()
1710 .map(|s| {
1711 let trimmed = s.trim();
1712 trimmed
1715 .split(&['|', '&'][..])
1716 .next()
1717 .unwrap_or(trimmed)
1718 .trim()
1719 .trim_start_matches('(')
1720 .trim_end_matches(')')
1721 .trim()
1722 .to_string()
1723 })
1724 .filter(|s| !s.is_empty())
1725 }
1726 }
1727}
1728
1729fn process_phpdoc_annotations(
1735 node: Node,
1736 content: &[u8],
1737 helper: &mut GraphBuildHelper,
1738) -> GraphResult<()> {
1739 match node.kind() {
1741 "function_definition" => {
1742 process_function_phpdoc(node, content, helper)?;
1743 }
1744 "method_declaration" => {
1745 process_method_phpdoc(node, content, helper)?;
1746 }
1747 "property_declaration" => {
1748 process_property_phpdoc(node, content, helper)?;
1749 }
1750 "simple_property" => {
1751 process_property_phpdoc(node, content, helper)?;
1753 }
1754 _ => {}
1755 }
1756
1757 let mut cursor = node.walk();
1759 for child in node.children(&mut cursor) {
1760 process_phpdoc_annotations(child, content, helper)?;
1761 }
1762
1763 Ok(())
1764}
1765
1766fn process_function_phpdoc(
1768 func_node: Node,
1769 content: &[u8],
1770 helper: &mut GraphBuildHelper,
1771) -> GraphResult<()> {
1772 let Some(phpdoc_text) = extract_phpdoc_comment(func_node, content) else {
1774 return Ok(());
1775 };
1776
1777 let tags = parse_phpdoc_tags(&phpdoc_text);
1779
1780 let Some(name_node) = func_node.child_by_field_name("name") else {
1782 return Ok(());
1783 };
1784
1785 let function_name = name_node
1786 .utf8_text(content)
1787 .map_err(|_| GraphBuilderError::ParseError {
1788 span: span_from_node(func_node),
1789 reason: "failed to read function name".to_string(),
1790 })?
1791 .trim()
1792 .to_string();
1793
1794 if function_name.is_empty() {
1795 return Ok(());
1796 }
1797
1798 let func_node_id = helper.ensure_function(&function_name, None, false, false);
1800
1801 let _ast_params = extract_ast_parameters(func_node, content);
1803
1804 for (param_idx, param_tag) in tags.params.iter().enumerate() {
1808 let canonical_type = canonical_type_string(¶m_tag.type_str);
1810 let type_node_id = helper.add_type(&canonical_type, None);
1811 helper.add_typeof_edge_with_context(
1812 func_node_id,
1813 type_node_id,
1814 Some(TypeOfContext::Parameter),
1815 param_idx.try_into().ok(), Some(¶m_tag.name),
1817 );
1818
1819 let type_names = extract_type_names(¶m_tag.type_str);
1821 for type_name in type_names {
1822 let ref_type_id = helper.add_type(&type_name, None);
1823 helper.add_reference_edge(func_node_id, ref_type_id);
1824 }
1825 }
1826
1827 if let Some(return_type) = &tags.returns {
1829 let canonical_type = canonical_type_string(return_type);
1830 let type_node_id = helper.add_type(&canonical_type, None);
1831 helper.add_typeof_edge_with_context(
1832 func_node_id,
1833 type_node_id,
1834 Some(TypeOfContext::Return),
1835 Some(0),
1836 None,
1837 );
1838
1839 let type_names = extract_type_names(return_type);
1841 for type_name in type_names {
1842 let ref_type_id = helper.add_type(&type_name, None);
1843 helper.add_reference_edge(func_node_id, ref_type_id);
1844 }
1845 }
1846
1847 Ok(())
1848}
1849
1850fn process_method_phpdoc(
1852 method_node: Node,
1853 content: &[u8],
1854 helper: &mut GraphBuildHelper,
1855) -> GraphResult<()> {
1856 let Some(phpdoc_text) = extract_phpdoc_comment(method_node, content) else {
1858 return Ok(());
1859 };
1860
1861 let tags = parse_phpdoc_tags(&phpdoc_text);
1863
1864 let Some(name_node) = method_node.child_by_field_name("name") else {
1866 return Ok(());
1867 };
1868
1869 let method_name = name_node
1870 .utf8_text(content)
1871 .map_err(|_| GraphBuilderError::ParseError {
1872 span: span_from_node(method_node),
1873 reason: "failed to read method name".to_string(),
1874 })?
1875 .trim()
1876 .to_string();
1877
1878 if method_name.is_empty() {
1879 return Ok(());
1880 }
1881
1882 let class_name = get_enclosing_class_name(method_node, content)?;
1884 let Some(class_name) = class_name else {
1885 return Ok(());
1886 };
1887
1888 let qualified_name = format!("{class_name}.{method_name}");
1890
1891 let method_node_id = helper.ensure_method(&qualified_name, None, false, false);
1894
1895 let _ast_params = extract_ast_parameters(method_node, content);
1897
1898 for (param_idx, param_tag) in tags.params.iter().enumerate() {
1901 let canonical_type = canonical_type_string(¶m_tag.type_str);
1903 let type_node_id = helper.add_type(&canonical_type, None);
1904 helper.add_typeof_edge_with_context(
1905 method_node_id,
1906 type_node_id,
1907 Some(TypeOfContext::Parameter),
1908 param_idx.try_into().ok(),
1909 Some(¶m_tag.name),
1910 );
1911
1912 let type_names = extract_type_names(¶m_tag.type_str);
1914 for type_name in type_names {
1915 let ref_type_id = helper.add_type(&type_name, None);
1916 helper.add_reference_edge(method_node_id, ref_type_id);
1917 }
1918 }
1919
1920 if let Some(return_type) = &tags.returns {
1922 let canonical_type = canonical_type_string(return_type);
1923 let type_node_id = helper.add_type(&canonical_type, None);
1924 helper.add_typeof_edge_with_context(
1925 method_node_id,
1926 type_node_id,
1927 Some(TypeOfContext::Return),
1928 Some(0),
1929 None,
1930 );
1931
1932 let type_names = extract_type_names(return_type);
1934 for type_name in type_names {
1935 let ref_type_id = helper.add_type(&type_name, None);
1936 helper.add_reference_edge(method_node_id, ref_type_id);
1937 }
1938 }
1939
1940 Ok(())
1941}
1942
1943#[allow(clippy::unnecessary_wraps)]
1945fn process_property_phpdoc(
1946 prop_node: Node,
1947 content: &[u8],
1948 helper: &mut GraphBuildHelper,
1949) -> GraphResult<()> {
1950 let Some(phpdoc_text) = extract_phpdoc_comment(prop_node, content) else {
1952 return Ok(());
1953 };
1954
1955 let tags = parse_phpdoc_tags(&phpdoc_text);
1957
1958 let Some(var_type) = &tags.var_type else {
1960 return Ok(());
1961 };
1962
1963 let property_names = extract_property_names(prop_node, content);
1965
1966 if property_names.is_empty() {
1967 let generic_name = format!("property_{:?}", prop_node.id());
1970 let prop_node_id = helper.add_variable(&generic_name, None);
1971
1972 let canonical_type = canonical_type_string(var_type);
1974 let type_node_id = helper.add_type(&canonical_type, None);
1975 helper.add_typeof_edge_with_context(
1976 prop_node_id,
1977 type_node_id,
1978 Some(TypeOfContext::Variable),
1979 None,
1980 Some(&generic_name),
1981 );
1982
1983 let type_names = extract_type_names(var_type);
1985 for type_name in type_names {
1986 let ref_type_id = helper.add_type(&type_name, None);
1987 helper.add_reference_edge(prop_node_id, ref_type_id);
1988 }
1989
1990 return Ok(());
1991 }
1992
1993 for prop_name in property_names {
1995 let prop_node_id = helper.add_variable(&prop_name, None);
1999
2000 let canonical_type = canonical_type_string(var_type);
2002 let type_node_id = helper.add_type(&canonical_type, None);
2003 helper.add_typeof_edge_with_context(
2004 prop_node_id,
2005 type_node_id,
2006 Some(TypeOfContext::Variable),
2007 None,
2008 Some(&prop_name),
2009 );
2010
2011 let type_names = extract_type_names(var_type);
2013 for type_name in type_names {
2014 let ref_type_id = helper.add_type(&type_name, None);
2015 helper.add_reference_edge(prop_node_id, ref_type_id);
2016 }
2017 }
2018
2019 Ok(())
2020}
2021
2022fn extract_ast_parameters(func_node: Node, content: &[u8]) -> Vec<(usize, String)> {
2024 let mut params = Vec::new();
2025
2026 let Some(params_node) = func_node.child_by_field_name("parameters") else {
2028 return params;
2029 };
2030
2031 let mut index = 0;
2032 let mut cursor = params_node.walk();
2033
2034 for child in params_node.children(&mut cursor) {
2035 if !child.is_named() {
2036 continue;
2037 }
2038
2039 match child.kind() {
2040 "simple_parameter" => {
2041 let mut param_cursor = child.walk();
2043 for param_child in child.children(&mut param_cursor) {
2044 if param_child.kind() == "variable_name"
2045 && let Ok(param_text) = param_child.utf8_text(content)
2046 {
2047 params.push((index, param_text.trim().to_string()));
2048 index += 1;
2049 break;
2050 }
2051 }
2052 }
2053 "variadic_parameter" => {
2054 let mut param_cursor = child.walk();
2056 for param_child in child.children(&mut param_cursor) {
2057 if param_child.kind() == "variable_name"
2058 && let Ok(param_text) = param_child.utf8_text(content)
2059 {
2060 params.push((index, param_text.trim().to_string()));
2061 index += 1;
2062 break;
2063 }
2064 }
2065 }
2066 _ => {}
2067 }
2068 }
2069
2070 params
2071}
2072
2073#[allow(clippy::unnecessary_wraps)]
2075fn get_enclosing_class_name(node: Node, content: &[u8]) -> GraphResult<Option<String>> {
2076 let mut current = node;
2077
2078 while let Some(parent) = current.parent() {
2080 if parent.kind() == "class_declaration" {
2081 if let Some(name_node) = parent.child_by_field_name("name")
2083 && let Ok(name_text) = name_node.utf8_text(content)
2084 {
2085 return Ok(Some(name_text.trim().to_string()));
2086 }
2087 return Ok(None);
2088 }
2089 current = parent;
2090 }
2091
2092 Ok(None)
2093}
2094
2095fn extract_property_names(prop_node: Node, content: &[u8]) -> Vec<String> {
2097 let mut names = Vec::new();
2098
2099 match prop_node.kind() {
2100 "property_declaration" => {
2101 let mut cursor = prop_node.walk();
2104 for child in prop_node.children(&mut cursor) {
2105 match child.kind() {
2106 "property_initializer" => {
2107 if let Some(var_node) = child.child_by_field_name("name")
2109 && let Ok(var_text) = var_node.utf8_text(content)
2110 {
2111 names.push(var_text.trim().to_string());
2112 }
2113 }
2114 "simple_property" => {
2115 if let Some(var_node) = child.child_by_field_name("name")
2117 && let Ok(var_text) = var_node.utf8_text(content)
2118 {
2119 names.push(var_text.trim().to_string());
2120 }
2121 }
2122 _ => {}
2123 }
2124 }
2125 }
2126 "simple_property" => {
2127 if let Some(var_node) = prop_node.child_by_field_name("name")
2129 && let Ok(var_text) = var_node.utf8_text(content)
2130 {
2131 names.push(var_text.trim().to_string());
2132 }
2133 }
2134 _ => {}
2135 }
2136
2137 names
2138}
2139
2140fn process_ffi_member_call(
2148 node: Node,
2149 method_name: &str,
2150 ast_graph: &ASTGraph,
2151 helper: &mut GraphBuildHelper,
2152 node_map: &mut HashMap<String, NodeId>,
2153) {
2154 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
2156 return;
2157 };
2158
2159 let source_id = *node_map
2161 .entry(call_context.qualified_name.clone())
2162 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
2163
2164 let ffi_name = format!("native::ffi::{method_name}");
2166 let call_span = span_from_node(node);
2167 let target_id = helper.add_module(&ffi_name, Some(call_span));
2168
2169 helper.add_ffi_edge(source_id, target_id, FfiConvention::C);
2171}
2172
2173fn process_ffi_static_call(
2178 node: Node,
2179 method_name: &str,
2180 ast_graph: &ASTGraph,
2181 helper: &mut GraphBuildHelper,
2182 node_map: &mut HashMap<String, NodeId>,
2183 content: &[u8],
2184) {
2185 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
2187 return;
2188 };
2189
2190 let source_id = *node_map
2192 .entry(call_context.qualified_name.clone())
2193 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
2194
2195 let library_name = extract_php_ffi_library_name(node, content, method_name == "cdef")
2197 .map(|lib| php_ffi_library_simple_name(&lib))
2198 .unwrap_or_else(|| "unknown".to_string());
2199
2200 let ffi_name = format!("native::{library_name}");
2202 let call_span = span_from_node(node);
2203 let target_id = helper.add_module(&ffi_name, Some(call_span));
2204
2205 helper.add_ffi_edge(source_id, target_id, FfiConvention::C);
2207}
2208
2209fn is_php_ffi_call(object_node: Node, content: &[u8]) -> bool {
2222 if object_node.kind() == "scoped_call_expression"
2224 && let Some(scope_node) = object_node.child_by_field_name("scope")
2225 && let Some(name_node) = object_node.child_by_field_name("name")
2226 && let Ok(scope_text) = scope_node.utf8_text(content)
2227 && let Ok(name_text) = name_node.utf8_text(content)
2228 && is_ffi_static_call(scope_text, name_text)
2229 {
2230 return true;
2231 }
2232
2233 if object_node.kind() == "parenthesized_expression"
2235 && let Some(inner) = object_node.named_child(0)
2236 && inner.kind() == "scoped_call_expression"
2237 && let Some(scope_node) = inner.child_by_field_name("scope")
2238 && let Some(name_node) = inner.child_by_field_name("name")
2239 && let Ok(scope_text) = scope_node.utf8_text(content)
2240 && let Ok(name_text) = name_node.utf8_text(content)
2241 && is_ffi_static_call(scope_text, name_text)
2242 {
2243 return true;
2244 }
2245
2246 let Ok(object_text) = object_node.utf8_text(content) else {
2248 return false;
2249 };
2250
2251 let object_text = object_text.trim();
2252
2253 if object_text == "$ffi" || object_text == "$_ffi" {
2255 return true;
2256 }
2257
2258 if object_text.ends_with("->ffi")
2260 || object_text.ends_with("::$ffi")
2261 || object_text.ends_with("->_ffi")
2262 || object_text.ends_with("::$_ffi")
2263 {
2264 return true;
2265 }
2266
2267 false
2268}
2269
2270fn is_ffi_static_call(scope_text: &str, method_text: &str) -> bool {
2274 (scope_text == "FFI" || scope_text == "\\FFI")
2275 && (method_text == "cdef" || method_text == "load")
2276}
2277
2278fn extract_php_ffi_library_name(call_node: Node, content: &[u8], is_cdef: bool) -> Option<String> {
2286 let args = call_node.child_by_field_name("arguments")?;
2287
2288 let mut cursor = args.walk();
2289 let args_vec: Vec<Node> = args
2290 .children(&mut cursor)
2291 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
2292 .collect();
2293
2294 let target_arg_name = if is_cdef { "lib" } else { "filename" };
2297
2298 if let Some(named_arg) = find_named_argument(&args_vec, target_arg_name, content) {
2300 return extract_string_from_argument(named_arg, content);
2301 }
2302
2303 if is_cdef {
2305 args_vec
2307 .get(1)
2308 .and_then(|arg| extract_string_from_argument(*arg, content))
2309 } else {
2310 args_vec
2312 .first()
2313 .and_then(|arg| extract_string_from_argument(*arg, content))
2314 }
2315}
2316
2317fn find_named_argument<'a>(args: &'a [Node], param_name: &str, content: &[u8]) -> Option<Node<'a>> {
2324 for arg in args {
2325 if arg.kind() != "argument" {
2326 continue;
2327 }
2328
2329 if arg.named_child_count() < 2 {
2332 continue;
2333 }
2334
2335 if let Some(name_node) = arg.child_by_field_name("name")
2337 && let Ok(name_text) = name_node.utf8_text(content)
2338 && name_text == param_name
2339 {
2340 return Some(*arg);
2341 } else if let Some(name_node) = arg.named_child(0)
2342 && let Ok(name_text) = name_node.utf8_text(content)
2343 && name_text == param_name
2344 {
2345 return Some(*arg);
2347 }
2348 }
2349
2350 None
2351}
2352
2353fn extract_string_from_argument(arg_node: Node, content: &[u8]) -> Option<String> {
2361 let value_node = unwrap_argument_node(arg_node)?;
2363
2364 if !is_string_literal_node(value_node) {
2366 return None;
2367 }
2368
2369 if is_interpolated_string(value_node) {
2371 return None;
2372 }
2373
2374 extract_php_string_content(value_node, content)
2375}
2376
2377fn unwrap_argument_node(node: Node) -> Option<Node> {
2389 if node.kind() != "argument" {
2390 return Some(node);
2392 }
2393
2394 let name_field_node = node.child_by_field_name("name");
2401 let ref_modifier_field_node = node.child_by_field_name("reference_modifier");
2402
2403 for i in 0..node.named_child_count() {
2405 if let Some(child) = node.named_child(i as u32) {
2406 let is_name_field = name_field_node.is_some_and(|n| n.id() == child.id());
2408 let is_ref_modifier = ref_modifier_field_node.is_some_and(|n| n.id() == child.id());
2409
2410 if !is_name_field && !is_ref_modifier {
2411 return Some(child);
2413 }
2414 }
2415 }
2416
2417 None
2419}
2420
2421fn is_string_literal_node(node: Node) -> bool {
2428 matches!(
2429 node.kind(),
2430 "string" | "encapsed_string" | "heredoc" | "nowdoc"
2431 )
2432}
2433
2434fn is_interpolated_string(node: Node) -> bool {
2447 if !matches!(node.kind(), "encapsed_string" | "heredoc") {
2448 return false;
2449 }
2450
2451 has_variable_node(node)
2453}
2454
2455fn has_variable_node(node: Node) -> bool {
2469 if matches!(
2471 node.kind(),
2472 "variable_name" | "simple_variable" | "variable" | "complex_variable"
2474 | "dynamic_variable_name"
2476 | "subscript_expression" | "member_access_expression" | "member_call_expression"
2478 | "function_call_expression"
2480 | "scoped_call_expression" | "scoped_property_access_expression"
2482 | "class_constant_access_expression"
2484 | "nullsafe_member_access_expression" | "nullsafe_member_call_expression"
2486 ) {
2487 return true;
2488 }
2489
2490 for i in 0..node.child_count() {
2492 if let Some(child) = node.child(i as u32)
2493 && has_variable_node(child)
2494 {
2495 return true;
2496 }
2497 }
2498
2499 false
2500}
2501
2502fn extract_php_string_content(string_node: Node, content: &[u8]) -> Option<String> {
2506 let Ok(text) = string_node.utf8_text(content) else {
2507 return None;
2508 };
2509
2510 let text = text.trim();
2511
2512 if ((text.starts_with('"') && text.ends_with('"'))
2514 || (text.starts_with('\'') && text.ends_with('\'')))
2515 && text.len() >= 2
2516 {
2517 return Some(text[1..text.len() - 1].to_string());
2518 }
2519
2520 Some(text.to_string())
2522}
2523
2524fn php_ffi_library_simple_name(library_path: &str) -> String {
2526 use std::path::Path;
2527
2528 let filename = Path::new(library_path)
2530 .file_name()
2531 .and_then(|f| f.to_str())
2532 .unwrap_or(library_path);
2533
2534 if let Some(so_pos) = filename.find(".so.") {
2536 return filename[..so_pos].to_string();
2537 }
2538
2539 if let Some(dot_pos) = filename.find('.') {
2541 let extension = &filename[dot_pos + 1..];
2542 if extension == "so"
2543 || extension == "dll"
2544 || extension == "dylib"
2545 || extension == "h"
2546 || extension == "hpp"
2547 {
2548 return filename[..dot_pos].to_string();
2549 }
2550 }
2551
2552 filename.to_string()
2553}