1use bonsai_common::{FileId, Span};
3use bonsai_lang_api::{
4 collect_modifier_visibility, collect_param_type_aliases, decl_index_with_handler, extract_imports_via,
5 kit::{
6 call_arg_from_node_with_handler, collect_kinds, first_named_child_of_kind, language_from_pack,
7 named_child_call_args_with_handler, node_text, parse_with, span_of,
8 },
9 AdapterContext, AdapterError, AssignValueKind, AssignmentValueIndex, CallArg, CallKind,
10 CallTargetExtraction, DeclIndex, DeclKind, FieldWrite, FlowEvent, FragmentParseContext, GrammarHandler,
11 ImportIndex, ImportScope, ImportSpec, LanguageAdapter, LanguageCapabilities, LanguageId,
12 ModifierVocabulary, TypeAliasVocabulary, Visibility, EMPTY_HANDLER,
13};
14use std::collections::BTreeSet;
15fn php_call_target<'tree>(node: Node<'tree>, src: &[u8]) -> Option<CallTargetExtraction<'tree>> {
16 let (target, full_text) = match node.kind() {
17 "function_call_expression" => {
18 let target = node.child_by_field_name("function")?;
19 (target, node_text(&target, src).trim().to_string())
20 }
21 "member_call_expression" | "nullsafe_member_call_expression" => {
22 let receiver = node.child_by_field_name("object")?;
23 let target = node.child_by_field_name("name")?;
24 (
25 target,
26 format!(
27 "{}.{}",
28 node_text(&receiver, src).trim(),
29 node_text(&target, src).trim()
30 ),
31 )
32 }
33 "scoped_call_expression" => {
34 let receiver = node.child_by_field_name("scope")?;
35 let target = node.child_by_field_name("name")?;
36 (
37 target,
38 format!(
39 "{}::{}",
40 node_text(&receiver, src).trim(),
41 node_text(&target, src).trim()
42 ),
43 )
44 }
45 "object_creation_expression" => {
46 let target = node.child_by_field_name("type").or_else(|| node.named_child(0))?;
49 (target, node_text(&target, src).trim().to_string())
50 }
51 _ => return None,
52 };
53 (!full_text.is_empty()).then_some(CallTargetExtraction {
54 node: target,
55 full_text,
56 })
57}
58
59const PHP_TYPE_ALIASES: TypeAliasVocabulary = TypeAliasVocabulary {
60 fn_kinds: &["function_definition", "method_declaration"],
61 param_kinds: &["simple_parameter", "property_promotion_parameter"],
62 name_field: "name",
63 type_field: "type",
64};
65
66const PHP_VOCAB: ModifierVocabulary = ModifierVocabulary {
67 decl_kinds: &[
68 "method_declaration",
69 "property_declaration",
70 "class_declaration",
71 "interface_declaration",
72 "trait_declaration",
73 "enum_declaration",
74 ],
75 modifier_container_kinds: &["visibility_modifier", "modifier"],
76 keyword_to_visibility: &[
77 ("private", Visibility::Private),
78 ("protected", Visibility::Protected),
79 ("public", Visibility::Public),
80 ],
81 default_visibility: Visibility::Public,
83};
84use tree_sitter::{Language, Node, Tree};
85
86pub const LANG_ID: LanguageId = LanguageId::new("php");
87const PACK_NAME: &str = "php";
88
89fn extract_php_callable_reference(node: Node<'_>, src: &[u8]) -> Option<String> {
90 if !matches!(
91 node.kind(),
92 "function_call_expression"
93 | "member_call_expression"
94 | "nullsafe_member_call_expression"
95 | "scoped_call_expression"
96 ) {
97 return None;
98 }
99 let callee = node
100 .child_by_field_name("function")
101 .or_else(|| node.child_by_field_name("name"))
102 .or_else(|| node.child_by_field_name("target"))?;
103 let arguments = node
104 .child_by_field_name("arguments")
105 .or_else(|| node.child_by_field_name("argument_list"))?;
106 if arguments.named_child_count() != 1 || arguments.named_child(0)?.kind() != "variadic_placeholder" {
107 return None;
108 }
109 let name = node_text(&callee, src).trim();
110 (!name.is_empty()).then(|| name.to_string())
111}
112
113fn php_subscript_parts(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
114 if node.kind() != "subscript_expression" {
115 return None;
116 }
117 let mut cursor = node.walk();
122 let mut children = node.named_children(&mut cursor);
123 let first = children.next()?;
124 let second = children.next()?;
125 let object = node.child_by_field_name("object").unwrap_or(first);
126 let key = node.child_by_field_name("index").unwrap_or(second);
127 Some((object, key))
128}
129
130fn php_static_subscript_key(node: Node<'_>, src: &[u8]) -> Option<String> {
131 if node.kind() != "string" {
132 return None;
133 }
134 let text = node_text(&node, src).trim();
135 let quote = text.as_bytes().first().copied()?;
136 if !matches!(quote, b'\'' | b'"') || text.as_bytes().last().copied() != Some(quote) {
137 return None;
138 }
139 let value = text.get(1..text.len().checked_sub(1)?)?;
140 (!value.contains('\\')).then(|| value.to_string())
141}
142
143fn php_reference_name(node: Node<'_>, src: &[u8]) -> Option<String> {
144 let raw = node_text(&node, src).trim();
145 (!raw.is_empty()).then(|| raw.to_string())
149}
150
151fn php_binding_name(node: Node<'_>, src: &[u8]) -> Option<String> {
152 let raw = node_text(&node, src).trim();
153 let binding = raw.strip_prefix('$').unwrap_or(raw);
154 (!binding.is_empty()).then(|| binding.to_string())
155}
156
157fn php_assignment_place(node: Node<'_>, src: &[u8]) -> Option<String> {
163 if node.kind() != "subscript_expression" || node.named_child_count() != 1 {
164 return None;
165 }
166 let base = node.named_child(0)?;
167 if base.kind() == "variable_name" {
168 return php_reference_name(base, src);
169 }
170 None
171}
172
173fn php_aggregate_pairs(node: Node<'_>) -> Vec<(Node<'_>, Node<'_>)> {
174 if node.kind() != "list_literal" {
175 return Vec::new();
176 }
177 let mut pairs = Vec::new();
178 let mut pending_key = None;
179 let mut saw_pair_operator = false;
180 let mut cursor = node.walk();
181 if cursor.goto_first_child() {
182 loop {
183 let child = cursor.node();
184 if child.is_named() {
185 if saw_pair_operator {
186 if let Some(key) = pending_key.take() {
187 pairs.push((key, child));
188 }
189 saw_pair_operator = false;
190 } else {
191 pending_key = Some(child);
192 }
193 } else if child.kind() == "=>" {
194 saw_pair_operator = true;
195 }
196 if !cursor.goto_next_sibling() {
197 break;
198 }
199 }
200 }
201 pairs
202}
203
204fn php_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
205 if node.kind() != "foreach_statement" {
206 return None;
207 }
208 let body_id = node.child_by_field_name("body").map(|body| body.id());
209 let mut cursor = node.walk();
210 let mut header = node
211 .named_children(&mut cursor)
212 .filter(|child| Some(child.id()) != body_id);
213 let iterable = header.next()?;
214 let binding = header.next()?;
215 Some((binding, iterable))
216}
217const HANDLER: GrammarHandler = GrammarHandler {
218 expression_value_kind_extractor: None,
219 literal_value_kinds: &["null", "boolean", "integer", "float"],
220 string_literal_kinds: &["string", "encapsed_string", "heredoc", "nowdoc_string"],
221 comment_kinds: &["comment"],
222 doc_comment_prefixes: &["/**"],
223 decorator_kinds: &["attribute"],
224 parameter_container_kinds: &["formal_parameters"],
225 parameter_kinds: &[
226 "simple_parameter",
227 "variadic_parameter",
228 "property_promotion_parameter",
229 ],
230 parameter_modifier_kinds: &["attribute_list"],
231 parameter_annotation_kinds: &["attribute"],
232 variadic_parameter_kinds: &["variadic_parameter"],
233 binding_identifier_kinds: &["variable_name", "name"],
234 non_binding_pattern_field_names: &["type", "key"],
235 binding_name_extractor: Some(php_binding_name),
236 identifier_kinds: &["variable_name", "name"],
237 aggregate_pattern_kinds: &["list_literal", "array_creation_expression"],
238 named_aggregate_kinds: &["array_creation_expression"],
239 positional_aggregate_kinds: &["array", "list", "list_literal", "array_creation_expression"],
240 two_child_aggregate_pair_kinds: &["array_element_initializer"],
241 aggregate_pair_extractor: Some(php_aggregate_pairs),
242 aggregate_value_field_names: &["value"],
243 static_field_name_kinds: &["name"],
244 lambda_value_container_kinds: &["array_creation_expression", "array_element_initializer"],
245 transparent_call_wrapper_kinds: &[
246 "member_access_expression",
247 "scoped_call_expression",
248 "parenthesized_expression",
249 ],
250 assignment_target_wrapper_kinds: &["variable_declaration", "property_element"],
251 assignment_place_extractor: Some(php_assignment_place),
252 binding_declaration_keyword_spellings: &["static"],
253 fn_kinds: &["function_definition", "method_declaration"],
254 call_kinds: &[
255 "function_call_expression",
256 "member_call_expression",
257 "nullsafe_member_call_expression",
258 "scoped_call_expression",
259 "object_creation_expression",
260 ],
261 constructor_call_kinds: &["object_creation_expression"],
262 call_callee_field_names: &["function"],
263 call_receiver_field_names: &["object", "scope"],
264 call_member_field_names: &["name"],
265 constructor_type_field_names: &["type"],
266 call_target_extractor: Some(php_call_target),
267 call_argument_field_names: &["arguments"],
268 call_argument_container_kinds: &["arguments"],
269 argument_wrapper_kinds: &["argument", "named_argument"],
273 argument_name_field_names: &["name"],
274 argument_value_field_names: &["value"],
275 lambda_body_field_names: &["body"],
276 pseudo_call_extractor: Some(extract_php_pseudo_call),
277 syntax_event_extractor: None,
278 argument_passing_mode_extractor: None,
279 call_ref_kinds: &[
280 "function_call_expression",
281 "member_call_expression",
282 "nullsafe_member_call_expression",
283 "scoped_call_expression",
284 "object_creation_expression",
285 ],
286 member_expression_kinds: &[
287 "member_access_expression",
288 "member_expression",
289 "nullsafe_member_access_expression",
290 ],
291 subscript_expression_kinds: &["subscript_expression"],
292 member_base_field_names: &["object"],
293 member_name_field_names: &["name"],
294 subscript_base_field_names: &["object"],
295 subscript_index_field_names: &["index"],
296 static_subscript_key_extractor: Some(php_static_subscript_key),
297 computed_subscript_extractor: Some(php_subscript_parts),
298 sigil_variable_kinds: &["variable_name"],
299 reference_name_extractor: Some(php_reference_name),
300 callable_reference_extractor: Some(extract_php_callable_reference),
301 constructor_names: &["__construct"],
302 runtime_type_guard_operators: &["instanceof"],
303 runtime_type_wrapper_kinds: &["parenthesized_expression"],
304 class_kinds: &[
305 "class_declaration",
306 "interface_declaration",
307 "trait_declaration",
308 "enum_declaration",
309 ],
310 class_decl_kinds: &[
311 ("class_declaration", DeclKind::Class),
312 ("interface_declaration", DeclKind::Interface),
313 ("trait_declaration", DeclKind::Trait),
314 ("enum_declaration", DeclKind::Enum),
315 ],
316 method_kinds: &["method_declaration"],
317 method_context_kinds: &[
318 "class_declaration",
319 "interface_declaration",
320 "trait_declaration",
321 "enum_declaration",
322 ],
323 if_kinds: &[
324 "if_statement",
325 "conditional_expression",
326 "switch_statement",
327 "match_expression",
328 ],
329 branch_then_field_names: &["consequence", "body"],
330 branch_else_field_names: &["alternative"],
331 branch_condition_field_names: &["condition", "value"],
332 loop_body_field_names: &["body"],
333 loop_body_kinds: &["compound_statement", "expression_statement"],
334 branch_arm_kinds: &["compound_statement", "else_clause", "else_if_clause"],
335 additional_alternative_kinds: &["else_clause", "else_if_clause"],
336 for_kinds: &["for_statement"],
337 foreach_kinds: &["foreach_statement"],
338 foreach_binding_extractor: Some(php_foreach_binding),
339 while_kinds: &["while_statement"],
340 do_kinds: &["do_statement"],
341 assignment_kinds: &[
342 "assignment_expression",
343 "augmented_assignment_expression",
344 "reference_assignment_expression",
345 "property_declaration",
346 ],
347 compound_assignment_kinds: &["augmented_assignment_expression"],
348 compound_assignment_operators: &[
349 "+=", "-=", "*=", "/=", "%=", "**=", ".=", "<<=", ">>=", "&=", "^=", "|=", "??=",
350 ],
351 type_only_declaration_kinds: &["property_declaration"],
352 return_kinds: &["return_statement"],
353 throw_kinds: &["throw_expression"],
354 lambda_kinds: &["anonymous_function", "arrow_function"],
355 try_kinds: &["try_statement"],
356 catch_kinds: &["catch_clause"],
357 finally_kinds: &["finally_clause"],
358 break_kinds: &["break_statement"],
359 continue_kinds: &["continue_statement"],
360 control_label_field_names: &[],
361 yield_kinds: &["yield_expression"],
362 yield_value_field_names: &["value"],
363 try_body_field_names: &["body"],
364 implicit_receiver_names: &["$this", "this"],
365 ..EMPTY_HANDLER
366};
367
368fn extract_php_pseudo_call(
369 node: Node<'_>,
370 file: FileId,
371 src: &[u8],
372 handler: &GrammarHandler,
373) -> Option<FlowEvent> {
374 let name = match node.kind() {
375 "echo_statement" => "echo",
376 "unset_statement" => "unset",
377 _ => return None,
378 };
379 Some(FlowEvent::Call {
380 span: span_of(file, &node),
381 receiver: None,
382 receiver_types: Vec::new(),
383 name: name.to_string(),
384 call_kind: CallKind::Function,
385 args: named_child_call_args_with_handler(&node, file, src, handler),
386 })
387}
388
389#[derive(Debug, Default, Copy, Clone)]
391pub struct PhpAdapter;
392
393impl PhpAdapter {
394 #[must_use]
396 pub fn new() -> Self {
397 Self
398 }
399}
400
401impl LanguageAdapter for PhpAdapter {
402 fn language_id(&self) -> LanguageId {
403 LANG_ID
404 }
405 fn display_name(&self) -> &'static str {
406 "PHP"
407 }
408 fn file_extensions(&self) -> &'static [&'static str] {
409 &["php"]
410 }
411 fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
412 language_from_pack(PACK_NAME)
413 }
414 fn fragment_parse_context(&self) -> FragmentParseContext {
415 FragmentParseContext {
418 prefix: "<?php\n",
419 suffix: "",
420 }
421 }
422 fn capabilities(&self) -> LanguageCapabilities {
423 LanguageCapabilities {
424 module_default_export_names: &[],
425 universal_type_names: &["mixed", "object"],
426 receiver_types: bonsai_lang_api::CapabilityLevel::Partial,
427 module_path_syntax: bonsai_lang_api::ModulePathSyntax {
428 rooted_prefixes: &["\\"],
429 repeatable_rooted_prefixes: &[],
430 },
431 constructor_method_names: &["__construct"],
432 super_receiver_tokens: &["parent"],
433 implicit_receiver_tokens: &["$this", "self", "static"],
437 receiver_type_syntax: bonsai_lang_api::ReceiverTypeSyntax {
438 wrapper_calls: &[],
439 class_object_suffixes: &["::class"],
440 },
441 quoted_callable_literals: true,
442 ..LanguageCapabilities::partial_baseline()
443 }
444 }
445 fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
446 let mut idx = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
447 let parsed = parse_with(PACK_NAME, file, ctx);
448 let source = parsed
449 .as_ref()
450 .map(|(snapshot, _)| snapshot.text.to_string())
451 .unwrap_or_default();
452 if let Some((_, tree)) = parsed.as_ref() {
461 let synthesized = synthesize_php_construct_events(tree, source.as_bytes(), file);
462 if !synthesized.is_empty() {
463 attach_synthesized_calls_to_decls(&mut idx, synthesized);
464 }
465 }
466 let namespace_segments = parsed
469 .as_ref()
470 .and_then(|(snapshot, tree)| extract_php_namespace(tree.root_node(), snapshot.text.as_bytes()));
471 if let Some(segments) = namespace_segments {
472 bonsai_lang_api::apply_module_path_semantic_identity(&mut idx, segments);
473 } else {
474 bonsai_lang_api::apply_file_stem_semantic_identity(&mut idx, ctx);
476 }
477 if let Some((snapshot, tree)) = parsed.as_ref() {
478 let src = snapshot.text.as_bytes();
479 let visibility_by_span = collect_modifier_visibility(tree.root_node(), file, src, &PHP_VOCAB);
480 let aliases_by_span = collect_param_type_aliases(tree, file, src, &PHP_TYPE_ALIASES);
481 for decl in &mut idx.defs {
482 if let Some(visibility) = visibility_by_span.get(&decl.span).copied() {
483 decl.visibility = visibility;
484 }
485 if let Some(aliases) = aliases_by_span.get(&decl.span) {
486 decl.type_aliases = aliases.clone();
487 }
488 }
489 let bases_by_span = collect_php_class_bases(tree, file, src);
494 for decl in &mut idx.defs {
495 if !is_class_like(decl.kind) {
496 continue;
497 }
498 if let Some(bases) = bases_by_span.iter().find_map(|(span, name, bases)| {
499 (*span == decl.span || name == &decl.name).then_some(bases)
500 }) {
501 decl.bases = bases.clone();
502 }
503 }
504 let promoted_writes_by_span = collect_php_property_promotion_writes(tree, file, src);
505 for decl in &mut idx.defs {
506 if !matches!(decl.kind, DeclKind::Constructor) {
507 continue;
508 }
509 let Some(promotions) = promoted_writes_by_span
510 .iter()
511 .find_map(|(span, promotions)| (*span == decl.span).then_some(promotions))
512 else {
513 continue;
514 };
515 for promotion in promotions {
516 let Some(param_idx) = decl.params.iter().position(|param| {
517 php_param_matches_promoted_property(
518 param,
519 &promotion.param_name,
520 &promotion.field_name,
521 )
522 }) else {
523 continue;
524 };
525 decl.receiver_field_writes.push(FieldWrite {
526 span: promotion.span,
527 target: format!("this.{}", promotion.field_name),
528 source_param_indices: vec![param_idx],
529 });
530 }
531 decl.receiver_field_writes.sort_by_key(|write| {
532 (
533 write.span.start,
534 write.target.clone(),
535 write.source_param_indices.clone(),
536 )
537 });
538 decl.receiver_field_writes.dedup();
539 }
540 }
541 let assignment_values = AssignmentValueIndex::new(&idx.assignment_values);
542 for decl in &mut idx.defs {
543 let invoked_variables = php_invoked_variables(&decl.flow_events);
544 augment_php_quoted_callable_literals(
545 &mut decl.flow_events,
546 &source,
547 &assignment_values,
548 &invoked_variables,
549 );
550 bonsai_lang_api::normalize_call_result_assignment_sources(&mut decl.flow_events);
551 }
552 bonsai_lang_api::apply_constructor_result_type_aliases(&mut idx);
563 bonsai_lang_api::apply_class_field_type_aliases(&mut idx);
564 let capabilities = self.capabilities();
565 bonsai_lang_api::apply_call_receiver_types_with_language_syntax(
566 &mut idx,
567 capabilities.super_receiver_tokens,
568 capabilities.implicit_receiver_tokens,
569 capabilities.constructor_method_names,
570 capabilities.receiver_type_syntax,
571 );
572 idx
573 }
574 fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
575 extract_imports_via(PACK_NAME, file, ctx, parse_imports)
576 }
577}
578
579fn augment_php_quoted_callable_literals(
585 events: &mut [FlowEvent],
586 source: &str,
587 assignment_values: &AssignmentValueIndex,
588 invoked_variables: &BTreeSet<String>,
589) {
590 for event in events {
591 match event {
592 FlowEvent::Assign {
593 target,
594 span,
595 source_name,
596 value_kind,
597 ..
598 } => {
599 if matches!(value_kind, Some(AssignValueKind::Destructure)) {
600 continue;
601 }
602 if invoked_variables.contains(target) {
603 if let Some(rhs) = assignment_values.rendering(*span, source) {
604 if let Some(callable) = target
605 .trim_start()
606 .starts_with('$')
607 .then(|| php_quoted_bare_callable_literal(rhs))
608 .flatten()
609 {
610 *source_name = Some(callable.to_string());
611 *value_kind = Some(AssignValueKind::CallableReference);
612 }
613 }
614 }
615 }
616 FlowEvent::Branch {
617 then_events,
618 else_events,
619 ..
620 } => {
621 augment_php_quoted_callable_literals(
622 then_events,
623 source,
624 assignment_values,
625 invoked_variables,
626 );
627 augment_php_quoted_callable_literals(
628 else_events,
629 source,
630 assignment_values,
631 invoked_variables,
632 );
633 }
634 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
635 augment_php_quoted_callable_literals(body, source, assignment_values, invoked_variables);
636 }
637 FlowEvent::Try {
638 body,
639 catch_events,
640 finally_events,
641 ..
642 } => {
643 augment_php_quoted_callable_literals(body, source, assignment_values, invoked_variables);
644 augment_php_quoted_callable_literals(
645 catch_events,
646 source,
647 assignment_values,
648 invoked_variables,
649 );
650 augment_php_quoted_callable_literals(
651 finally_events,
652 source,
653 assignment_values,
654 invoked_variables,
655 );
656 }
657 _ => {}
658 }
659 }
660}
661
662fn php_invoked_variables(events: &[FlowEvent]) -> BTreeSet<String> {
666 let mut invoked = BTreeSet::new();
667 bonsai_lang_api::for_each_flow_event(events, &mut |event| {
668 if let FlowEvent::Call {
669 name,
670 call_kind: CallKind::Function | CallKind::Indirect,
671 ..
672 } = event
673 {
674 if name.starts_with('$') {
675 invoked.insert(name.clone());
676 }
677 }
678 });
679 invoked
680}
681
682fn php_quoted_bare_callable_literal(value: &str) -> Option<&str> {
683 let value = value.trim();
684 let quote = value.as_bytes().first().copied()?;
685 if !matches!(quote, b'\'' | b'"') || value.as_bytes().last().copied() != Some(quote) {
686 return None;
687 }
688 let inner = value.get(1..value.len().saturating_sub(1))?.trim();
689 if inner.is_empty()
690 || inner
691 .chars()
692 .any(|ch| !(ch == '_' || ch == '\\' || ch.is_ascii_alphanumeric()))
693 || inner.chars().next().is_some_and(|ch| ch.is_ascii_digit())
694 {
695 return None;
696 }
697 Some(inner)
698}
699
700fn parse_imports(tree: &Tree, src: &[u8], file: FileId) -> Vec<ImportSpec> {
702 let mut imports = Vec::new();
703 for clause in collect_kinds(tree, &["namespace_use_clause"]) {
711 let raw = node_text(&clause, src)
712 .trim_start_matches("use ")
713 .trim_end_matches(';')
714 .trim();
715 if raw.is_empty() {
716 continue;
717 }
718 let (module_text, explicit_alias) = if let Some((module_part, alias_part)) = raw.rsplit_once(" as ") {
720 (
721 module_part.trim().to_string(),
722 Some(alias_part.trim().to_string()),
723 )
724 } else {
725 (raw.to_string(), None)
726 };
727 let qualified_module = match group_namespace_prefix(&clause, src) {
733 Some(prefix) if !module_text.starts_with(&prefix) => format!("{prefix}\\{module_text}"),
734 _ => module_text,
735 };
736 let alias = explicit_alias.or_else(|| canonical_php_base_name(&qualified_module));
741 imports.push(ImportSpec {
742 span: span_of(file, &clause),
743 module: qualified_module,
744 alias,
745 is_wildcard: false,
746 original_name: None,
747 scope: ImportScope::Module,
748 });
749 }
750 for node in collect_kinds(
751 tree,
752 &[
753 "require_expression",
754 "require_once_expression",
755 "include_expression",
756 "include_once_expression",
757 ],
758 ) {
759 let module = first_string_descendant(&node, src);
763 if module.is_empty() {
764 continue;
765 }
766 imports.push(ImportSpec {
767 span: span_of(file, &node),
768 module,
769 alias: None,
770 is_wildcard: true,
771 original_name: None,
772 scope: ImportScope::Module,
773 });
774 }
775 imports
776}
777
778fn group_namespace_prefix(clause: &tree_sitter::Node<'_>, src: &[u8]) -> Option<String> {
783 let mut ancestor = clause.parent();
784 while let Some(parent) = ancestor {
785 if parent.kind() == "namespace_use_group" {
786 let outer = parent.parent()?;
793 let mut outer_cursor = outer.walk();
794 let mut last_prefix: Option<String> = None;
795 for child in outer.named_children(&mut outer_cursor) {
796 if child.id() == parent.id() {
799 break;
800 }
801 if matches!(child.kind(), "namespace_name" | "qualified_name") {
802 let text = node_text(&child, src).trim_end_matches('\\').trim().to_string();
803 if !text.is_empty() {
804 last_prefix = Some(text);
805 }
806 }
807 }
808 return last_prefix;
809 }
810 ancestor = parent.parent();
811 }
812 None
813}
814
815fn first_string_descendant(node: &tree_sitter::Node<'_>, src: &[u8]) -> String {
818 let mut stack = vec![*node];
819 while let Some(current) = stack.pop() {
820 if current.kind() == "string" {
821 if let Some(content) = first_named_child_of_kind(¤t, "string_content") {
824 return node_text(&content, src).to_string();
825 }
826 return node_text(¤t, src)
827 .trim_matches(|ch: char| matches!(ch, '"' | '\''))
828 .to_string();
829 }
830 let mut cursor = current.walk();
831 for child in current.named_children(&mut cursor) {
832 stack.push(child);
833 }
834 }
835 String::new()
836}
837
838fn synthesize_php_construct_events(tree: &Tree, src: &[u8], file: FileId) -> Vec<(Span, FlowEvent)> {
856 const CONSTRUCT_KINDS: &[(&str, &str)] = &[
859 ("include_expression", "include"),
860 ("include_once_expression", "include_once"),
861 ("require_expression", "require"),
862 ("require_once_expression", "require_once"),
863 ("shell_command_expression", "shell_exec"),
864 ];
865 let mut synthesized = Vec::new();
866 for (kind, callee) in CONSTRUCT_KINDS {
867 for node in collect_kinds(tree, &[*kind]) {
868 let span = span_of(file, &node);
869 let mut args: Vec<CallArg> = Vec::new();
870 if *kind == "shell_command_expression" {
874 let mut cursor = node.walk();
875 let mut stack: Vec<tree_sitter::Node<'_>> = Vec::new();
876 for child in node.named_children(&mut cursor) {
877 stack.push(child);
878 }
879 while let Some(current) = stack.pop() {
880 if matches!(
883 current.kind(),
884 "variable_name" | "subscript_expression" | "member_access_expression"
885 ) {
886 if let Some(argument) =
887 call_arg_from_node_with_handler(current, file, src, None, &HANDLER)
888 {
889 args.push(argument);
890 }
891 continue;
892 }
893 let mut child_cursor = current.walk();
894 for child in current.named_children(&mut child_cursor) {
895 stack.push(child);
896 }
897 }
898 } else {
899 let mut cursor = node.walk();
900 for child in node.named_children(&mut cursor) {
901 if let Some(argument) = call_arg_from_node_with_handler(child, file, src, None, &HANDLER)
902 {
903 args.push(argument);
904 break;
906 }
907 }
908 }
909 let event = FlowEvent::Call {
910 span,
911 name: (*callee).to_string(),
912 receiver: None,
913 receiver_types: Vec::new(),
914 call_kind: CallKind::Function,
915 args,
916 };
917 synthesized.push((span, event));
918 }
919 }
920 synthesized
921}
922
923fn attach_synthesized_calls_to_decls(idx: &mut DeclIndex, events: Vec<(Span, FlowEvent)>) {
926 for (event_span, event) in events {
933 let mut best_decl: Option<usize> = None;
934 let mut best_body_len: u64 = u64::MAX;
935 for (decl_idx, decl) in idx.defs.iter().enumerate() {
936 let body = decl.body_span.unwrap_or(decl.span);
937 if event_span.file == body.file && event_span.start >= body.start && event_span.end <= body.end {
938 let body_len = body.end.saturating_sub(body.start);
939 if body_len < best_body_len {
940 best_decl = Some(decl_idx);
941 best_body_len = body_len;
942 }
943 }
944 }
945 if let Some(decl_idx) = best_decl {
946 idx.defs[decl_idx].flow_events.push(event);
947 }
948 }
949}
950
951fn is_class_like(kind: DeclKind) -> bool {
954 matches!(
955 kind,
956 DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct | DeclKind::Enum
957 )
958}
959
960#[derive(Clone, Debug)]
961struct PhpPromotedPropertyWrite {
962 span: Span,
963 param_name: String,
964 field_name: String,
965}
966
967fn collect_php_property_promotion_writes(
968 tree: &Tree,
969 file: FileId,
970 src: &[u8],
971) -> Vec<(Span, Vec<PhpPromotedPropertyWrite>)> {
972 let mut out = Vec::new();
973 for method_node in collect_kinds(tree, &["method_declaration"]) {
974 let Some(name_node) = method_node
975 .child_by_field_name("name")
976 .or_else(|| first_named_child_of_kind(&method_node, "name"))
977 else {
978 continue;
979 };
980 if node_text(&name_node, src).trim() != "__construct" {
981 continue;
982 }
983 let mut promotions = Vec::new();
984 collect_php_property_promotion_writes_inner(method_node, file, src, &mut promotions);
985 if !promotions.is_empty() {
986 out.push((span_of(file, &method_node), promotions));
987 }
988 }
989 out
990}
991
992fn collect_php_property_promotion_writes_inner(
993 node: tree_sitter::Node<'_>,
994 file: FileId,
995 src: &[u8],
996 out: &mut Vec<PhpPromotedPropertyWrite>,
997) {
998 if node.kind() == "property_promotion_parameter" {
999 if let Some(name_node) = node.child_by_field_name("name") {
1000 let param_name = node_text(&name_node, src).trim().to_string();
1001 let field_name = php_promoted_property_field_name(&name_node, src)
1002 .unwrap_or_else(|| param_name.trim_start_matches('$').to_string());
1003 if !param_name.is_empty() && !field_name.is_empty() {
1004 out.push(PhpPromotedPropertyWrite {
1005 span: span_of(file, &node),
1006 param_name,
1007 field_name,
1008 });
1009 }
1010 }
1011 return;
1012 }
1013 let mut cursor = node.walk();
1014 let children: Vec<_> = node.named_children(&mut cursor).collect();
1015 for child in children {
1016 collect_php_property_promotion_writes_inner(child, file, src, out);
1017 }
1018}
1019
1020fn php_promoted_property_field_name(name_node: &tree_sitter::Node<'_>, src: &[u8]) -> Option<String> {
1021 if name_node.kind() == "variable_name" {
1022 let mut cursor = name_node.walk();
1023 for child in name_node.named_children(&mut cursor) {
1024 if child.kind() == "name" {
1025 let name = node_text(&child, src).trim();
1026 if !name.is_empty() {
1027 return Some(name.to_string());
1028 }
1029 }
1030 }
1031 }
1032 let raw = node_text(name_node, src);
1033 let bare = raw.trim().trim_start_matches('$');
1034 (!bare.is_empty()).then(|| bare.to_string())
1035}
1036
1037fn php_param_matches_promoted_property(param: &str, promoted_param: &str, field_name: &str) -> bool {
1038 let param = param.trim();
1039 let promoted_param = promoted_param.trim();
1040 param == promoted_param
1041 || param.trim_start_matches('$') == promoted_param.trim_start_matches('$')
1042 || param.trim_start_matches('$') == field_name
1043}
1044
1045fn collect_php_class_bases(
1057 tree: &Tree,
1058 file: FileId,
1059 src: &[u8],
1060) -> Vec<(bonsai_common::Span, String, Vec<String>)> {
1061 let mut bases_by_class = Vec::new();
1062 let class_kinds = &["class_declaration", "interface_declaration", "enum_declaration"];
1063 for class_node in collect_kinds(tree, class_kinds) {
1064 let Some(name_node) = class_node
1065 .child_by_field_name("name")
1066 .or_else(|| first_named_child_of_kind(&class_node, "name"))
1067 .or_else(|| first_named_child_of_kind(&class_node, "qualified_name"))
1068 else {
1069 continue;
1070 };
1071 let class_name = node_text(&name_node, src).trim();
1072 if class_name.is_empty() {
1073 continue;
1074 }
1075 let mut bases: Vec<String> = Vec::new();
1076 let mut cursor = class_node.walk();
1077 for child in class_node.named_children(&mut cursor) {
1078 match child.kind() {
1079 "base_clause" | "class_interface_clause" | "interface_base_clause" => {
1080 let mut clause_cursor = child.walk();
1081 for entry in child.named_children(&mut clause_cursor) {
1082 if matches!(entry.kind(), "name" | "qualified_name") {
1086 let raw = node_text(&entry, src);
1087 if let Some(name) = canonical_php_base_name(raw) {
1088 if !bases.iter().any(|existing| existing == &name) {
1089 bases.push(name);
1090 }
1091 }
1092 }
1093 }
1094 }
1095 _ => {}
1096 }
1097 }
1098 if !bases.is_empty() {
1099 bases_by_class.push((span_of(file, &class_node), class_name.to_string(), bases));
1100 }
1101 }
1102 bases_by_class
1103}
1104
1105fn canonical_php_base_name(raw: &str) -> Option<String> {
1108 let trimmed = raw.trim().trim_start_matches('\\');
1109 let bare = trimmed.rsplit('\\').next().unwrap_or(trimmed).trim();
1110 if bare.is_empty() {
1111 return None;
1112 }
1113 Some(bare.to_string())
1114}
1115
1116fn extract_php_namespace(root: tree_sitter::Node<'_>, src: &[u8]) -> Option<Vec<String>> {
1119 let mut cursor = root.walk();
1120 for child in root.children(&mut cursor) {
1121 if child.kind() != "namespace_definition" {
1122 continue;
1123 }
1124 if let Some(name_node) = child.child_by_field_name("name") {
1125 let text = node_text(&name_node, src);
1126 let segments: Vec<String> = text
1127 .split('\\')
1128 .map(str::trim)
1129 .filter(|segment| !segment.is_empty())
1130 .map(str::to_string)
1131 .collect();
1132 if !segments.is_empty() {
1133 return Some(segments);
1134 }
1135 }
1136 }
1137 None
1138}
1139
1140#[cfg(test)]
1141mod callable_reference_tests {
1142 use super::*;
1143
1144 #[test]
1145 fn first_class_callable_placeholder_is_adapter_owned() {
1146 let language = language_from_pack(PACK_NAME).expect("php grammar");
1147 let mut parser = tree_sitter::Parser::new();
1148 parser.set_language(&language).expect("set php grammar");
1149 let src = "<?php function f() { $cb = system(...); $value = system($x); }";
1150 let tree = parser.parse(src, None).expect("parse php source");
1151 let refs = collect_kinds(
1152 &tree,
1153 &[
1154 "function_call_expression",
1155 "member_call_expression",
1156 "nullsafe_member_call_expression",
1157 "scoped_call_expression",
1158 ],
1159 )
1160 .into_iter()
1161 .filter_map(|node| extract_php_callable_reference(node, src.as_bytes()))
1162 .collect::<Vec<_>>();
1163 assert_eq!(refs, vec!["system"]);
1164 }
1165}