1use std::ops::ControlFlow;
4
5use php_ast::{
6 CatchClause, ClassMember, ClassMemberKind, EnumMember, EnumMemberKind, Expr, ExprKind, Name,
7 NamespaceBody, Span, Stmt, StmtKind, TypeHint, TypeHintKind,
8 visitor::{
9 Visitor, walk_catch_clause, walk_class_member, walk_enum_member, walk_expr, walk_stmt,
10 walk_type_hint,
11 },
12};
13
14use crate::ast::str_offset;
15
16pub fn refs_in_stmts(source: &str, stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
19 let mut v = AllRefsVisitor {
20 source,
21 word,
22 out: Vec::new(),
23 };
24 for stmt in stmts {
25 let _ = v.visit_stmt(stmt);
26 }
27 out.append(&mut v.out);
28}
29
30pub fn refs_in_stmts_with_use(
33 source: &str,
34 stmts: &[Stmt<'_, '_>],
35 word: &str,
36 out: &mut Vec<Span>,
37) {
38 refs_in_stmts(source, stmts, word, out);
39 use_refs(stmts, word, out);
40}
41
42fn use_refs(stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
43 for stmt in stmts {
44 match &stmt.kind {
45 StmtKind::Use(u) => {
46 for use_item in u.uses.iter() {
47 let fqn = use_item.name.to_string_repr().into_owned();
48 let alias_match = use_item.alias.map(|a| a == word).unwrap_or(false);
49 let last_seg = fqn.rsplit('\\').next().unwrap_or(&fqn);
50 if alias_match || last_seg == word {
51 let name_span = use_item.name.span();
52 let offset = (fqn.len() - last_seg.len()) as u32;
53 let syn_span = Span {
54 start: name_span.start + offset,
55 end: name_span.start + fqn.len() as u32,
56 };
57 out.push(syn_span);
58 }
59 }
60 }
61 StmtKind::Namespace(ns) => {
62 if let NamespaceBody::Braced(inner) = &ns.body {
63 use_refs(inner, word, out);
64 }
65 }
66 _ => {}
67 }
68 }
69}
70
71struct AllRefsVisitor<'a> {
74 source: &'a str,
75 word: &'a str,
76 out: Vec<Span>,
77}
78
79impl AllRefsVisitor<'_> {
80 fn push_name_str(&mut self, name: &str) {
81 if name == self.word {
82 let start = str_offset(self.source, name);
83 self.out.push(Span {
84 start,
85 end: start + name.len() as u32,
86 });
87 }
88 }
89}
90
91impl<'arena, 'src> Visitor<'arena, 'src> for AllRefsVisitor<'_> {
92 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
93 match &stmt.kind {
94 StmtKind::Function(f) => self.push_name_str(f.name),
95 StmtKind::Class(c) => {
96 if let Some(name) = c.name {
97 self.push_name_str(name);
98 }
99 }
100 StmtKind::Interface(i) => self.push_name_str(i.name),
101 StmtKind::Trait(t) => self.push_name_str(t.name),
102 StmtKind::Enum(e) => self.push_name_str(e.name),
103 _ => {}
104 }
105 walk_stmt(self, stmt)
106 }
107
108 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
109 if let ClassMemberKind::Method(m) = &member.kind {
110 self.push_name_str(m.name);
111 }
112 walk_class_member(self, member)
113 }
114
115 fn visit_enum_member(&mut self, member: &EnumMember<'arena, 'src>) -> ControlFlow<()> {
116 if let EnumMemberKind::Method(m) = &member.kind {
117 self.push_name_str(m.name);
118 }
119 walk_enum_member(self, member)
120 }
121
122 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
123 if let ExprKind::Identifier(name) = &expr.kind
124 && name.as_str() == self.word
125 {
126 self.out.push(expr.span);
127 }
128 walk_expr(self, expr)
129 }
130}
131
132pub fn var_refs_in_stmts(stmts: &[Stmt<'_, '_>], var_name: &str, out: &mut Vec<Span>) {
139 let mut v = VarRefsVisitor {
140 var_name,
141 out: Vec::new(),
142 };
143 for stmt in stmts {
144 let _ = v.visit_stmt(stmt);
145 }
146 out.append(&mut v.out);
147}
148
149struct VarRefsVisitor<'a> {
150 var_name: &'a str,
151 out: Vec<Span>,
152}
153
154impl<'arena, 'src> Visitor<'arena, 'src> for VarRefsVisitor<'_> {
155 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
156 match &stmt.kind {
158 StmtKind::Function(_)
159 | StmtKind::Class(_)
160 | StmtKind::Trait(_)
161 | StmtKind::Enum(_)
162 | StmtKind::Interface(_) => ControlFlow::Continue(()),
163 _ => walk_stmt(self, stmt),
164 }
165 }
166
167 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
168 match &expr.kind {
169 ExprKind::Variable(name) => {
171 if name.as_str() == self.var_name {
172 self.out.push(expr.span);
173 }
174 ControlFlow::Continue(())
175 }
176 ExprKind::Closure(_) | ExprKind::ArrowFunction(_) => ControlFlow::Continue(()),
178 _ => walk_expr(self, expr),
179 }
180 }
181}
182
183pub fn collect_var_refs_in_scope(
188 stmts: &[Stmt<'_, '_>],
189 var_name: &str,
190 byte_off: usize,
191 out: &mut Vec<Span>,
192) {
193 for stmt in stmts {
194 if collect_in_fn_at(stmt, var_name, byte_off, out) {
195 return;
196 }
197 }
198 var_refs_in_stmts(stmts, var_name, out);
200}
201
202fn collect_in_fn_at(
205 stmt: &Stmt<'_, '_>,
206 var_name: &str,
207 byte_off: usize,
208 out: &mut Vec<Span>,
209) -> bool {
210 match &stmt.kind {
211 StmtKind::Function(f) => {
212 if byte_off < stmt.span.start as usize || byte_off >= stmt.span.end as usize {
213 return false;
214 }
215 for inner in f.body.iter() {
217 if collect_in_fn_at(inner, var_name, byte_off, out) {
218 return true;
219 }
220 }
221 for p in f.params.iter() {
223 if p.name == var_name {
224 out.push(p.span);
225 }
226 }
227 var_refs_in_stmts(&f.body, var_name, out);
228 true
229 }
230 StmtKind::Class(c) => {
231 for member in c.members.iter() {
232 if let ClassMemberKind::Method(m) = &member.kind {
233 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
234 {
235 continue;
236 }
237 if let Some(body) = &m.body {
238 for inner in body.iter() {
239 if collect_in_fn_at(inner, var_name, byte_off, out) {
240 return true;
241 }
242 }
243 for p in m.params.iter() {
244 if p.name == var_name {
245 out.push(p.span);
246 }
247 }
248 var_refs_in_stmts(body, var_name, out);
249 }
250 return true;
251 }
252 }
253 false
254 }
255 StmtKind::Trait(t) => {
256 for member in t.members.iter() {
257 if let ClassMemberKind::Method(m) = &member.kind {
258 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
259 {
260 continue;
261 }
262 if let Some(body) = &m.body {
263 for inner in body.iter() {
264 if collect_in_fn_at(inner, var_name, byte_off, out) {
265 return true;
266 }
267 }
268 for p in m.params.iter() {
269 if p.name == var_name {
270 out.push(p.span);
271 }
272 }
273 var_refs_in_stmts(body, var_name, out);
274 }
275 return true;
276 }
277 }
278 false
279 }
280 StmtKind::Enum(e) => {
281 for member in e.members.iter() {
282 if let EnumMemberKind::Method(m) = &member.kind {
283 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
284 {
285 continue;
286 }
287 if let Some(body) = &m.body {
288 for inner in body.iter() {
289 if collect_in_fn_at(inner, var_name, byte_off, out) {
290 return true;
291 }
292 }
293 for p in m.params.iter() {
294 if p.name == var_name {
295 out.push(p.span);
296 }
297 }
298 var_refs_in_stmts(body, var_name, out);
299 }
300 return true;
301 }
302 }
303 false
304 }
305 StmtKind::Interface(i) => {
306 for member in i.members.iter() {
307 if let ClassMemberKind::Method(m) = &member.kind {
308 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
309 {
310 continue;
311 }
312 if let Some(body) = &m.body {
313 for inner in body.iter() {
314 if collect_in_fn_at(inner, var_name, byte_off, out) {
315 return true;
316 }
317 }
318 for p in m.params.iter() {
319 if p.name == var_name {
320 out.push(p.span);
321 }
322 }
323 var_refs_in_stmts(body, var_name, out);
324 }
325 return true;
326 }
327 }
328 false
329 }
330 StmtKind::Namespace(ns) => {
331 if let NamespaceBody::Braced(inner) = &ns.body {
332 for s in inner.iter() {
333 if collect_in_fn_at(s, var_name, byte_off, out) {
334 return true;
335 }
336 }
337 }
338 false
339 }
340 _ => false,
341 }
342}
343
344pub fn property_refs_in_stmts(
349 source: &str,
350 stmts: &[Stmt<'_, '_>],
351 prop_name: &str,
352 out: &mut Vec<Span>,
353) {
354 let mut v = PropertyRefsVisitor {
355 source,
356 prop_name,
357 out: Vec::new(),
358 };
359 for stmt in stmts {
360 let _ = v.visit_stmt(stmt);
361 }
362 out.append(&mut v.out);
363}
364
365struct PropertyRefsVisitor<'a> {
366 source: &'a str,
367 prop_name: &'a str,
368 out: Vec<Span>,
369}
370
371impl<'arena, 'src> Visitor<'arena, 'src> for PropertyRefsVisitor<'_> {
372 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
373 match &expr.kind {
374 ExprKind::PropertyAccess(p) | ExprKind::NullsafePropertyAccess(p) => {
375 let span = p.property.span;
376 let name_in_src = self
377 .source
378 .get(span.start as usize..span.end as usize)
379 .unwrap_or("");
380 if name_in_src == self.prop_name {
381 self.out.push(span);
382 }
383 }
384 _ => {}
385 }
386 walk_expr(self, expr)
387 }
388
389 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
390 if let ClassMemberKind::Property(p) = &member.kind
391 && p.name == self.prop_name
392 {
393 let offset = str_offset(self.source, p.name);
394 self.out.push(Span {
395 start: offset,
396 end: offset + p.name.len() as u32,
397 });
398 }
399 walk_class_member(self, member)
400 }
401}
402
403pub fn function_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
409 let mut v = FunctionRefsVisitor {
410 name,
411 out: Vec::new(),
412 };
413 for stmt in stmts {
414 let _ = v.visit_stmt(stmt);
415 }
416 out.append(&mut v.out);
417}
418
419struct FunctionRefsVisitor<'a> {
420 name: &'a str,
421 out: Vec<Span>,
422}
423
424impl<'arena, 'src> Visitor<'arena, 'src> for FunctionRefsVisitor<'_> {
425 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
426 if let ExprKind::FunctionCall(f) = &expr.kind
427 && let ExprKind::Identifier(id) = &f.name.kind
428 && id.as_str() == self.name
429 {
430 self.out.push(f.name.span);
431 }
432 walk_expr(self, expr)
433 }
434}
435
436pub fn method_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
441 let mut v = MethodRefsVisitor {
442 name,
443 out: Vec::new(),
444 };
445 for stmt in stmts {
446 let _ = v.visit_stmt(stmt);
447 }
448 out.append(&mut v.out);
449}
450
451struct MethodRefsVisitor<'a> {
452 name: &'a str,
453 out: Vec<Span>,
454}
455
456impl<'arena, 'src> Visitor<'arena, 'src> for MethodRefsVisitor<'_> {
457 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
458 match &expr.kind {
459 ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
460 if let ExprKind::Identifier(id) = &m.method.kind
461 && id.as_str() == self.name
462 {
463 self.out.push(m.method.span);
464 }
465 }
466 ExprKind::StaticMethodCall(s) => {
467 if s.method.name_str() == Some(self.name) {
468 self.out.push(s.method.span);
469 }
470 }
471 _ => {}
472 }
473 walk_expr(self, expr)
474 }
475}
476
477pub fn new_refs_in_stmts(
486 stmts: &[Stmt<'_, '_>],
487 class_name: &str,
488 class_fqn: Option<&str>,
489 out: &mut Vec<Span>,
490) {
491 let mut v = NewRefsVisitor {
492 class_name,
493 class_fqn,
494 out: Vec::new(),
495 };
496 for stmt in stmts {
497 let _ = v.visit_stmt(stmt);
498 }
499 out.append(&mut v.out);
500}
501
502struct NewRefsVisitor<'a> {
503 class_name: &'a str,
504 class_fqn: Option<&'a str>,
505 out: Vec<Span>,
506}
507
508impl<'arena, 'src> Visitor<'arena, 'src> for NewRefsVisitor<'_> {
509 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
510 if let ExprKind::New(n) = &expr.kind
511 && let ExprKind::Identifier(id) = &n.class.kind
512 {
513 let matches = if id.contains('\\')
514 && let Some(fqn) = self.class_fqn
515 {
516 id.trim_start_matches('\\') == fqn.trim_start_matches('\\')
518 } else {
519 id.rsplit('\\').next().unwrap_or(id) == self.class_name
520 };
521 if matches {
522 self.out.push(n.class.span);
523 }
524 }
525 walk_expr(self, expr)
526 }
527}
528
529pub fn class_refs_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str, out: &mut Vec<Span>) {
533 let mut v = ClassRefsVisitor {
534 class_name,
535 out: Vec::new(),
536 };
537 for stmt in stmts {
538 let _ = v.visit_stmt(stmt);
539 }
540 out.append(&mut v.out);
541}
542
543struct ClassRefsVisitor<'a> {
544 class_name: &'a str,
545 out: Vec<Span>,
546}
547
548impl ClassRefsVisitor<'_> {
549 fn collect_name<'a, 'b>(&mut self, name: &Name<'a, 'b>) {
551 let repr = name.to_string_repr();
552 let last = repr.rsplit('\\').next().unwrap_or(repr.as_ref());
553 if last == self.class_name {
554 let span = name.span();
555 let offset = (repr.len() - last.len()) as u32;
556 self.out.push(Span {
557 start: span.start + offset,
558 end: span.end,
559 });
560 }
561 }
562}
563
564impl<'arena, 'src> Visitor<'arena, 'src> for ClassRefsVisitor<'_> {
565 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
566 match &stmt.kind {
567 StmtKind::Class(c) => {
568 if let Some(ext) = &c.extends {
569 self.collect_name(ext);
570 }
571 for iface in c.implements.iter() {
572 self.collect_name(iface);
573 }
574 }
575 StmtKind::Interface(i) => {
576 for parent in i.extends.iter() {
577 self.collect_name(parent);
578 }
579 }
580 _ => {}
581 }
582 walk_stmt(self, stmt)
583 }
584
585 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
586 match &expr.kind {
587 ExprKind::New(n) => {
588 if let ExprKind::Identifier(id) = &n.class.kind
589 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
590 {
591 self.out.push(n.class.span);
592 }
593 }
594 ExprKind::Binary(b) => {
595 if let ExprKind::Identifier(id) = &b.right.kind
596 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
597 {
598 self.out.push(b.right.span);
599 }
600 }
601 ExprKind::StaticMethodCall(s) => {
602 if let ExprKind::Identifier(id) = &s.class.kind
603 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
604 {
605 self.out.push(s.class.span);
606 }
607 }
608 ExprKind::StaticPropertyAccess(s) => {
609 if let ExprKind::Identifier(id) = &s.class.kind
610 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
611 {
612 self.out.push(s.class.span);
613 }
614 }
615 ExprKind::ClassConstAccess(c) => {
616 if let ExprKind::Identifier(id) = &c.class.kind
617 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
618 {
619 self.out.push(c.class.span);
620 }
621 }
622 _ => {}
623 }
624 walk_expr(self, expr)
625 }
626
627 fn visit_type_hint(&mut self, type_hint: &TypeHint<'arena, 'src>) -> ControlFlow<()> {
628 if let TypeHintKind::Named(name) = &type_hint.kind {
629 self.collect_name(name);
630 }
631 walk_type_hint(self, type_hint)
632 }
633
634 fn visit_catch_clause(&mut self, catch: &CatchClause<'arena, 'src>) -> ControlFlow<()> {
635 for ty in catch.types.iter() {
636 self.collect_name(ty);
637 }
638 walk_catch_clause(self, catch)
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use crate::ast::ParsedDoc;
646
647 fn spans_to_strs<'a>(source: &'a str, spans: &[Span]) -> Vec<&'a str> {
649 spans
650 .iter()
651 .map(|s| &source[s.start as usize..s.end as usize])
652 .collect()
653 }
654
655 fn parse(src: &str) -> ParsedDoc {
656 ParsedDoc::parse(src.to_string())
657 }
658
659 #[test]
662 fn refs_finds_function_declaration_and_call() {
663 let src = "<?php\nfunction greet() {}\ngreet();";
664 let doc = parse(src);
665 let mut out = vec![];
666 refs_in_stmts(src, &doc.program().stmts, "greet", &mut out);
667 let texts = spans_to_strs(src, &out);
668 assert!(texts.contains(&"greet"), "expected function decl name");
669 assert_eq!(texts.iter().filter(|&&t| t == "greet").count(), 2);
670 }
671
672 #[test]
673 fn refs_finds_class_declaration_and_new() {
674 let src = "<?php\nclass Foo {}\n$x = new Foo();";
675 let doc = parse(src);
676 let mut out = vec![];
677 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
678 let texts = spans_to_strs(src, &out);
679 assert!(texts.iter().all(|&t| t == "Foo"));
680 assert_eq!(texts.len(), 2);
681 }
682
683 #[test]
684 fn refs_finds_method_declaration_inside_class() {
685 let src = "<?php\nclass Bar { function run() { $this->run(); } }";
686 let doc = parse(src);
687 let mut out = vec![];
688 refs_in_stmts(src, &doc.program().stmts, "run", &mut out);
689 let texts = spans_to_strs(src, &out);
690 assert!(texts.iter().any(|&t| t == "run"));
692 }
693
694 #[test]
695 fn refs_returns_empty_for_unknown_name() {
696 let src = "<?php\nfunction greet() {}";
697 let doc = parse(src);
698 let mut out = vec![];
699 refs_in_stmts(src, &doc.program().stmts, "nope", &mut out);
700 assert!(out.is_empty());
701 }
702
703 #[test]
706 fn refs_with_use_includes_use_import() {
707 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
708 let doc = parse(src);
709 let mut out = vec![];
710 refs_in_stmts_with_use(src, &doc.program().stmts, "Foo", &mut out);
711 let texts = spans_to_strs(src, &out);
712 assert!(
714 texts.iter().filter(|&&t| t == "Foo").count() >= 2,
715 "got: {texts:?}"
716 );
717 }
718
719 #[test]
720 fn refs_without_use_misses_use_import() {
721 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
722 let doc = parse(src);
723 let mut out = vec![];
724 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
725 let texts = spans_to_strs(src, &out);
726 assert!(
728 texts.iter().filter(|&&t| t == "Foo").count() < 2,
729 "refs_in_stmts should not include use import; got: {texts:?}"
730 );
731 }
732
733 #[test]
736 fn var_refs_finds_variable_in_assignment_and_echo() {
737 let src = "<?php\n$x = 1;\necho $x;";
738 let doc = parse(src);
739 let mut out = vec![];
740 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
741 assert_eq!(out.len(), 2, "expected $x in assignment and echo");
742 }
743
744 #[test]
745 fn var_refs_respects_function_scope_boundary() {
746 let src = "<?php\n$x = 1;\nfunction inner() { $x = 2; }";
748 let doc = parse(src);
749 let mut out = vec![];
750 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
751 assert_eq!(out.len(), 1, "inner $x must not cross scope boundary");
753 }
754
755 #[test]
756 fn var_refs_traverses_if_while_for_foreach() {
757 let src = "<?php\n$x = 0;\nif ($x) { $x++; }\nwhile ($x > 0) { $x--; }\nfor ($x = 0; $x < 3; $x++) {}\nforeach ([$x] as $v) {}";
758 let doc = parse(src);
759 let mut out = vec![];
760 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
761 assert!(
762 out.len() >= 5,
763 "expected multiple $x refs, got {}",
764 out.len()
765 );
766 }
767
768 #[test]
769 fn var_refs_does_not_cross_closure_boundary() {
770 let src = "<?php\n$x = 1;\n$f = function() { $x = 2; };";
771 let doc = parse(src);
772 let mut out = vec![];
773 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
774 assert_eq!(
776 out.len(),
777 1,
778 "closure $x must not be collected by outer scope walk"
779 );
780 }
781
782 #[test]
785 fn collect_scope_finds_var_inside_function() {
786 let src = "<?php\nfunction foo($x) { return $x + 1; }";
787 let doc = parse(src);
788 let byte_off = src.find("return").unwrap();
790 let mut out = vec![];
791 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
792 assert!(
794 out.len() >= 2,
795 "expected param + body ref, got {}",
796 out.len()
797 );
798 }
799
800 #[test]
801 fn collect_scope_top_level_when_no_function() {
802 let src = "<?php\n$x = 1;\necho $x;";
803 let doc = parse(src);
804 let byte_off = src.find("echo").unwrap();
805 let mut out = vec![];
806 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
807 assert_eq!(out.len(), 2);
808 }
809
810 #[test]
811 fn collect_scope_finds_var_inside_enum_method() {
812 let src = "<?php\nenum Status {\n public function label($arg) { return $arg; }\n}";
813 let doc = parse(src);
814 let byte_off = src.find("return").unwrap();
815 let mut out = vec![];
816 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
817 assert!(
818 out.len() >= 2,
819 "expected param + body ref in enum method, got {}",
820 out.len()
821 );
822 }
823
824 #[test]
825 fn collect_scope_does_not_bleed_enum_method_into_outer_scope() {
826 let src =
827 "<?php\n$arg = 1;\nenum Status {\n public function label($arg) { return $arg; }\n}";
828 let doc = parse(src);
829 let byte_off = src.find("$arg").unwrap();
831 let mut out = vec![];
832 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
833 assert_eq!(
835 out.len(),
836 1,
837 "enum method $arg must not bleed into outer scope"
838 );
839 }
840
841 #[test]
844 fn property_refs_finds_declaration_and_access() {
845 let src = "<?php\nclass Baz { public int $val = 0; function get() { return $this->val; } }";
846 let doc = parse(src);
847 let mut out = vec![];
848 property_refs_in_stmts(src, &doc.program().stmts, "val", &mut out);
849 assert_eq!(out.len(), 2, "expected decl + access, got {}", out.len());
851 }
852
853 #[test]
854 fn property_refs_finds_nullsafe_access() {
855 let src = "<?php\n$r = $obj?->name;";
856 let doc = parse(src);
857 let mut out = vec![];
858 property_refs_in_stmts(src, &doc.program().stmts, "name", &mut out);
859 assert_eq!(out.len(), 1);
860 }
861
862 #[test]
865 fn function_refs_only_matches_free_calls_not_methods() {
866 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
867 let doc = parse(src);
868 let mut out = vec![];
869 function_refs_in_stmts(&doc.program().stmts, "run", &mut out);
870 assert_eq!(out.len(), 1, "got: {out:?}");
872 }
873
874 #[test]
877 fn method_refs_only_matches_method_calls_not_free_functions() {
878 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
879 let doc = parse(src);
880 let mut out = vec![];
881 method_refs_in_stmts(&doc.program().stmts, "run", &mut out);
882 assert_eq!(out.len(), 1, "got: {out:?}");
884 }
885
886 #[test]
887 fn method_refs_finds_nullsafe_method_call() {
888 let src = "<?php\n$obj?->process();";
889 let doc = parse(src);
890 let mut out = vec![];
891 method_refs_in_stmts(&doc.program().stmts, "process", &mut out);
892 assert_eq!(out.len(), 1);
893 }
894
895 #[test]
898 fn class_refs_finds_new_and_extends() {
899 let src = "<?php\nclass Child extends Base {}\n$x = new Base();";
900 let doc = parse(src);
901 let mut out = vec![];
902 class_refs_in_stmts(&doc.program().stmts, "Base", &mut out);
903 assert!(out.len() >= 2, "expected extends + new, got {}", out.len());
904 }
905
906 #[test]
907 fn class_refs_does_not_match_free_function_with_same_name() {
908 let src = "<?php\nfunction Foo() {}\nFoo();";
909 let doc = parse(src);
910 let mut out = vec![];
911 class_refs_in_stmts(&doc.program().stmts, "Foo", &mut out);
912 assert!(
913 out.is_empty(),
914 "free function call must not be a class ref; got: {out:?}"
915 );
916 }
917
918 #[test]
919 fn class_refs_finds_type_hint_in_function_param() {
920 let src = "<?php\nfunction take(MyClass $obj): MyClass { return $obj; }";
921 let doc = parse(src);
922 let mut out = vec![];
923 class_refs_in_stmts(&doc.program().stmts, "MyClass", &mut out);
924 assert_eq!(out.len(), 2, "got {out:?}");
926 }
927}