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 var_refs_in_stmts(body, var_name, out);
244 }
245 for p in m.params.iter() {
246 if p.name == var_name {
247 out.push(p.span);
248 }
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 var_refs_in_stmts(body, var_name, out);
269 }
270 for p in m.params.iter() {
271 if p.name == var_name {
272 out.push(p.span);
273 }
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 var_refs_in_stmts(body, var_name, out);
319 }
320 for p in m.params.iter() {
321 if p.name == var_name {
322 out.push(p.span);
323 }
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 match &member.kind {
391 ClassMemberKind::Property(p) if p.name == self.prop_name => {
392 let offset = str_offset(self.source, p.name);
393 self.out.push(Span {
394 start: offset,
395 end: offset + p.name.len() as u32,
396 });
397 }
398 ClassMemberKind::Method(m) if m.name == "__construct" => {
400 for p in m.params.iter() {
401 if p.visibility.is_some() && p.name == self.prop_name {
402 let offset = str_offset(self.source, p.name);
403 self.out.push(Span {
404 start: offset,
405 end: offset + p.name.len() as u32,
406 });
407 }
408 }
409 }
410 _ => {}
411 }
412 walk_class_member(self, member)
413 }
414}
415
416pub fn function_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
422 let mut v = FunctionRefsVisitor {
423 name,
424 out: Vec::new(),
425 };
426 for stmt in stmts {
427 let _ = v.visit_stmt(stmt);
428 }
429 out.append(&mut v.out);
430}
431
432struct FunctionRefsVisitor<'a> {
433 name: &'a str,
434 out: Vec<Span>,
435}
436
437impl<'arena, 'src> Visitor<'arena, 'src> for FunctionRefsVisitor<'_> {
438 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
439 if let ExprKind::FunctionCall(f) = &expr.kind
440 && let ExprKind::Identifier(id) = &f.name.kind
441 && id.as_str() == self.name
442 {
443 self.out.push(f.name.span);
444 }
445 walk_expr(self, expr)
446 }
447}
448
449pub fn method_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
454 let mut v = MethodRefsVisitor {
455 name,
456 out: Vec::new(),
457 };
458 for stmt in stmts {
459 let _ = v.visit_stmt(stmt);
460 }
461 out.append(&mut v.out);
462}
463
464struct MethodRefsVisitor<'a> {
465 name: &'a str,
466 out: Vec<Span>,
467}
468
469impl<'arena, 'src> Visitor<'arena, 'src> for MethodRefsVisitor<'_> {
470 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
471 match &expr.kind {
472 ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
473 if let ExprKind::Identifier(id) = &m.method.kind
474 && id.as_str() == self.name
475 {
476 self.out.push(m.method.span);
477 }
478 }
479 ExprKind::StaticMethodCall(s) => {
480 if s.method.name_str() == Some(self.name) {
481 self.out.push(s.method.span);
482 }
483 }
484 _ => {}
485 }
486 walk_expr(self, expr)
487 }
488}
489
490pub fn new_refs_in_stmts(
499 stmts: &[Stmt<'_, '_>],
500 class_name: &str,
501 class_fqn: Option<&str>,
502 out: &mut Vec<Span>,
503) {
504 let mut v = NewRefsVisitor {
505 class_name,
506 class_fqn,
507 out: Vec::new(),
508 };
509 for stmt in stmts {
510 let _ = v.visit_stmt(stmt);
511 }
512 out.append(&mut v.out);
513}
514
515struct NewRefsVisitor<'a> {
516 class_name: &'a str,
517 class_fqn: Option<&'a str>,
518 out: Vec<Span>,
519}
520
521impl<'arena, 'src> Visitor<'arena, 'src> for NewRefsVisitor<'_> {
522 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
523 if let ExprKind::New(n) = &expr.kind
524 && let ExprKind::Identifier(id) = &n.class.kind
525 {
526 let matches = if id.contains('\\')
527 && let Some(fqn) = self.class_fqn
528 {
529 id.trim_start_matches('\\') == fqn.trim_start_matches('\\')
531 } else {
532 id.rsplit('\\').next().unwrap_or(id) == self.class_name
533 };
534 if matches {
535 self.out.push(n.class.span);
536 }
537 }
538 walk_expr(self, expr)
539 }
540}
541
542pub fn class_refs_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str, out: &mut Vec<Span>) {
546 let mut v = ClassRefsVisitor {
547 class_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 ClassRefsVisitor<'a> {
557 class_name: &'a str,
558 out: Vec<Span>,
559}
560
561impl ClassRefsVisitor<'_> {
562 fn collect_name<'a, 'b>(&mut self, name: &Name<'a, 'b>) {
564 let repr = name.to_string_repr();
565 let last = repr.rsplit('\\').next().unwrap_or(repr.as_ref());
566 if last == self.class_name {
567 let span = name.span();
568 let offset = (repr.len() - last.len()) as u32;
569 self.out.push(Span {
570 start: span.start + offset,
571 end: span.end,
572 });
573 }
574 }
575}
576
577impl<'arena, 'src> Visitor<'arena, 'src> for ClassRefsVisitor<'_> {
578 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
579 match &stmt.kind {
580 StmtKind::Class(c) => {
581 if let Some(ext) = &c.extends {
582 self.collect_name(ext);
583 }
584 for iface in c.implements.iter() {
585 self.collect_name(iface);
586 }
587 }
588 StmtKind::Interface(i) => {
589 for parent in i.extends.iter() {
590 self.collect_name(parent);
591 }
592 }
593 _ => {}
594 }
595 walk_stmt(self, stmt)
596 }
597
598 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
599 match &expr.kind {
600 ExprKind::New(n) => {
601 if let ExprKind::Identifier(id) = &n.class.kind
602 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
603 {
604 self.out.push(n.class.span);
605 }
606 }
607 ExprKind::Binary(b) => {
608 if let ExprKind::Identifier(id) = &b.right.kind
609 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
610 {
611 self.out.push(b.right.span);
612 }
613 }
614 ExprKind::StaticMethodCall(s) => {
615 if let ExprKind::Identifier(id) = &s.class.kind
616 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
617 {
618 self.out.push(s.class.span);
619 }
620 }
621 ExprKind::StaticPropertyAccess(s) => {
622 if let ExprKind::Identifier(id) = &s.class.kind
623 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
624 {
625 self.out.push(s.class.span);
626 }
627 }
628 ExprKind::ClassConstAccess(c) => {
629 if let ExprKind::Identifier(id) = &c.class.kind
630 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
631 {
632 self.out.push(c.class.span);
633 }
634 }
635 _ => {}
636 }
637 walk_expr(self, expr)
638 }
639
640 fn visit_type_hint(&mut self, type_hint: &TypeHint<'arena, 'src>) -> ControlFlow<()> {
641 if let TypeHintKind::Named(name) = &type_hint.kind {
642 self.collect_name(name);
643 }
644 walk_type_hint(self, type_hint)
645 }
646
647 fn visit_catch_clause(&mut self, catch: &CatchClause<'arena, 'src>) -> ControlFlow<()> {
648 for ty in catch.types.iter() {
649 self.collect_name(ty);
650 }
651 walk_catch_clause(self, catch)
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::ast::ParsedDoc;
659
660 fn spans_to_strs<'a>(source: &'a str, spans: &[Span]) -> Vec<&'a str> {
662 spans
663 .iter()
664 .map(|s| &source[s.start as usize..s.end as usize])
665 .collect()
666 }
667
668 fn parse(src: &str) -> ParsedDoc {
669 ParsedDoc::parse(src.to_string())
670 }
671
672 #[test]
675 fn refs_finds_function_declaration_and_call() {
676 let src = "<?php\nfunction greet() {}\ngreet();";
677 let doc = parse(src);
678 let mut out = vec![];
679 refs_in_stmts(src, &doc.program().stmts, "greet", &mut out);
680 let texts = spans_to_strs(src, &out);
681 assert!(texts.contains(&"greet"), "expected function decl name");
682 assert_eq!(texts.iter().filter(|&&t| t == "greet").count(), 2);
683 }
684
685 #[test]
686 fn refs_finds_class_declaration_and_new() {
687 let src = "<?php\nclass Foo {}\n$x = new Foo();";
688 let doc = parse(src);
689 let mut out = vec![];
690 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
691 let texts = spans_to_strs(src, &out);
692 assert!(texts.iter().all(|&t| t == "Foo"));
693 assert_eq!(texts.len(), 2);
694 }
695
696 #[test]
697 fn refs_finds_method_declaration_inside_class() {
698 let src = "<?php\nclass Bar { function run() { $this->run(); } }";
699 let doc = parse(src);
700 let mut out = vec![];
701 refs_in_stmts(src, &doc.program().stmts, "run", &mut out);
702 let texts = spans_to_strs(src, &out);
703 assert!(texts.iter().any(|&t| t == "run"));
705 }
706
707 #[test]
708 fn refs_returns_empty_for_unknown_name() {
709 let src = "<?php\nfunction greet() {}";
710 let doc = parse(src);
711 let mut out = vec![];
712 refs_in_stmts(src, &doc.program().stmts, "nope", &mut out);
713 assert!(out.is_empty());
714 }
715
716 #[test]
719 fn refs_with_use_includes_use_import() {
720 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
721 let doc = parse(src);
722 let mut out = vec![];
723 refs_in_stmts_with_use(src, &doc.program().stmts, "Foo", &mut out);
724 let texts = spans_to_strs(src, &out);
725 assert!(
727 texts.iter().filter(|&&t| t == "Foo").count() >= 2,
728 "got: {texts:?}"
729 );
730 }
731
732 #[test]
733 fn refs_without_use_misses_use_import() {
734 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
735 let doc = parse(src);
736 let mut out = vec![];
737 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
738 let texts = spans_to_strs(src, &out);
739 assert!(
741 texts.iter().filter(|&&t| t == "Foo").count() < 2,
742 "refs_in_stmts should not include use import; got: {texts:?}"
743 );
744 }
745
746 #[test]
749 fn var_refs_finds_variable_in_assignment_and_echo() {
750 let src = "<?php\n$x = 1;\necho $x;";
751 let doc = parse(src);
752 let mut out = vec![];
753 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
754 assert_eq!(out.len(), 2, "expected $x in assignment and echo");
755 }
756
757 #[test]
758 fn var_refs_respects_function_scope_boundary() {
759 let src = "<?php\n$x = 1;\nfunction inner() { $x = 2; }";
761 let doc = parse(src);
762 let mut out = vec![];
763 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
764 assert_eq!(out.len(), 1, "inner $x must not cross scope boundary");
766 }
767
768 #[test]
769 fn var_refs_traverses_if_while_for_foreach() {
770 let src = "<?php\n$x = 0;\nif ($x) { $x++; }\nwhile ($x > 0) { $x--; }\nfor ($x = 0; $x < 3; $x++) {}\nforeach ([$x] as $v) {}";
771 let doc = parse(src);
772 let mut out = vec![];
773 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
774 assert!(
775 out.len() >= 5,
776 "expected multiple $x refs, got {}",
777 out.len()
778 );
779 }
780
781 #[test]
782 fn var_refs_does_not_cross_closure_boundary() {
783 let src = "<?php\n$x = 1;\n$f = function() { $x = 2; };";
784 let doc = parse(src);
785 let mut out = vec![];
786 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
787 assert_eq!(
789 out.len(),
790 1,
791 "closure $x must not be collected by outer scope walk"
792 );
793 }
794
795 #[test]
798 fn collect_scope_finds_var_inside_function() {
799 let src = "<?php\nfunction foo($x) { return $x + 1; }";
800 let doc = parse(src);
801 let byte_off = src.find("return").unwrap();
803 let mut out = vec![];
804 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
805 assert!(
807 out.len() >= 2,
808 "expected param + body ref, got {}",
809 out.len()
810 );
811 }
812
813 #[test]
814 fn collect_scope_top_level_when_no_function() {
815 let src = "<?php\n$x = 1;\necho $x;";
816 let doc = parse(src);
817 let byte_off = src.find("echo").unwrap();
818 let mut out = vec![];
819 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
820 assert_eq!(out.len(), 2);
821 }
822
823 #[test]
824 fn collect_scope_finds_var_inside_enum_method() {
825 let src = "<?php\nenum Status {\n public function label($arg) { return $arg; }\n}";
826 let doc = parse(src);
827 let byte_off = src.find("return").unwrap();
828 let mut out = vec![];
829 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
830 assert!(
831 out.len() >= 2,
832 "expected param + body ref in enum method, got {}",
833 out.len()
834 );
835 }
836
837 #[test]
838 fn collect_scope_does_not_bleed_enum_method_into_outer_scope() {
839 let src =
840 "<?php\n$arg = 1;\nenum Status {\n public function label($arg) { return $arg; }\n}";
841 let doc = parse(src);
842 let byte_off = src.find("$arg").unwrap();
844 let mut out = vec![];
845 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
846 assert_eq!(
848 out.len(),
849 1,
850 "enum method $arg must not bleed into outer scope"
851 );
852 }
853
854 #[test]
857 fn property_refs_finds_declaration_and_access() {
858 let src = "<?php\nclass Baz { public int $val = 0; function get() { return $this->val; } }";
859 let doc = parse(src);
860 let mut out = vec![];
861 property_refs_in_stmts(src, &doc.program().stmts, "val", &mut out);
862 assert_eq!(out.len(), 2, "expected decl + access, got {}", out.len());
864 }
865
866 #[test]
867 fn property_refs_finds_nullsafe_access() {
868 let src = "<?php\n$r = $obj?->name;";
869 let doc = parse(src);
870 let mut out = vec![];
871 property_refs_in_stmts(src, &doc.program().stmts, "name", &mut out);
872 assert_eq!(out.len(), 1);
873 }
874
875 #[test]
878 fn function_refs_only_matches_free_calls_not_methods() {
879 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
880 let doc = parse(src);
881 let mut out = vec![];
882 function_refs_in_stmts(&doc.program().stmts, "run", &mut out);
883 assert_eq!(out.len(), 1, "got: {out:?}");
885 }
886
887 #[test]
890 fn method_refs_only_matches_method_calls_not_free_functions() {
891 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
892 let doc = parse(src);
893 let mut out = vec![];
894 method_refs_in_stmts(&doc.program().stmts, "run", &mut out);
895 assert_eq!(out.len(), 1, "got: {out:?}");
897 }
898
899 #[test]
900 fn method_refs_finds_nullsafe_method_call() {
901 let src = "<?php\n$obj?->process();";
902 let doc = parse(src);
903 let mut out = vec![];
904 method_refs_in_stmts(&doc.program().stmts, "process", &mut out);
905 assert_eq!(out.len(), 1);
906 }
907
908 #[test]
911 fn class_refs_finds_new_and_extends() {
912 let src = "<?php\nclass Child extends Base {}\n$x = new Base();";
913 let doc = parse(src);
914 let mut out = vec![];
915 class_refs_in_stmts(&doc.program().stmts, "Base", &mut out);
916 assert!(out.len() >= 2, "expected extends + new, got {}", out.len());
917 }
918
919 #[test]
920 fn class_refs_does_not_match_free_function_with_same_name() {
921 let src = "<?php\nfunction Foo() {}\nFoo();";
922 let doc = parse(src);
923 let mut out = vec![];
924 class_refs_in_stmts(&doc.program().stmts, "Foo", &mut out);
925 assert!(
926 out.is_empty(),
927 "free function call must not be a class ref; got: {out:?}"
928 );
929 }
930
931 #[test]
932 fn class_refs_finds_type_hint_in_function_param() {
933 let src = "<?php\nfunction take(MyClass $obj): MyClass { return $obj; }";
934 let doc = parse(src);
935 let mut out = vec![];
936 class_refs_in_stmts(&doc.program().stmts, "MyClass", &mut out);
937 assert_eq!(out.len(), 2, "got {out:?}");
939 }
940}