1use std::collections::HashSet;
4use std::ops::ControlFlow;
5
6use php_ast::{
7 Attribute, CatchClause, ClassMember, ClassMemberKind, EnumMember, EnumMemberKind, Expr,
8 ExprKind, MethodDecl, Name, NamespaceBody, Span, Stmt, StmtKind, TraitUseDecl, TypeHint,
9 TypeHintKind, UnaryPostfixOp, UnaryPrefixOp,
10 visitor::{
11 Visitor, walk_attribute, walk_catch_clause, walk_class_member, walk_enum_member, walk_expr,
12 walk_stmt, walk_trait_use, walk_type_hint,
13 },
14};
15use tower_lsp::lsp_types::DocumentHighlightKind;
16
17use crate::ast::{str_offset, str_offset_in_range};
18use crate::util::fqn_short_name;
19
20pub fn refs_in_stmts(source: &str, stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
23 walk_all_refs(source, stmts, word, false, out);
24}
25
26pub fn refs_in_stmts_with_use(
29 source: &str,
30 stmts: &[Stmt<'_, '_>],
31 word: &str,
32 out: &mut Vec<Span>,
33) {
34 walk_all_refs(source, stmts, word, true, out);
35}
36
37fn walk_all_refs(
38 source: &str,
39 stmts: &[Stmt<'_, '_>],
40 word: &str,
41 include_use: bool,
42 out: &mut Vec<Span>,
43) {
44 let mut v = AllRefsVisitor {
45 source,
46 word,
47 include_use,
48 out: Vec::new(),
49 };
50 for stmt in stmts {
51 let _ = v.visit_stmt(stmt);
52 }
53 out.append(&mut v.out);
54}
55
56struct AllRefsVisitor<'a> {
59 source: &'a str,
60 word: &'a str,
61 include_use: bool,
62 out: Vec<Span>,
63}
64
65impl AllRefsVisitor<'_> {
66 fn push_name_str(&mut self, name: &str, stmt_span: Span) {
67 if name == self.word {
68 let start =
69 str_offset_in_range(self.source, stmt_span, name).unwrap_or(stmt_span.start);
70 self.out.push(Span {
71 start,
72 end: start + name.len() as u32,
73 });
74 }
75 }
76}
77
78impl<'arena, 'src> Visitor<'arena, 'src> for AllRefsVisitor<'_> {
79 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
80 match &stmt.kind {
81 StmtKind::Function(f) => self.push_name_str(f.name.or_error(), stmt.span),
82 StmtKind::Class(c) => {
83 if let Some(name) = c.name {
84 self.push_name_str(name.or_error(), stmt.span);
85 }
86 }
87 StmtKind::Interface(i) => self.push_name_str(i.name.or_error(), stmt.span),
88 StmtKind::Trait(t) => self.push_name_str(t.name.or_error(), stmt.span),
89 StmtKind::Enum(e) => self.push_name_str(e.name.or_error(), stmt.span),
90 StmtKind::Use(u) if self.include_use => {
91 for use_item in u.uses.iter() {
92 let fqn = use_item.name.to_string_repr().into_owned();
93 if let Some(alias) = use_item.alias {
94 if alias == self.word {
96 if let Some(offset) = str_offset(self.source, alias) {
98 self.out.push(Span {
99 start: offset,
100 end: offset + alias.len() as u32,
101 });
102 }
103 }
104 } else {
105 let last_seg = fqn_short_name(&fqn);
107 if last_seg == self.word {
108 let name_span = use_item.name.span();
109 let offset = (fqn.len() - last_seg.len()) as u32;
110 self.out.push(Span {
111 start: name_span.start + offset,
112 end: name_span.start + fqn.len() as u32,
113 });
114 }
115 }
116 }
117 }
118 _ => {}
119 }
120 walk_stmt(self, stmt)
121 }
122
123 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
124 match &member.kind {
125 ClassMemberKind::Method(m) if m.name == self.word => {
126 let name_str = m.name.or_error();
127 let start = str_offset_in_range(self.source, member.span, name_str).unwrap_or(0);
132 self.out.push(Span {
133 start,
134 end: start + name_str.len() as u32,
135 });
136 }
137 ClassMemberKind::ClassConst(cc) if cc.name == self.word => {
138 let name_str = cc.name.or_error();
139 let start = str_offset_in_range(self.source, member.span, name_str)
140 .unwrap_or_else(|| str_offset(self.source, name_str).unwrap_or(0));
141 self.out.push(Span {
142 start,
143 end: start + name_str.len() as u32,
144 });
145 }
146 _ => {}
147 }
148 walk_class_member(self, member)
149 }
150
151 fn visit_enum_member(&mut self, member: &EnumMember<'arena, 'src>) -> ControlFlow<()> {
152 if let EnumMemberKind::Method(m) = &member.kind
153 && m.name == self.word
154 {
155 let name_str = m.name.or_error();
156 let start = str_offset(self.source, name_str).unwrap_or(0);
157 self.out.push(Span {
158 start,
159 end: start + name_str.len() as u32,
160 });
161 }
162 walk_enum_member(self, member)
163 }
164
165 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
166 if let ExprKind::Identifier(name) = &expr.kind
167 && name.as_str() == self.word
168 {
169 self.out.push(expr.span);
170 }
171 walk_expr(self, expr)
172 }
173}
174
175pub fn var_refs_in_stmts(
182 stmts: &[Stmt<'_, '_>],
183 var_name: &str,
184 out: &mut Vec<(Span, DocumentHighlightKind)>,
185) {
186 let mut v = VarRefsVisitor {
187 var_name,
188 out: Vec::new(),
189 };
190 for stmt in stmts {
191 let _ = v.visit_stmt(stmt);
192 }
193 out.append(&mut v.out);
194}
195
196struct VarRefsVisitor<'a> {
197 var_name: &'a str,
198 out: Vec<(Span, DocumentHighlightKind)>,
199}
200
201impl<'arena, 'src> Visitor<'arena, 'src> for VarRefsVisitor<'_> {
202 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
203 match &stmt.kind {
205 StmtKind::Function(_)
206 | StmtKind::Class(_)
207 | StmtKind::Trait(_)
208 | StmtKind::Enum(_)
209 | StmtKind::Interface(_) => ControlFlow::Continue(()),
210 StmtKind::Foreach(f) => {
211 if let Some(key) = &f.key
213 && let ExprKind::Variable(name) = &key.kind
214 && name.as_str() == self.var_name
215 {
216 self.out.push((key.span, DocumentHighlightKind::WRITE));
217 }
218 if let ExprKind::Variable(name) = &f.value.kind
219 && name.as_str() == self.var_name
220 {
221 self.out.push((f.value.span, DocumentHighlightKind::WRITE));
222 }
223 let _ = self.visit_expr(&f.expr);
225 let _ = self.visit_stmt(f.body);
226 ControlFlow::Continue(())
227 }
228 _ => walk_stmt(self, stmt),
229 }
230 }
231
232 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
233 match &expr.kind {
234 ExprKind::Variable(name) => {
236 if name.as_str() == self.var_name {
237 self.out.push((expr.span, DocumentHighlightKind::READ));
238 }
239 ControlFlow::Continue(())
240 }
241 ExprKind::Assign(a) => {
243 if let ExprKind::Variable(name) = &a.target.kind {
245 if name.as_str() == self.var_name {
246 self.out.push((a.target.span, DocumentHighlightKind::WRITE));
247 }
248 } else {
249 let _ = self.visit_expr(a.target);
250 }
251 let _ = self.visit_expr(a.value);
253 ControlFlow::Continue(())
254 }
255 ExprKind::UnaryPrefix(u) => {
257 if matches!(
258 u.op,
259 UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement
260 ) && let ExprKind::Variable(name) = &u.operand.kind
261 && name.as_str() == self.var_name
262 {
263 self.out
264 .push((u.operand.span, DocumentHighlightKind::WRITE));
265 return ControlFlow::Continue(());
266 }
267 walk_expr(self, expr)
268 }
269 ExprKind::UnaryPostfix(u) => {
270 if matches!(
271 u.op,
272 UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement
273 ) && let ExprKind::Variable(name) = &u.operand.kind
274 && name.as_str() == self.var_name
275 {
276 self.out
277 .push((u.operand.span, DocumentHighlightKind::WRITE));
278 return ControlFlow::Continue(());
279 }
280 walk_expr(self, expr)
281 }
282 ExprKind::Closure(c) => {
284 for use_var in c.use_vars.iter() {
286 if use_var.name == self.var_name {
287 self.out.push((use_var.span, DocumentHighlightKind::READ));
288 }
289 }
290 ControlFlow::Continue(())
291 }
292 ExprKind::ArrowFunction(_) => walk_expr(self, expr),
294 _ => walk_expr(self, expr),
295 }
296 }
297}
298
299pub fn collect_var_refs_in_scope(
304 stmts: &[Stmt<'_, '_>],
305 var_name: &str,
306 byte_off: usize,
307 out: &mut Vec<(Span, DocumentHighlightKind)>,
308) {
309 for stmt in stmts {
310 if collect_in_fn_at(stmt, var_name, byte_off, out) {
311 return;
312 }
313 }
314 var_refs_in_stmts(stmts, var_name, out);
316}
317
318fn collect_method_scope(
321 m: &MethodDecl<'_, '_>,
322 member_span: Span,
323 var_name: &str,
324 byte_off: usize,
325 out: &mut Vec<(Span, DocumentHighlightKind)>,
326) -> bool {
327 if byte_off < member_span.start as usize || byte_off >= member_span.end as usize {
328 return false;
329 }
330 if let Some(body) = &m.body {
331 for inner in body.stmts.iter() {
332 if collect_in_fn_at(inner, var_name, byte_off, out) {
333 return true;
334 }
335 }
336 var_refs_in_stmts(&body.stmts, var_name, out);
337 }
338 for p in m.params.iter() {
339 if p.name == var_name {
340 out.push((p.span, DocumentHighlightKind::WRITE));
341 }
342 }
343 true
344}
345
346fn collect_in_class_members(
349 members: &[ClassMember<'_, '_>],
350 var_name: &str,
351 byte_off: usize,
352 out: &mut Vec<(Span, DocumentHighlightKind)>,
353) -> bool {
354 for member in members {
355 if let ClassMemberKind::Method(m) = &member.kind
356 && collect_method_scope(m, member.span, var_name, byte_off, out)
357 {
358 return true;
359 }
360 }
361 false
362}
363
364fn collect_in_fn_at(
367 stmt: &Stmt<'_, '_>,
368 var_name: &str,
369 byte_off: usize,
370 out: &mut Vec<(Span, DocumentHighlightKind)>,
371) -> bool {
372 match &stmt.kind {
373 StmtKind::Function(f) => {
374 if byte_off < stmt.span.start as usize || byte_off >= stmt.span.end as usize {
375 return false;
376 }
377 for inner in f.body.stmts.iter() {
378 if collect_in_fn_at(inner, var_name, byte_off, out) {
379 return true;
380 }
381 }
382 for p in f.params.iter() {
383 if p.name == var_name {
384 out.push((p.span, DocumentHighlightKind::WRITE));
385 }
386 }
387 var_refs_in_stmts(&f.body.stmts, var_name, out);
388 true
389 }
390 StmtKind::Class(c) => collect_in_class_members(&c.body.members, var_name, byte_off, out),
391 StmtKind::Trait(t) => collect_in_class_members(&t.body.members, var_name, byte_off, out),
392 StmtKind::Interface(i) => {
393 collect_in_class_members(&i.body.members, var_name, byte_off, out)
394 }
395 StmtKind::Enum(e) => {
396 for member in e.body.members.iter() {
397 if let EnumMemberKind::Method(m) = &member.kind
398 && collect_method_scope(m, member.span, var_name, byte_off, out)
399 {
400 return true;
401 }
402 }
403 false
404 }
405 StmtKind::Namespace(ns) => {
406 if let NamespaceBody::Braced(inner) = &ns.body {
407 for s in inner.stmts.iter() {
408 if collect_in_fn_at(s, var_name, byte_off, out) {
409 return true;
410 }
411 }
412 }
413 false
414 }
415 _ => false,
416 }
417}
418
419pub fn property_refs_in_stmts(
424 source: &str,
425 stmts: &[Stmt<'_, '_>],
426 prop_name: &str,
427 out: &mut Vec<Span>,
428) {
429 let mut v = PropertyRefsVisitor {
430 source,
431 prop_name,
432 out: Vec::new(),
433 };
434 for stmt in stmts {
435 let _ = v.visit_stmt(stmt);
436 }
437 out.append(&mut v.out);
438}
439
440struct PropertyRefsVisitor<'a> {
441 source: &'a str,
442 prop_name: &'a str,
443 out: Vec<Span>,
444}
445
446impl<'arena, 'src> Visitor<'arena, 'src> for PropertyRefsVisitor<'_> {
447 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
448 match &expr.kind {
449 ExprKind::PropertyAccess(p) | ExprKind::NullsafePropertyAccess(p) => {
450 let span = p.property.span;
451 let name_in_src = self
452 .source
453 .get(span.start as usize..span.end as usize)
454 .unwrap_or("");
455 if name_in_src == self.prop_name {
456 self.out.push(span);
457 }
458 }
459 ExprKind::StaticPropertyAccess(s) => {
464 if let ExprKind::Identifier(name) = &s.member.kind
465 && name.as_str() == self.prop_name
466 && s.member.span.start + 1 < s.member.span.end
467 {
468 self.out.push(Span {
469 start: s.member.span.start + 1,
470 end: s.member.span.end,
471 });
472 }
473 }
474 _ => {}
475 }
476 walk_expr(self, expr)
477 }
478
479 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
480 match &member.kind {
481 ClassMemberKind::Property(p) if p.name == self.prop_name => {
482 let name_str = p.name.or_error();
483 let offset = str_offset(self.source, name_str).unwrap_or(0);
484 self.out.push(Span {
485 start: offset,
486 end: offset + name_str.len() as u32,
487 });
488 }
489 ClassMemberKind::Method(m) if m.name == "__construct" => {
491 for p in m.params.iter() {
492 if p.visibility.is_some() && p.name == self.prop_name {
493 let name_str = p.name.or_error();
494 let offset = str_offset(self.source, name_str).unwrap_or(0);
495 self.out.push(Span {
496 start: offset,
497 end: offset + name_str.len() as u32,
498 });
499 }
500 }
501 }
502 _ => {}
503 }
504 walk_class_member(self, member)
505 }
506}
507
508pub fn function_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
514 let mut v = FunctionRefsVisitor {
515 name,
516 out: Vec::new(),
517 };
518 for stmt in stmts {
519 let _ = v.visit_stmt(stmt);
520 }
521 out.append(&mut v.out);
522}
523
524struct FunctionRefsVisitor<'a> {
525 name: &'a str,
526 out: Vec<Span>,
527}
528
529impl<'arena, 'src> Visitor<'arena, 'src> for FunctionRefsVisitor<'_> {
530 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
531 if let ExprKind::FunctionCall(f) = &expr.kind
532 && let ExprKind::Identifier(id) = &f.name.kind
533 && id.as_str() == self.name
534 {
535 self.out.push(f.name.span);
536 }
537 walk_expr(self, expr)
538 }
539}
540
541pub fn method_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
546 let mut v = MethodRefsVisitor {
547 name,
548 out: Vec::new(),
549 };
550 for stmt in stmts {
551 let _ = v.visit_stmt(stmt);
552 }
553 out.append(&mut v.out);
554}
555
556struct MethodRefsVisitor<'a> {
557 name: &'a str,
558 out: Vec<Span>,
559}
560
561impl<'arena, 'src> Visitor<'arena, 'src> for MethodRefsVisitor<'_> {
562 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
563 match &expr.kind {
564 ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
565 if let ExprKind::Identifier(id) = &m.method.kind
566 && id.as_str() == self.name
567 {
568 self.out.push(m.method.span);
569 }
570 }
571 ExprKind::StaticMethodCall(s) if s.method.name_str() == Some(self.name) => {
572 self.out.push(s.method.span);
573 }
574 _ => {}
575 }
576 walk_expr(self, expr)
577 }
578}
579
580pub fn constant_refs_in_stmts(
590 source: &str,
591 stmts: &[Stmt<'_, '_>],
592 const_name: &str,
593 class_filter: Option<&str>,
594 out: &mut Vec<Span>,
595) {
596 let allowed: Option<HashSet<String>> = class_filter.map(|owner| {
599 let mut set = HashSet::new();
600 set.insert(owner.to_string());
601 for stmt in stmts {
602 if let StmtKind::Class(c) = &stmt.kind
603 && let Some(extends) = &c.extends
604 && extends.to_string_repr() == owner
605 && let Some(name) = c.name
606 {
607 set.insert(name.to_string());
608 }
609 }
610 set
611 });
612 let mut v = ConstantRefsVisitor {
613 source,
614 const_name,
615 allowed: allowed.as_ref(),
616 current_class: None,
617 out: Vec::new(),
618 };
619 for stmt in stmts {
620 let _ = v.visit_stmt(stmt);
621 }
622 out.append(&mut v.out);
623}
624
625struct ConstantRefsVisitor<'a> {
626 source: &'a str,
627 const_name: &'a str,
628 allowed: Option<&'a HashSet<String>>,
631 current_class: Option<String>,
634 out: Vec<Span>,
635}
636
637impl<'arena, 'src> Visitor<'arena, 'src> for ConstantRefsVisitor<'_> {
638 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
639 let class_name: Option<String> = match &stmt.kind {
640 StmtKind::Class(c) => c.name.map(|n| n.to_string()),
641 StmtKind::Interface(i) => Some(i.name.to_string()),
642 StmtKind::Trait(t) => Some(t.name.to_string()),
643 StmtKind::Enum(e) => Some(e.name.to_string()),
644 _ => {
645 return walk_stmt(self, stmt);
646 }
647 };
648 let prev = self.current_class.take();
649 self.current_class = class_name;
650 let r = walk_stmt(self, stmt);
651 self.current_class = prev;
652 r
653 }
654
655 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
656 if let ExprKind::ClassConstAccess(s) = &expr.kind
657 && let ExprKind::Identifier(name) = &s.member.kind
658 && name.as_str() == self.const_name
659 {
660 let include = self.allowed.is_none_or(|allowed| {
661 if let ExprKind::Identifier(class_id) = &s.class.kind {
662 let cn = class_id.as_str();
663 matches!(cn, "self" | "parent" | "static") || allowed.contains(cn)
664 } else {
665 true
666 }
667 });
668 if include {
669 self.out.push(s.member.span);
670 }
671 }
672 walk_expr(self, expr)
673 }
674
675 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
676 if let ClassMemberKind::ClassConst(c) = &member.kind
677 && c.name == self.const_name
678 {
679 let class_ok = self.allowed.is_none_or(|allowed| {
680 self.current_class
681 .as_deref()
682 .is_none_or(|cls| allowed.contains(cls))
683 });
684 if class_ok {
685 let name = c.name.to_string();
686 let start = str_offset_in_range(self.source, member.span, &name)
687 .unwrap_or_else(|| str_offset(self.source, &name).unwrap_or(0));
688 self.out.push(Span {
689 start,
690 end: start + name.len() as u32,
691 });
692 }
693 }
694 walk_class_member(self, member)
695 }
696
697 fn visit_enum_member(&mut self, member: &EnumMember<'arena, 'src>) -> ControlFlow<()> {
698 if let EnumMemberKind::ClassConst(c) = &member.kind
699 && c.name == self.const_name
700 {
701 let class_ok = self.allowed.is_none_or(|allowed| {
702 self.current_class
703 .as_deref()
704 .is_none_or(|cls| allowed.contains(cls))
705 });
706 if class_ok {
707 let name = c.name.to_string();
708 let start = str_offset_in_range(self.source, member.span, &name)
709 .unwrap_or_else(|| str_offset(self.source, &name).unwrap_or(0));
710 self.out.push(Span {
711 start,
712 end: start + name.len() as u32,
713 });
714 }
715 }
716 walk_enum_member(self, member)
717 }
718}
719
720pub fn global_constant_refs_in_stmts(
734 source: &str,
735 stmts: &[Stmt<'_, '_>],
736 const_name: &str,
737 const_fqn: Option<&str>,
738 out: &mut Vec<Span>,
739) {
740 let mut v = GlobalConstRefsVisitor {
741 source,
742 const_name,
743 const_fqn,
744 out: Vec::new(),
745 };
746 for stmt in stmts {
747 let _ = v.visit_stmt(stmt);
748 }
749 out.append(&mut v.out);
750}
751
752struct GlobalConstRefsVisitor<'a> {
753 source: &'a str,
754 const_name: &'a str,
755 const_fqn: Option<&'a str>,
759 out: Vec<Span>,
760}
761
762impl<'arena, 'src> Visitor<'arena, 'src> for GlobalConstRefsVisitor<'_> {
763 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
764 if let StmtKind::Const(items) = &stmt.kind {
765 for item in items.iter() {
766 if item.name == self.const_name {
767 let name = item.name.to_string();
768 if let Some(start) = str_offset_in_range(self.source, item.span, &name) {
769 self.out.push(Span {
770 start,
771 end: start + name.len() as u32,
772 });
773 }
774 }
775 let _ = self.visit_expr(&item.value);
777 }
778 return ControlFlow::Continue(());
779 }
780 if let StmtKind::Expression(expr) = &stmt.kind
782 && let ExprKind::FunctionCall(f) = &expr.kind
783 && let ExprKind::Identifier(id) = &f.name.kind
784 && id.as_str() == "define"
785 && let Some(first_arg) = f.args.first()
786 && let ExprKind::String(s) = &first_arg.value.kind
787 && *s == self.const_name
788 {
789 let start = first_arg.value.span.start + 1;
791 self.out.push(Span {
792 start,
793 end: start + s.len() as u32,
794 });
795 return ControlFlow::Continue(());
797 }
798 walk_stmt(self, stmt)
799 }
800
801 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
802 match &expr.kind {
803 ExprKind::Identifier(name) => {
804 let s = name.as_str();
805 let name_offset = if s == self.const_name {
807 Some(0usize)
808 } else if let Some(fqn) = self.const_fqn {
809 let bare_fqn = s.trim_start_matches('\\');
811 if bare_fqn == fqn {
812 Some(s.len() - self.const_name.len())
814 } else {
815 None
816 }
817 } else {
818 None
819 };
820 if let Some(off) = name_offset {
821 let start = expr.span.start + off as u32;
822 self.out.push(Span {
823 start,
824 end: start + self.const_name.len() as u32,
825 });
826 }
827 ControlFlow::Continue(())
828 }
829
830 ExprKind::FunctionCall(f) => {
832 for arg in f.args.iter() {
833 let _ = self.visit_arg(arg);
834 }
835 ControlFlow::Continue(())
836 }
837
838 ExprKind::StaticMethodCall(call) => {
840 for arg in call.args.iter() {
841 let _ = self.visit_arg(arg);
842 }
843 ControlFlow::Continue(())
844 }
845 ExprKind::StaticDynMethodCall(call) => {
846 for arg in call.args.iter() {
847 let _ = self.visit_arg(arg);
848 }
849 ControlFlow::Continue(())
850 }
851
852 ExprKind::New(new_expr) => {
854 for arg in new_expr.args.iter() {
855 let _ = self.visit_arg(arg);
856 }
857 ControlFlow::Continue(())
858 }
859
860 ExprKind::StaticPropertyAccess(_)
863 | ExprKind::ClassConstAccess(_)
864 | ExprKind::ClassConstAccessDynamic { .. }
865 | ExprKind::StaticPropertyAccessDynamic { .. } => ControlFlow::Continue(()),
866
867 ExprKind::Binary(b) if b.op == php_ast::BinaryOp::Instanceof => {
869 let _ = self.visit_expr(b.left);
870 ControlFlow::Continue(())
872 }
873
874 _ => walk_expr(self, expr),
875 }
876 }
877}
878
879pub fn new_refs_in_stmts(
888 stmts: &[Stmt<'_, '_>],
889 class_name: &str,
890 class_fqn: Option<&str>,
891 out: &mut Vec<Span>,
892) {
893 let mut v = NewRefsVisitor {
894 class_name,
895 class_fqn,
896 out: Vec::new(),
897 };
898 for stmt in stmts {
899 let _ = v.visit_stmt(stmt);
900 }
901 out.append(&mut v.out);
902}
903
904struct NewRefsVisitor<'a> {
905 class_name: &'a str,
906 class_fqn: Option<&'a str>,
907 out: Vec<Span>,
908}
909
910impl<'arena, 'src> Visitor<'arena, 'src> for NewRefsVisitor<'_> {
911 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
912 if let ExprKind::New(n) = &expr.kind
913 && let ExprKind::Identifier(id) = &n.class.kind
914 {
915 let matches = if id.contains('\\')
916 && let Some(fqn) = self.class_fqn
917 {
918 id.trim_start_matches('\\') == fqn.trim_start_matches('\\')
920 } else {
921 fqn_short_name(id) == self.class_name
922 };
923 if matches {
924 self.out.push(n.class.span);
925 }
926 }
927 walk_expr(self, expr)
928 }
929}
930
931pub fn fqn_new_class_refs_in_stmts(stmts: &[Stmt<'_, '_>]) -> Vec<String> {
936 let mut v = FqnNewRefsVisitor { out: Vec::new() };
937 for stmt in stmts {
938 let _ = v.visit_stmt(stmt);
939 }
940 v.out.sort_unstable();
941 v.out.dedup();
942 v.out
943}
944
945struct FqnNewRefsVisitor {
946 out: Vec<String>,
947}
948
949impl<'arena, 'src> Visitor<'arena, 'src> for FqnNewRefsVisitor {
950 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
951 if let ExprKind::New(n) = &expr.kind
952 && let ExprKind::Identifier(id) = &n.class.kind
953 && id.starts_with('\\')
954 {
955 self.out.push(id.trim_start_matches('\\').to_string());
956 }
957 walk_expr(self, expr)
958 }
959}
960
961pub fn all_class_ref_names_in_stmts(stmts: &[Stmt<'_, '_>]) -> Vec<String> {
968 let mut v = AllClassRefsVisitor { out: Vec::new() };
969 for stmt in stmts {
970 let _ = v.visit_stmt(stmt);
971 }
972 v.out.sort_unstable();
973 v.out.dedup();
974 v.out
975}
976
977struct AllClassRefsVisitor {
978 out: Vec<String>,
979}
980
981impl AllClassRefsVisitor {
982 fn push_name(&mut self, name: &Name<'_, '_>) {
983 self.out.push(name.to_string_repr().into_owned());
984 }
985
986 fn push_id(&mut self, id: &str) {
987 self.out.push(id.to_string());
988 }
989}
990
991impl<'arena, 'src> Visitor<'arena, 'src> for AllClassRefsVisitor {
992 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
993 match &stmt.kind {
994 StmtKind::Class(c) => {
995 if let Some(ext) = &c.extends {
996 self.push_name(ext);
997 }
998 for iface in c.implements.iter() {
999 self.push_name(iface);
1000 }
1001 }
1002 StmtKind::Interface(i) => {
1003 for parent in i.extends.iter() {
1004 self.push_name(parent);
1005 }
1006 }
1007 _ => {}
1008 }
1009 walk_stmt(self, stmt)
1010 }
1011
1012 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
1013 match &expr.kind {
1014 ExprKind::New(n) => {
1015 if let ExprKind::Identifier(id) = &n.class.kind {
1016 self.push_id(id);
1017 }
1018 }
1019 ExprKind::AnonymousClass(c) => {
1020 if let Some(ext) = &c.extends {
1021 self.push_name(ext);
1022 }
1023 for iface in c.implements.iter() {
1024 self.push_name(iface);
1025 }
1026 }
1027 ExprKind::Binary(b) => {
1028 if let ExprKind::Identifier(id) = &b.right.kind {
1031 self.push_id(id);
1032 }
1033 }
1034 ExprKind::StaticMethodCall(s) => {
1035 if let ExprKind::Identifier(id) = &s.class.kind {
1036 self.push_id(id);
1037 }
1038 }
1039 ExprKind::StaticPropertyAccess(s) => {
1040 if let ExprKind::Identifier(id) = &s.class.kind {
1041 self.push_id(id);
1042 }
1043 }
1044 ExprKind::ClassConstAccess(c) => {
1045 if let ExprKind::Identifier(id) = &c.class.kind {
1046 self.push_id(id);
1047 }
1048 }
1049 _ => {}
1050 }
1051 walk_expr(self, expr)
1052 }
1053
1054 fn visit_attribute(&mut self, attribute: &Attribute<'arena, 'src>) -> ControlFlow<()> {
1055 self.push_name(&attribute.name);
1056 walk_attribute(self, attribute)
1057 }
1058
1059 fn visit_type_hint(&mut self, type_hint: &TypeHint<'arena, 'src>) -> ControlFlow<()> {
1060 match &type_hint.kind {
1061 TypeHintKind::Named(name) => {
1062 self.push_name(name);
1063 walk_type_hint(self, type_hint)
1064 }
1065 TypeHintKind::Nullable(_) => walk_type_hint(self, type_hint),
1066 TypeHintKind::Union(types) => {
1067 for inner in types.iter() {
1068 let _ = self.visit_type_hint(inner);
1069 }
1070 ControlFlow::Continue(())
1071 }
1072 TypeHintKind::Intersection(types) => {
1073 for inner in types.iter() {
1074 let _ = self.visit_type_hint(inner);
1075 }
1076 ControlFlow::Continue(())
1077 }
1078 TypeHintKind::Keyword(_, _) => ControlFlow::Continue(()),
1079 }
1080 }
1081
1082 fn visit_catch_clause(&mut self, catch: &CatchClause<'arena, 'src>) -> ControlFlow<()> {
1083 for ty in catch.types.iter() {
1084 self.push_name(ty);
1085 }
1086 walk_catch_clause(self, catch)
1087 }
1088
1089 fn visit_trait_use(&mut self, trait_use: &TraitUseDecl<'arena, 'src>) -> ControlFlow<()> {
1090 for name in trait_use.traits.iter() {
1091 self.push_name(name);
1092 }
1093 walk_trait_use(self, trait_use)
1094 }
1095}
1096
1097pub fn class_refs_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str, out: &mut Vec<Span>) {
1101 let mut v = ClassRefsVisitor {
1102 class_name,
1103 out: Vec::new(),
1104 };
1105 for stmt in stmts {
1106 let _ = v.visit_stmt(stmt);
1107 }
1108 out.append(&mut v.out);
1109}
1110
1111struct ClassRefsVisitor<'a> {
1112 class_name: &'a str,
1113 out: Vec<Span>,
1114}
1115
1116impl ClassRefsVisitor<'_> {
1117 fn collect_name<'a, 'b>(&mut self, name: &Name<'a, 'b>) {
1119 let repr = name.to_string_repr();
1120 let last = fqn_short_name(&repr);
1121 if last == self.class_name {
1122 let span = name.span();
1123 let offset = (repr.len() - last.len()) as u32;
1124 self.out.push(Span {
1125 start: span.start + offset,
1126 end: span.end,
1127 });
1128 }
1129 }
1130}
1131
1132impl<'arena, 'src> Visitor<'arena, 'src> for ClassRefsVisitor<'_> {
1133 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
1134 match &stmt.kind {
1135 StmtKind::Class(c) => {
1136 if let Some(ext) = &c.extends {
1137 self.collect_name(ext);
1138 }
1139 for iface in c.implements.iter() {
1140 self.collect_name(iface);
1141 }
1142 }
1143 StmtKind::Interface(i) => {
1144 for parent in i.extends.iter() {
1145 self.collect_name(parent);
1146 }
1147 }
1148 _ => {}
1149 }
1150 walk_stmt(self, stmt)
1151 }
1152
1153 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
1154 match &expr.kind {
1155 ExprKind::New(n) => {
1156 if let ExprKind::Identifier(id) = &n.class.kind
1157 && fqn_short_name(id) == self.class_name
1158 {
1159 self.out.push(n.class.span);
1160 }
1161 }
1162 ExprKind::AnonymousClass(c) => {
1163 if let Some(ext) = &c.extends {
1164 self.collect_name(ext);
1165 }
1166 for iface in c.implements.iter() {
1167 self.collect_name(iface);
1168 }
1169 }
1170 ExprKind::Binary(b) => {
1171 if let ExprKind::Identifier(id) = &b.right.kind
1172 && fqn_short_name(id) == self.class_name
1173 {
1174 self.out.push(b.right.span);
1175 }
1176 }
1177 ExprKind::StaticMethodCall(s) => {
1178 if let ExprKind::Identifier(id) = &s.class.kind
1179 && fqn_short_name(id) == self.class_name
1180 {
1181 self.out.push(s.class.span);
1182 }
1183 }
1184 ExprKind::StaticPropertyAccess(s) => {
1185 if let ExprKind::Identifier(id) = &s.class.kind
1186 && fqn_short_name(id) == self.class_name
1187 {
1188 self.out.push(s.class.span);
1189 }
1190 }
1191 ExprKind::ClassConstAccess(c) => {
1192 if let ExprKind::Identifier(id) = &c.class.kind
1193 && fqn_short_name(id) == self.class_name
1194 {
1195 self.out.push(c.class.span);
1196 }
1197 }
1198 _ => {}
1199 }
1200 walk_expr(self, expr)
1201 }
1202
1203 fn visit_attribute(&mut self, attribute: &Attribute<'arena, 'src>) -> ControlFlow<()> {
1204 self.collect_name(&attribute.name);
1205 walk_attribute(self, attribute)
1206 }
1207
1208 fn visit_type_hint(&mut self, type_hint: &TypeHint<'arena, 'src>) -> ControlFlow<()> {
1209 match &type_hint.kind {
1210 TypeHintKind::Named(name) => {
1211 self.collect_name(name);
1212 walk_type_hint(self, type_hint)
1213 }
1214 TypeHintKind::Nullable(_) => walk_type_hint(self, type_hint),
1215 TypeHintKind::Union(types) => {
1216 for inner in types.iter() {
1217 let _ = self.visit_type_hint(inner);
1218 }
1219 ControlFlow::Continue(())
1220 }
1221 TypeHintKind::Intersection(types) => {
1222 for inner in types.iter() {
1223 let _ = self.visit_type_hint(inner);
1224 }
1225 ControlFlow::Continue(())
1226 }
1227 TypeHintKind::Keyword(_, _) => ControlFlow::Continue(()),
1228 }
1229 }
1230
1231 fn visit_catch_clause(&mut self, catch: &CatchClause<'arena, 'src>) -> ControlFlow<()> {
1232 for ty in catch.types.iter() {
1233 self.collect_name(ty);
1234 }
1235 walk_catch_clause(self, catch)
1236 }
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242 use crate::ast::ParsedDoc;
1243
1244 fn spans_to_strs<'a>(source: &'a str, spans: &[Span]) -> Vec<&'a str> {
1246 spans
1247 .iter()
1248 .map(|s| &source[s.start as usize..s.end as usize])
1249 .collect()
1250 }
1251
1252 fn parse(src: &str) -> ParsedDoc {
1253 ParsedDoc::parse(src.to_string())
1254 }
1255
1256 #[test]
1259 fn refs_finds_function_declaration_and_call() {
1260 let src = "<?php\nfunction greet() {}\ngreet();";
1261 let doc = parse(src);
1262 let mut out = vec![];
1263 refs_in_stmts(src, &doc.program().stmts, "greet", &mut out);
1264 let texts = spans_to_strs(src, &out);
1265 assert!(texts.contains(&"greet"), "expected function decl name");
1266 assert_eq!(texts.iter().filter(|&&t| t == "greet").count(), 2);
1267 }
1268
1269 #[test]
1270 fn refs_finds_class_declaration_and_new() {
1271 let src = "<?php\nclass Foo {}\n$x = new Foo();";
1272 let doc = parse(src);
1273 let mut out = vec![];
1274 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
1275 let texts = spans_to_strs(src, &out);
1276 assert!(texts.iter().all(|&t| t == "Foo"));
1277 assert_eq!(texts.len(), 2);
1278 }
1279
1280 #[test]
1281 fn refs_finds_method_declaration_inside_class() {
1282 let src = "<?php\nclass Bar { function run() { $this->run(); } }";
1283 let doc = parse(src);
1284 let mut out = vec![];
1285 refs_in_stmts(src, &doc.program().stmts, "run", &mut out);
1286 let texts = spans_to_strs(src, &out);
1287 assert!(texts.iter().any(|&t| t == "run"));
1289 }
1290
1291 #[test]
1292 fn refs_returns_empty_for_unknown_name() {
1293 let src = "<?php\nfunction greet() {}";
1294 let doc = parse(src);
1295 let mut out = vec![];
1296 refs_in_stmts(src, &doc.program().stmts, "nope", &mut out);
1297 assert!(out.is_empty());
1298 }
1299
1300 #[test]
1303 fn refs_with_use_includes_use_import() {
1304 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
1305 let doc = parse(src);
1306 let mut out = vec![];
1307 refs_in_stmts_with_use(src, &doc.program().stmts, "Foo", &mut out);
1308 let texts = spans_to_strs(src, &out);
1309 assert!(
1311 texts.iter().filter(|&&t| t == "Foo").count() >= 2,
1312 "got: {texts:?}"
1313 );
1314 }
1315
1316 #[test]
1317 fn refs_without_use_misses_use_import() {
1318 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
1319 let doc = parse(src);
1320 let mut out = vec![];
1321 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
1322 let texts = spans_to_strs(src, &out);
1323 assert!(
1325 texts.iter().filter(|&&t| t == "Foo").count() < 2,
1326 "refs_in_stmts should not include use import; got: {texts:?}"
1327 );
1328 }
1329
1330 #[test]
1333 fn var_refs_finds_variable_in_assignment_and_echo() {
1334 let src = "<?php\n$x = 1;\necho $x;";
1335 let doc = parse(src);
1336 let mut out = vec![];
1337 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1338 assert_eq!(out.len(), 2, "expected $x in assignment and echo");
1339 }
1340
1341 #[test]
1342 fn var_refs_respects_function_scope_boundary() {
1343 let src = "<?php\n$x = 1;\nfunction inner() { $x = 2; }";
1345 let doc = parse(src);
1346 let mut out = vec![];
1347 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1348 assert_eq!(out.len(), 1, "inner $x must not cross scope boundary");
1350 }
1351
1352 #[test]
1353 fn var_refs_traverses_if_while_for_foreach() {
1354 let src = "<?php\n$x = 0;\nif ($x) { $x++; }\nwhile ($x > 0) { $x--; }\nfor ($x = 0; $x < 3; $x++) {}\nforeach ([$x] as $v) {}";
1355 let doc = parse(src);
1356 let mut out = vec![];
1357 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1358 assert!(
1359 out.len() >= 5,
1360 "expected multiple $x refs, got {}",
1361 out.len()
1362 );
1363 }
1364
1365 #[test]
1366 fn var_refs_does_not_cross_closure_boundary() {
1367 let src = "<?php\n$x = 1;\n$f = function() { $x = 2; };";
1368 let doc = parse(src);
1369 let mut out = vec![];
1370 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1371 assert_eq!(
1373 out.len(),
1374 1,
1375 "closure $x must not be collected by outer scope walk"
1376 );
1377 }
1378
1379 #[test]
1382 fn collect_scope_finds_var_inside_function() {
1383 let src = "<?php\nfunction foo($x) { return $x + 1; }";
1384 let doc = parse(src);
1385 let byte_off = src.find("return").unwrap();
1387 let mut out = vec![];
1388 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
1389 assert!(
1391 out.len() >= 2,
1392 "expected param + body ref, got {}",
1393 out.len()
1394 );
1395 }
1396
1397 #[test]
1398 fn collect_scope_top_level_when_no_function() {
1399 let src = "<?php\n$x = 1;\necho $x;";
1400 let doc = parse(src);
1401 let byte_off = src.find("echo").unwrap();
1402 let mut out = vec![];
1403 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
1404 assert_eq!(out.len(), 2);
1405 }
1406
1407 #[test]
1408 fn collect_scope_finds_var_inside_enum_method() {
1409 let src = "<?php\nenum Status {\n public function label($arg) { return $arg; }\n}";
1410 let doc = parse(src);
1411 let byte_off = src.find("return").unwrap();
1412 let mut out = vec![];
1413 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
1414 assert!(
1415 out.len() >= 2,
1416 "expected param + body ref in enum method, got {}",
1417 out.len()
1418 );
1419 }
1420
1421 #[test]
1422 fn collect_scope_does_not_bleed_enum_method_into_outer_scope() {
1423 let src =
1424 "<?php\n$arg = 1;\nenum Status {\n public function label($arg) { return $arg; }\n}";
1425 let doc = parse(src);
1426 let byte_off = src.find("$arg").unwrap();
1428 let mut out = vec![];
1429 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
1430 assert_eq!(
1432 out.len(),
1433 1,
1434 "enum method $arg must not bleed into outer scope"
1435 );
1436 }
1437
1438 #[test]
1441 fn property_refs_finds_declaration_and_access() {
1442 let src = "<?php\nclass Baz { public int $val = 0; function get() { return $this->val; } }";
1443 let doc = parse(src);
1444 let mut out = vec![];
1445 property_refs_in_stmts(src, &doc.program().stmts, "val", &mut out);
1446 assert_eq!(out.len(), 2, "expected decl + access, got {}", out.len());
1448 }
1449
1450 #[test]
1451 fn property_refs_finds_nullsafe_access() {
1452 let src = "<?php\n$r = $obj?->name;";
1453 let doc = parse(src);
1454 let mut out = vec![];
1455 property_refs_in_stmts(src, &doc.program().stmts, "name", &mut out);
1456 assert_eq!(out.len(), 1);
1457 }
1458
1459 #[test]
1460 fn property_refs_finds_static_access() {
1461 let src = "<?php\nclass Reg { public static int $val = 0; }\nReg::$val;\nReg::$val = 1;";
1462 let doc = parse(src);
1463 let mut out = vec![];
1464 property_refs_in_stmts(src, &doc.program().stmts, "val", &mut out);
1465 assert_eq!(out.len(), 3, "expected decl + 2 accesses, got: {out:?}");
1467 }
1468
1469 #[test]
1472 fn constant_refs_finds_decl_and_class_access() {
1473 let src = "<?php\nclass S { const ACTIVE = 1; }\n$x = S::ACTIVE;\nif ($v === S::ACTIVE) {}";
1474 let doc = parse(src);
1475 let mut out = vec![];
1476 constant_refs_in_stmts(src, &doc.program().stmts, "ACTIVE", None, &mut out);
1477 assert_eq!(out.len(), 3, "expected decl + 2 accesses, got: {out:?}");
1479 }
1480
1481 #[test]
1482 fn constant_refs_finds_self_and_parent_access() {
1483 let src = "<?php\nclass Base { const V = 1; }\nclass Child extends Base { public function f(): int { return parent::V; } }";
1484 let doc = parse(src);
1485 let mut out = vec![];
1486 constant_refs_in_stmts(src, &doc.program().stmts, "V", Some("Base"), &mut out);
1487 let texts = spans_to_strs(src, &out);
1488 assert!(
1490 out.len() >= 2,
1491 "expected decl + parent::V access, got: {texts:?}"
1492 );
1493 }
1494
1495 #[test]
1496 fn constant_refs_parent_reference_full_source() {
1497 let src = "<?php\nclass Base {\n const VERSION = '1.0';\n}\n\nclass Extended extends Base {\n public function getVersion(): string {\n return parent::VERSION;\n }\n}\n\necho Extended::VERSION;";
1498 let doc = parse(src);
1499 let mut out = vec![];
1500 constant_refs_in_stmts(src, &doc.program().stmts, "VERSION", Some("Base"), &mut out);
1501 let texts = spans_to_strs(src, &out);
1502 assert!(
1503 out.len() >= 3,
1504 "expected decl + parent::VERSION + Extended::VERSION = 3, got {}: {texts:?}",
1505 out.len()
1506 );
1507 }
1508
1509 #[test]
1510 fn constant_refs_filters_same_name_different_class() {
1511 let src = "<?php\nclass A { const X = 1; }\nclass B { const X = 2; }\nA::X;\nB::X;";
1512 let doc = parse(src);
1513 let mut out = vec![];
1514 constant_refs_in_stmts(src, &doc.program().stmts, "X", Some("A"), &mut out);
1515 let texts = spans_to_strs(src, &out);
1517 assert!(!texts.is_empty(), "should find A::X: {texts:?}");
1518 }
1519
1520 #[test]
1523 fn function_refs_only_matches_free_calls_not_methods() {
1524 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
1525 let doc = parse(src);
1526 let mut out = vec![];
1527 function_refs_in_stmts(&doc.program().stmts, "run", &mut out);
1528 assert_eq!(out.len(), 1, "got: {out:?}");
1530 }
1531
1532 #[test]
1535 fn method_refs_only_matches_method_calls_not_free_functions() {
1536 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
1537 let doc = parse(src);
1538 let mut out = vec![];
1539 method_refs_in_stmts(&doc.program().stmts, "run", &mut out);
1540 assert_eq!(out.len(), 1, "got: {out:?}");
1542 }
1543
1544 #[test]
1545 fn method_refs_finds_nullsafe_method_call() {
1546 let src = "<?php\n$obj?->process();";
1547 let doc = parse(src);
1548 let mut out = vec![];
1549 method_refs_in_stmts(&doc.program().stmts, "process", &mut out);
1550 assert_eq!(out.len(), 1);
1551 }
1552
1553 #[test]
1556 fn class_refs_finds_new_and_extends() {
1557 let src = "<?php\nclass Child extends Base {}\n$x = new Base();";
1558 let doc = parse(src);
1559 let mut out = vec![];
1560 class_refs_in_stmts(&doc.program().stmts, "Base", &mut out);
1561 assert!(out.len() >= 2, "expected extends + new, got {}", out.len());
1562 }
1563
1564 #[test]
1565 fn class_refs_does_not_match_free_function_with_same_name() {
1566 let src = "<?php\nfunction Foo() {}\nFoo();";
1567 let doc = parse(src);
1568 let mut out = vec![];
1569 class_refs_in_stmts(&doc.program().stmts, "Foo", &mut out);
1570 assert!(
1571 out.is_empty(),
1572 "free function call must not be a class ref; got: {out:?}"
1573 );
1574 }
1575
1576 #[test]
1577 fn class_refs_finds_type_hint_in_function_param() {
1578 let src = "<?php\nfunction take(MyClass $obj): MyClass { return $obj; }";
1579 let doc = parse(src);
1580 let mut out = vec![];
1581 class_refs_in_stmts(&doc.program().stmts, "MyClass", &mut out);
1582 assert_eq!(out.len(), 2, "got {out:?}");
1584 }
1585
1586 #[test]
1589 fn all_class_refs_collects_extends_and_implements() {
1590 let src = "<?php\nclass A extends B implements C, D {}";
1591 let doc = parse(src);
1592 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1593 assert_eq!(out, vec!["B", "C", "D"]);
1594 }
1595
1596 #[test]
1597 fn all_class_refs_collects_interface_extends() {
1598 let src = "<?php\ninterface I extends J, K {}";
1599 let doc = parse(src);
1600 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1601 assert_eq!(out, vec!["J", "K"]);
1602 }
1603
1604 #[test]
1605 fn all_class_refs_collects_new_bare_and_fqn() {
1606 let src = "<?php\n$a = new Local();\n$b = new \\Vendor\\Pkg\\Cls();";
1607 let doc = parse(src);
1608 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1609 assert!(out.contains(&"Local".to_string()));
1610 assert!(out.contains(&"\\Vendor\\Pkg\\Cls".to_string()));
1611 }
1612
1613 #[test]
1614 fn all_class_refs_collects_instanceof() {
1615 let src = "<?php\nif ($x instanceof MyClass) {}";
1616 let doc = parse(src);
1617 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1618 assert!(out.contains(&"MyClass".to_string()));
1619 }
1620
1621 #[test]
1622 fn all_class_refs_collects_static_call_property_const() {
1623 let src = "<?php\nA::method();\nB::$prop;\nC::CONST;\n$x = D::class;";
1624 let doc = parse(src);
1625 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1626 assert!(out.contains(&"A".to_string()), "A::method() — got {out:?}");
1627 assert!(out.contains(&"B".to_string()), "B::$prop — got {out:?}");
1628 assert!(out.contains(&"C".to_string()), "C::CONST — got {out:?}");
1629 assert!(out.contains(&"D".to_string()), "D::class — got {out:?}");
1630 }
1631
1632 #[test]
1633 fn all_class_refs_collects_type_hints_in_all_positions() {
1634 let src = "<?php\nclass C {\n public P $prop;\n public function f(Q $q): R { return $q; }\n}";
1635 let doc = parse(src);
1636 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1637 assert!(
1638 out.contains(&"P".to_string()),
1639 "property type — got {out:?}"
1640 );
1641 assert!(out.contains(&"Q".to_string()), "param type — got {out:?}");
1642 assert!(out.contains(&"R".to_string()), "return type — got {out:?}");
1643 }
1644
1645 #[test]
1646 fn all_class_refs_collects_catch_types() {
1647 let src = "<?php\ntry {} catch (FirstException | SecondException $e) {}";
1648 let doc = parse(src);
1649 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1650 assert!(out.contains(&"FirstException".to_string()));
1651 assert!(out.contains(&"SecondException".to_string()));
1652 }
1653
1654 #[test]
1655 fn all_class_refs_does_not_collect_free_function_calls_or_method_names() {
1656 let src = "<?php\nrun();\n$obj->run();";
1657 let doc = parse(src);
1658 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1659 assert!(
1660 !out.contains(&"run".to_string()),
1661 "function call / method must not be a class ref; got {out:?}"
1662 );
1663 }
1664
1665 #[test]
1666 fn all_class_refs_collects_trait_use_in_class() {
1667 let src = "<?php\nclass C {\n use TraitOne, TraitTwo;\n}";
1668 let doc = parse(src);
1669 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1670 assert!(out.contains(&"TraitOne".to_string()), "got {out:?}");
1671 assert!(out.contains(&"TraitTwo".to_string()), "got {out:?}");
1672 }
1673
1674 #[test]
1675 fn all_class_refs_collects_trait_use_in_enum() {
1676 let src = "<?php\nenum E: int {\n use TraitEnum;\n case A = 1;\n}";
1677 let doc = parse(src);
1678 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1679 assert!(out.contains(&"TraitEnum".to_string()), "got {out:?}");
1680 }
1681
1682 #[test]
1683 fn all_class_refs_deduplicates() {
1684 let src = "<?php\n$a = new X();\n$b = new X();\n$c instanceof X;";
1685 let doc = parse(src);
1686 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1687 assert_eq!(out.iter().filter(|s| s == &"X").count(), 1);
1688 }
1689
1690 #[test]
1691 fn all_class_refs_collects_attribute_names() {
1692 let src = "<?php\n#[MyAttr]\nclass Foo {}\n#[ORM\\Entity]\nclass Bar {}";
1693 let doc = parse(src);
1694 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1695 assert!(
1696 out.contains(&"MyAttr".to_string()),
1697 "simple attribute — got {out:?}"
1698 );
1699 assert!(
1700 out.contains(&"ORM\\Entity".to_string()),
1701 "qualified attribute — got {out:?}"
1702 );
1703 }
1704
1705 #[test]
1706 fn all_class_refs_collects_anonymous_class_extends_and_implements() {
1707 let src = "<?php\n$x = new class extends Base implements Countable {};";
1708 let doc = parse(src);
1709 let out = all_class_ref_names_in_stmts(&doc.program().stmts);
1710 assert!(
1711 out.contains(&"Base".to_string()),
1712 "anon class extends — got {out:?}"
1713 );
1714 assert!(
1715 out.contains(&"Countable".to_string()),
1716 "anon class implements — got {out:?}"
1717 );
1718 }
1719}