1use std::ops::ControlFlow;
4
5use php_ast::{
6 CatchClause, ClassMember, ClassMemberKind, EnumMember, EnumMemberKind, Expr, ExprKind, Name,
7 NamespaceBody, Span, Stmt, StmtKind, TypeHint, TypeHintKind, UnaryPostfixOp, UnaryPrefixOp,
8 visitor::{
9 Visitor, walk_catch_clause, walk_class_member, walk_enum_member, walk_expr, walk_stmt,
10 walk_type_hint,
11 },
12};
13use tower_lsp::lsp_types::DocumentHighlightKind;
14
15use crate::ast::{str_offset, str_offset_in_range};
16
17pub fn refs_in_stmts(source: &str, stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
20 walk_all_refs(source, stmts, word, false, out);
21}
22
23pub fn refs_in_stmts_with_use(
26 source: &str,
27 stmts: &[Stmt<'_, '_>],
28 word: &str,
29 out: &mut Vec<Span>,
30) {
31 walk_all_refs(source, stmts, word, true, out);
32}
33
34fn walk_all_refs(
35 source: &str,
36 stmts: &[Stmt<'_, '_>],
37 word: &str,
38 include_use: bool,
39 out: &mut Vec<Span>,
40) {
41 let mut v = AllRefsVisitor {
42 source,
43 word,
44 include_use,
45 out: Vec::new(),
46 };
47 for stmt in stmts {
48 let _ = v.visit_stmt(stmt);
49 }
50 out.append(&mut v.out);
51}
52
53struct AllRefsVisitor<'a> {
56 source: &'a str,
57 word: &'a str,
58 include_use: bool,
59 out: Vec<Span>,
60}
61
62impl AllRefsVisitor<'_> {
63 fn push_name_str(&mut self, name: &str, stmt_span: Span) {
64 if name == self.word {
65 let start =
66 str_offset_in_range(self.source, stmt_span, name).unwrap_or(stmt_span.start);
67 self.out.push(Span {
68 start,
69 end: start + name.len() as u32,
70 });
71 }
72 }
73}
74
75impl<'arena, 'src> Visitor<'arena, 'src> for AllRefsVisitor<'_> {
76 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
77 match &stmt.kind {
78 StmtKind::Function(f) => self.push_name_str(&f.name.to_string(), stmt.span),
79 StmtKind::Class(c) => {
80 if let Some(name) = c.name {
81 self.push_name_str(&name.to_string(), stmt.span);
82 }
83 }
84 StmtKind::Interface(i) => self.push_name_str(&i.name.to_string(), stmt.span),
85 StmtKind::Trait(t) => self.push_name_str(&t.name.to_string(), stmt.span),
86 StmtKind::Enum(e) => self.push_name_str(&e.name.to_string(), stmt.span),
87 StmtKind::Use(u) if self.include_use => {
88 for use_item in u.uses.iter() {
89 let fqn = use_item.name.to_string_repr().into_owned();
90 if let Some(alias) = use_item.alias {
91 if alias == self.word {
93 if let Some(offset) = str_offset(self.source, alias) {
95 self.out.push(Span {
96 start: offset,
97 end: offset + alias.len() as u32,
98 });
99 }
100 }
101 } else {
102 let last_seg = fqn.rsplit('\\').next().unwrap_or(&fqn);
104 if last_seg == self.word {
105 let name_span = use_item.name.span();
106 let offset = (fqn.len() - last_seg.len()) as u32;
107 self.out.push(Span {
108 start: name_span.start + offset,
109 end: name_span.start + fqn.len() as u32,
110 });
111 }
112 }
113 }
114 }
115 _ => {}
116 }
117 walk_stmt(self, stmt)
118 }
119
120 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
121 match &member.kind {
122 ClassMemberKind::Method(m) if m.name == self.word => {
123 let name_str = m.name.to_string();
124 let start = str_offset_in_range(self.source, member.span, &name_str).unwrap_or(0);
129 self.out.push(Span {
130 start,
131 end: start + name_str.len() as u32,
132 });
133 }
134 ClassMemberKind::ClassConst(cc) if cc.name == self.word => {
135 let name_str = cc.name.to_string();
136 let start = str_offset_in_range(self.source, member.span, &name_str)
137 .unwrap_or_else(|| str_offset(self.source, &name_str).unwrap_or(0));
138 self.out.push(Span {
139 start,
140 end: start + name_str.len() as u32,
141 });
142 }
143 _ => {}
144 }
145 walk_class_member(self, member)
146 }
147
148 fn visit_enum_member(&mut self, member: &EnumMember<'arena, 'src>) -> ControlFlow<()> {
149 if let EnumMemberKind::Method(m) = &member.kind {
150 let start = str_offset(self.source, &m.name.to_string()).unwrap_or(0);
152 if m.name == self.word {
153 self.out.push(Span {
154 start,
155 end: start + m.name.to_string().len() as u32,
156 });
157 }
158 }
159 walk_enum_member(self, member)
160 }
161
162 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
163 if let ExprKind::Identifier(name) = &expr.kind
164 && name.as_str() == self.word
165 {
166 self.out.push(expr.span);
167 }
168 walk_expr(self, expr)
169 }
170}
171
172pub fn var_refs_in_stmts(
179 stmts: &[Stmt<'_, '_>],
180 var_name: &str,
181 out: &mut Vec<(Span, DocumentHighlightKind)>,
182) {
183 let mut v = VarRefsVisitor {
184 var_name,
185 out: Vec::new(),
186 };
187 for stmt in stmts {
188 let _ = v.visit_stmt(stmt);
189 }
190 out.append(&mut v.out);
191}
192
193struct VarRefsVisitor<'a> {
194 var_name: &'a str,
195 out: Vec<(Span, DocumentHighlightKind)>,
196}
197
198impl<'arena, 'src> Visitor<'arena, 'src> for VarRefsVisitor<'_> {
199 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
200 match &stmt.kind {
202 StmtKind::Function(_)
203 | StmtKind::Class(_)
204 | StmtKind::Trait(_)
205 | StmtKind::Enum(_)
206 | StmtKind::Interface(_) => ControlFlow::Continue(()),
207 StmtKind::Foreach(f) => {
208 if let Some(key) = &f.key
210 && let ExprKind::Variable(name) = &key.kind
211 && name.as_str() == self.var_name
212 {
213 self.out.push((key.span, DocumentHighlightKind::WRITE));
214 }
215 if let ExprKind::Variable(name) = &f.value.kind
216 && name.as_str() == self.var_name
217 {
218 self.out.push((f.value.span, DocumentHighlightKind::WRITE));
219 }
220 let _ = self.visit_expr(&f.expr);
222 let _ = self.visit_stmt(f.body);
223 ControlFlow::Continue(())
224 }
225 _ => walk_stmt(self, stmt),
226 }
227 }
228
229 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
230 match &expr.kind {
231 ExprKind::Variable(name) => {
233 if name.as_str() == self.var_name {
234 self.out.push((expr.span, DocumentHighlightKind::READ));
235 }
236 ControlFlow::Continue(())
237 }
238 ExprKind::Assign(a) => {
240 if let ExprKind::Variable(name) = &a.target.kind {
242 if name.as_str() == self.var_name {
243 self.out.push((a.target.span, DocumentHighlightKind::WRITE));
244 }
245 } else {
246 let _ = self.visit_expr(a.target);
247 }
248 let _ = self.visit_expr(a.value);
250 ControlFlow::Continue(())
251 }
252 ExprKind::UnaryPrefix(u) => {
254 if matches!(
255 u.op,
256 UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement
257 ) && let ExprKind::Variable(name) = &u.operand.kind
258 && name.as_str() == self.var_name
259 {
260 self.out
261 .push((u.operand.span, DocumentHighlightKind::WRITE));
262 return ControlFlow::Continue(());
263 }
264 walk_expr(self, expr)
265 }
266 ExprKind::UnaryPostfix(u) => {
267 if matches!(
268 u.op,
269 UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement
270 ) && let ExprKind::Variable(name) = &u.operand.kind
271 && name.as_str() == self.var_name
272 {
273 self.out
274 .push((u.operand.span, DocumentHighlightKind::WRITE));
275 return ControlFlow::Continue(());
276 }
277 walk_expr(self, expr)
278 }
279 ExprKind::Closure(c) => {
281 for use_var in c.use_vars.iter() {
283 if use_var.name == self.var_name {
284 self.out.push((use_var.span, DocumentHighlightKind::READ));
285 }
286 }
287 ControlFlow::Continue(())
288 }
289 ExprKind::ArrowFunction(_) => walk_expr(self, expr),
291 _ => walk_expr(self, expr),
292 }
293 }
294}
295
296pub fn collect_var_refs_in_scope(
301 stmts: &[Stmt<'_, '_>],
302 var_name: &str,
303 byte_off: usize,
304 out: &mut Vec<(Span, DocumentHighlightKind)>,
305) {
306 for stmt in stmts {
307 if collect_in_fn_at(stmt, var_name, byte_off, out) {
308 return;
309 }
310 }
311 var_refs_in_stmts(stmts, var_name, out);
313}
314
315fn collect_in_fn_at(
318 stmt: &Stmt<'_, '_>,
319 var_name: &str,
320 byte_off: usize,
321 out: &mut Vec<(Span, DocumentHighlightKind)>,
322) -> bool {
323 match &stmt.kind {
324 StmtKind::Function(f) => {
325 if byte_off < stmt.span.start as usize || byte_off >= stmt.span.end as usize {
326 return false;
327 }
328 for inner in f.body.iter() {
330 if collect_in_fn_at(inner, var_name, byte_off, out) {
331 return true;
332 }
333 }
334 for p in f.params.iter() {
336 if p.name == var_name {
337 out.push((p.span, DocumentHighlightKind::WRITE));
338 }
339 }
340 var_refs_in_stmts(&f.body, var_name, out);
341 true
342 }
343 StmtKind::Class(c) => {
344 for member in c.members.iter() {
345 if let ClassMemberKind::Method(m) = &member.kind {
346 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
347 {
348 continue;
349 }
350 if let Some(body) = &m.body {
351 for inner in body.iter() {
352 if collect_in_fn_at(inner, var_name, byte_off, out) {
353 return true;
354 }
355 }
356 var_refs_in_stmts(body, var_name, out);
357 }
358 for p in m.params.iter() {
359 if p.name == var_name {
360 out.push((p.span, DocumentHighlightKind::WRITE));
361 }
362 }
363 return true;
364 }
365 }
366 false
367 }
368 StmtKind::Trait(t) => {
369 for member in t.members.iter() {
370 if let ClassMemberKind::Method(m) = &member.kind {
371 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
372 {
373 continue;
374 }
375 if let Some(body) = &m.body {
376 for inner in body.iter() {
377 if collect_in_fn_at(inner, var_name, byte_off, out) {
378 return true;
379 }
380 }
381 var_refs_in_stmts(body, var_name, out);
382 }
383 for p in m.params.iter() {
384 if p.name == var_name {
385 out.push((p.span, DocumentHighlightKind::WRITE));
386 }
387 }
388 return true;
389 }
390 }
391 false
392 }
393 StmtKind::Enum(e) => {
394 for member in e.members.iter() {
395 if let EnumMemberKind::Method(m) = &member.kind {
396 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
397 {
398 continue;
399 }
400 if let Some(body) = &m.body {
401 for inner in body.iter() {
402 if collect_in_fn_at(inner, var_name, byte_off, out) {
403 return true;
404 }
405 }
406 for p in m.params.iter() {
407 if p.name == var_name {
408 out.push((p.span, DocumentHighlightKind::WRITE));
409 }
410 }
411 var_refs_in_stmts(body, var_name, out);
412 }
413 return true;
414 }
415 }
416 false
417 }
418 StmtKind::Interface(i) => {
419 for member in i.members.iter() {
420 if let ClassMemberKind::Method(m) = &member.kind {
421 if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
422 {
423 continue;
424 }
425 if let Some(body) = &m.body {
426 for inner in body.iter() {
427 if collect_in_fn_at(inner, var_name, byte_off, out) {
428 return true;
429 }
430 }
431 var_refs_in_stmts(body, var_name, out);
432 }
433 for p in m.params.iter() {
434 if p.name == var_name {
435 out.push((p.span, DocumentHighlightKind::WRITE));
436 }
437 }
438 return true;
439 }
440 }
441 false
442 }
443 StmtKind::Namespace(ns) => {
444 if let NamespaceBody::Braced(inner) = &ns.body {
445 for s in inner.iter() {
446 if collect_in_fn_at(s, var_name, byte_off, out) {
447 return true;
448 }
449 }
450 }
451 false
452 }
453 _ => false,
454 }
455}
456
457pub fn property_refs_in_stmts(
462 source: &str,
463 stmts: &[Stmt<'_, '_>],
464 prop_name: &str,
465 out: &mut Vec<Span>,
466) {
467 let mut v = PropertyRefsVisitor {
468 source,
469 prop_name,
470 out: Vec::new(),
471 };
472 for stmt in stmts {
473 let _ = v.visit_stmt(stmt);
474 }
475 out.append(&mut v.out);
476}
477
478struct PropertyRefsVisitor<'a> {
479 source: &'a str,
480 prop_name: &'a str,
481 out: Vec<Span>,
482}
483
484impl<'arena, 'src> Visitor<'arena, 'src> for PropertyRefsVisitor<'_> {
485 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
486 match &expr.kind {
487 ExprKind::PropertyAccess(p) | ExprKind::NullsafePropertyAccess(p) => {
488 let span = p.property.span;
489 let name_in_src = self
490 .source
491 .get(span.start as usize..span.end as usize)
492 .unwrap_or("");
493 if name_in_src == self.prop_name {
494 self.out.push(span);
495 }
496 }
497 _ => {}
498 }
499 walk_expr(self, expr)
500 }
501
502 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
503 match &member.kind {
504 ClassMemberKind::Property(p) if p.name == self.prop_name => {
505 let offset = str_offset(self.source, &p.name.to_string()).unwrap_or(0);
506 self.out.push(Span {
507 start: offset,
508 end: offset + p.name.to_string().len() as u32,
509 });
510 }
511 ClassMemberKind::Method(m) if m.name == "__construct" => {
513 for p in m.params.iter() {
514 if p.visibility.is_some() && p.name == self.prop_name {
515 let offset = str_offset(self.source, &p.name.to_string()).unwrap_or(0);
516 self.out.push(Span {
517 start: offset,
518 end: offset + p.name.to_string().len() as u32,
519 });
520 }
521 }
522 }
523 _ => {}
524 }
525 walk_class_member(self, member)
526 }
527}
528
529pub fn function_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
535 let mut v = FunctionRefsVisitor {
536 name,
537 out: Vec::new(),
538 };
539 for stmt in stmts {
540 let _ = v.visit_stmt(stmt);
541 }
542 out.append(&mut v.out);
543}
544
545struct FunctionRefsVisitor<'a> {
546 name: &'a str,
547 out: Vec<Span>,
548}
549
550impl<'arena, 'src> Visitor<'arena, 'src> for FunctionRefsVisitor<'_> {
551 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
552 if let ExprKind::FunctionCall(f) = &expr.kind
553 && let ExprKind::Identifier(id) = &f.name.kind
554 && id.as_str() == self.name
555 {
556 self.out.push(f.name.span);
557 }
558 walk_expr(self, expr)
559 }
560}
561
562pub fn method_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
567 let mut v = MethodRefsVisitor {
568 name,
569 out: Vec::new(),
570 };
571 for stmt in stmts {
572 let _ = v.visit_stmt(stmt);
573 }
574 out.append(&mut v.out);
575}
576
577struct MethodRefsVisitor<'a> {
578 name: &'a str,
579 out: Vec<Span>,
580}
581
582impl<'arena, 'src> Visitor<'arena, 'src> for MethodRefsVisitor<'_> {
583 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
584 match &expr.kind {
585 ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
586 if let ExprKind::Identifier(id) = &m.method.kind
587 && id.as_str() == self.name
588 {
589 self.out.push(m.method.span);
590 }
591 }
592 ExprKind::StaticMethodCall(s) if s.method.name_str() == Some(self.name) => {
593 self.out.push(s.method.span);
594 }
595 _ => {}
596 }
597 walk_expr(self, expr)
598 }
599}
600
601pub fn new_refs_in_stmts(
610 stmts: &[Stmt<'_, '_>],
611 class_name: &str,
612 class_fqn: Option<&str>,
613 out: &mut Vec<Span>,
614) {
615 let mut v = NewRefsVisitor {
616 class_name,
617 class_fqn,
618 out: Vec::new(),
619 };
620 for stmt in stmts {
621 let _ = v.visit_stmt(stmt);
622 }
623 out.append(&mut v.out);
624}
625
626struct NewRefsVisitor<'a> {
627 class_name: &'a str,
628 class_fqn: Option<&'a str>,
629 out: Vec<Span>,
630}
631
632impl<'arena, 'src> Visitor<'arena, 'src> for NewRefsVisitor<'_> {
633 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
634 if let ExprKind::New(n) = &expr.kind
635 && let ExprKind::Identifier(id) = &n.class.kind
636 {
637 let matches = if id.contains('\\')
638 && let Some(fqn) = self.class_fqn
639 {
640 id.trim_start_matches('\\') == fqn.trim_start_matches('\\')
642 } else {
643 id.rsplit('\\').next().unwrap_or(id) == self.class_name
644 };
645 if matches {
646 self.out.push(n.class.span);
647 }
648 }
649 walk_expr(self, expr)
650 }
651}
652
653pub fn fqn_new_class_refs_in_stmts(stmts: &[Stmt<'_, '_>]) -> Vec<String> {
658 let mut v = FqnNewRefsVisitor { out: Vec::new() };
659 for stmt in stmts {
660 let _ = v.visit_stmt(stmt);
661 }
662 v.out.sort_unstable();
663 v.out.dedup();
664 v.out
665}
666
667struct FqnNewRefsVisitor {
668 out: Vec<String>,
669}
670
671impl<'arena, 'src> Visitor<'arena, 'src> for FqnNewRefsVisitor {
672 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
673 if let ExprKind::New(n) = &expr.kind
674 && let ExprKind::Identifier(id) = &n.class.kind
675 && id.starts_with('\\')
676 {
677 self.out.push(id.trim_start_matches('\\').to_string());
678 }
679 walk_expr(self, expr)
680 }
681}
682
683pub fn class_refs_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str, out: &mut Vec<Span>) {
687 let mut v = ClassRefsVisitor {
688 class_name,
689 out: Vec::new(),
690 };
691 for stmt in stmts {
692 let _ = v.visit_stmt(stmt);
693 }
694 out.append(&mut v.out);
695}
696
697struct ClassRefsVisitor<'a> {
698 class_name: &'a str,
699 out: Vec<Span>,
700}
701
702impl ClassRefsVisitor<'_> {
703 fn collect_name<'a, 'b>(&mut self, name: &Name<'a, 'b>) {
705 let repr = name.to_string_repr();
706 let last = repr.rsplit('\\').next().unwrap_or(repr.as_ref());
707 if last == self.class_name {
708 let span = name.span();
709 let offset = (repr.len() - last.len()) as u32;
710 self.out.push(Span {
711 start: span.start + offset,
712 end: span.end,
713 });
714 }
715 }
716}
717
718impl<'arena, 'src> Visitor<'arena, 'src> for ClassRefsVisitor<'_> {
719 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
720 match &stmt.kind {
721 StmtKind::Class(c) => {
722 if let Some(ext) = &c.extends {
723 self.collect_name(ext);
724 }
725 for iface in c.implements.iter() {
726 self.collect_name(iface);
727 }
728 }
729 StmtKind::Interface(i) => {
730 for parent in i.extends.iter() {
731 self.collect_name(parent);
732 }
733 }
734 _ => {}
735 }
736 walk_stmt(self, stmt)
737 }
738
739 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
740 match &expr.kind {
741 ExprKind::New(n) => {
742 if let ExprKind::Identifier(id) = &n.class.kind
743 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
744 {
745 self.out.push(n.class.span);
746 }
747 }
748 ExprKind::Binary(b) => {
749 if let ExprKind::Identifier(id) = &b.right.kind
750 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
751 {
752 self.out.push(b.right.span);
753 }
754 }
755 ExprKind::StaticMethodCall(s) => {
756 if let ExprKind::Identifier(id) = &s.class.kind
757 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
758 {
759 self.out.push(s.class.span);
760 }
761 }
762 ExprKind::StaticPropertyAccess(s) => {
763 if let ExprKind::Identifier(id) = &s.class.kind
764 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
765 {
766 self.out.push(s.class.span);
767 }
768 }
769 ExprKind::ClassConstAccess(c) => {
770 if let ExprKind::Identifier(id) = &c.class.kind
771 && id.rsplit('\\').next().unwrap_or(id) == self.class_name
772 {
773 self.out.push(c.class.span);
774 }
775 }
776 _ => {}
777 }
778 walk_expr(self, expr)
779 }
780
781 fn visit_type_hint(&mut self, type_hint: &TypeHint<'arena, 'src>) -> ControlFlow<()> {
782 if let TypeHintKind::Named(name) = &type_hint.kind {
783 self.collect_name(name);
784 }
785 walk_type_hint(self, type_hint)
786 }
787
788 fn visit_catch_clause(&mut self, catch: &CatchClause<'arena, 'src>) -> ControlFlow<()> {
789 for ty in catch.types.iter() {
790 self.collect_name(ty);
791 }
792 walk_catch_clause(self, catch)
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799 use crate::ast::ParsedDoc;
800
801 fn spans_to_strs<'a>(source: &'a str, spans: &[Span]) -> Vec<&'a str> {
803 spans
804 .iter()
805 .map(|s| &source[s.start as usize..s.end as usize])
806 .collect()
807 }
808
809 fn parse(src: &str) -> ParsedDoc {
810 ParsedDoc::parse(src.to_string())
811 }
812
813 #[test]
816 fn refs_finds_function_declaration_and_call() {
817 let src = "<?php\nfunction greet() {}\ngreet();";
818 let doc = parse(src);
819 let mut out = vec![];
820 refs_in_stmts(src, &doc.program().stmts, "greet", &mut out);
821 let texts = spans_to_strs(src, &out);
822 assert!(texts.contains(&"greet"), "expected function decl name");
823 assert_eq!(texts.iter().filter(|&&t| t == "greet").count(), 2);
824 }
825
826 #[test]
827 fn refs_finds_class_declaration_and_new() {
828 let src = "<?php\nclass Foo {}\n$x = new Foo();";
829 let doc = parse(src);
830 let mut out = vec![];
831 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
832 let texts = spans_to_strs(src, &out);
833 assert!(texts.iter().all(|&t| t == "Foo"));
834 assert_eq!(texts.len(), 2);
835 }
836
837 #[test]
838 fn refs_finds_method_declaration_inside_class() {
839 let src = "<?php\nclass Bar { function run() { $this->run(); } }";
840 let doc = parse(src);
841 let mut out = vec![];
842 refs_in_stmts(src, &doc.program().stmts, "run", &mut out);
843 let texts = spans_to_strs(src, &out);
844 assert!(texts.iter().any(|&t| t == "run"));
846 }
847
848 #[test]
849 fn refs_returns_empty_for_unknown_name() {
850 let src = "<?php\nfunction greet() {}";
851 let doc = parse(src);
852 let mut out = vec![];
853 refs_in_stmts(src, &doc.program().stmts, "nope", &mut out);
854 assert!(out.is_empty());
855 }
856
857 #[test]
860 fn refs_with_use_includes_use_import() {
861 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
862 let doc = parse(src);
863 let mut out = vec![];
864 refs_in_stmts_with_use(src, &doc.program().stmts, "Foo", &mut out);
865 let texts = spans_to_strs(src, &out);
866 assert!(
868 texts.iter().filter(|&&t| t == "Foo").count() >= 2,
869 "got: {texts:?}"
870 );
871 }
872
873 #[test]
874 fn refs_without_use_misses_use_import() {
875 let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
876 let doc = parse(src);
877 let mut out = vec![];
878 refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
879 let texts = spans_to_strs(src, &out);
880 assert!(
882 texts.iter().filter(|&&t| t == "Foo").count() < 2,
883 "refs_in_stmts should not include use import; got: {texts:?}"
884 );
885 }
886
887 #[test]
890 fn var_refs_finds_variable_in_assignment_and_echo() {
891 let src = "<?php\n$x = 1;\necho $x;";
892 let doc = parse(src);
893 let mut out = vec![];
894 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
895 assert_eq!(out.len(), 2, "expected $x in assignment and echo");
896 }
897
898 #[test]
899 fn var_refs_respects_function_scope_boundary() {
900 let src = "<?php\n$x = 1;\nfunction inner() { $x = 2; }";
902 let doc = parse(src);
903 let mut out = vec![];
904 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
905 assert_eq!(out.len(), 1, "inner $x must not cross scope boundary");
907 }
908
909 #[test]
910 fn var_refs_traverses_if_while_for_foreach() {
911 let src = "<?php\n$x = 0;\nif ($x) { $x++; }\nwhile ($x > 0) { $x--; }\nfor ($x = 0; $x < 3; $x++) {}\nforeach ([$x] as $v) {}";
912 let doc = parse(src);
913 let mut out = vec![];
914 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
915 assert!(
916 out.len() >= 5,
917 "expected multiple $x refs, got {}",
918 out.len()
919 );
920 }
921
922 #[test]
923 fn var_refs_does_not_cross_closure_boundary() {
924 let src = "<?php\n$x = 1;\n$f = function() { $x = 2; };";
925 let doc = parse(src);
926 let mut out = vec![];
927 var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
928 assert_eq!(
930 out.len(),
931 1,
932 "closure $x must not be collected by outer scope walk"
933 );
934 }
935
936 #[test]
939 fn collect_scope_finds_var_inside_function() {
940 let src = "<?php\nfunction foo($x) { return $x + 1; }";
941 let doc = parse(src);
942 let byte_off = src.find("return").unwrap();
944 let mut out = vec![];
945 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
946 assert!(
948 out.len() >= 2,
949 "expected param + body ref, got {}",
950 out.len()
951 );
952 }
953
954 #[test]
955 fn collect_scope_top_level_when_no_function() {
956 let src = "<?php\n$x = 1;\necho $x;";
957 let doc = parse(src);
958 let byte_off = src.find("echo").unwrap();
959 let mut out = vec![];
960 collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
961 assert_eq!(out.len(), 2);
962 }
963
964 #[test]
965 fn collect_scope_finds_var_inside_enum_method() {
966 let src = "<?php\nenum Status {\n public function label($arg) { return $arg; }\n}";
967 let doc = parse(src);
968 let byte_off = src.find("return").unwrap();
969 let mut out = vec![];
970 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
971 assert!(
972 out.len() >= 2,
973 "expected param + body ref in enum method, got {}",
974 out.len()
975 );
976 }
977
978 #[test]
979 fn collect_scope_does_not_bleed_enum_method_into_outer_scope() {
980 let src =
981 "<?php\n$arg = 1;\nenum Status {\n public function label($arg) { return $arg; }\n}";
982 let doc = parse(src);
983 let byte_off = src.find("$arg").unwrap();
985 let mut out = vec![];
986 collect_var_refs_in_scope(&doc.program().stmts, "arg", byte_off, &mut out);
987 assert_eq!(
989 out.len(),
990 1,
991 "enum method $arg must not bleed into outer scope"
992 );
993 }
994
995 #[test]
998 fn property_refs_finds_declaration_and_access() {
999 let src = "<?php\nclass Baz { public int $val = 0; function get() { return $this->val; } }";
1000 let doc = parse(src);
1001 let mut out = vec![];
1002 property_refs_in_stmts(src, &doc.program().stmts, "val", &mut out);
1003 assert_eq!(out.len(), 2, "expected decl + access, got {}", out.len());
1005 }
1006
1007 #[test]
1008 fn property_refs_finds_nullsafe_access() {
1009 let src = "<?php\n$r = $obj?->name;";
1010 let doc = parse(src);
1011 let mut out = vec![];
1012 property_refs_in_stmts(src, &doc.program().stmts, "name", &mut out);
1013 assert_eq!(out.len(), 1);
1014 }
1015
1016 #[test]
1019 fn function_refs_only_matches_free_calls_not_methods() {
1020 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
1021 let doc = parse(src);
1022 let mut out = vec![];
1023 function_refs_in_stmts(&doc.program().stmts, "run", &mut out);
1024 assert_eq!(out.len(), 1, "got: {out:?}");
1026 }
1027
1028 #[test]
1031 fn method_refs_only_matches_method_calls_not_free_functions() {
1032 let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
1033 let doc = parse(src);
1034 let mut out = vec![];
1035 method_refs_in_stmts(&doc.program().stmts, "run", &mut out);
1036 assert_eq!(out.len(), 1, "got: {out:?}");
1038 }
1039
1040 #[test]
1041 fn method_refs_finds_nullsafe_method_call() {
1042 let src = "<?php\n$obj?->process();";
1043 let doc = parse(src);
1044 let mut out = vec![];
1045 method_refs_in_stmts(&doc.program().stmts, "process", &mut out);
1046 assert_eq!(out.len(), 1);
1047 }
1048
1049 #[test]
1052 fn class_refs_finds_new_and_extends() {
1053 let src = "<?php\nclass Child extends Base {}\n$x = new Base();";
1054 let doc = parse(src);
1055 let mut out = vec![];
1056 class_refs_in_stmts(&doc.program().stmts, "Base", &mut out);
1057 assert!(out.len() >= 2, "expected extends + new, got {}", out.len());
1058 }
1059
1060 #[test]
1061 fn class_refs_does_not_match_free_function_with_same_name() {
1062 let src = "<?php\nfunction Foo() {}\nFoo();";
1063 let doc = parse(src);
1064 let mut out = vec![];
1065 class_refs_in_stmts(&doc.program().stmts, "Foo", &mut out);
1066 assert!(
1067 out.is_empty(),
1068 "free function call must not be a class ref; got: {out:?}"
1069 );
1070 }
1071
1072 #[test]
1073 fn class_refs_finds_type_hint_in_function_param() {
1074 let src = "<?php\nfunction take(MyClass $obj): MyClass { return $obj; }";
1075 let doc = parse(src);
1076 let mut out = vec![];
1077 class_refs_in_stmts(&doc.program().stmts, "MyClass", &mut out);
1078 assert_eq!(out.len(), 2, "got {out:?}");
1080 }
1081}