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