Skip to main content

php_lsp/
walk.rs

1/// Deep AST walker — collects all spans where `word` appears as a name reference
2/// (function calls, `new Foo`, method calls, bare identifiers, static calls).
3use php_ast::{
4    ClassMemberKind, EnumMemberKind, Expr, ExprKind, NamespaceBody, Span, Stmt, StmtKind, TypeHint,
5    TypeHintKind,
6};
7
8use crate::ast::str_offset;
9
10pub fn refs_in_stmts(source: &str, stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
11    for stmt in stmts {
12        refs_in_stmt(source, stmt, word, out);
13    }
14}
15
16/// Like `refs_in_stmts`, but also matches spans inside `use` statements.
17/// Needed so that renaming a class also renames its `use` import.
18pub fn refs_in_stmts_with_use(
19    source: &str,
20    stmts: &[Stmt<'_, '_>],
21    word: &str,
22    out: &mut Vec<Span>,
23) {
24    refs_in_stmts(source, stmts, word, out);
25    use_refs(stmts, word, out);
26}
27
28fn use_refs(stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
29    for stmt in stmts {
30        match &stmt.kind {
31            StmtKind::Use(u) => {
32                for use_item in u.uses.iter() {
33                    let fqn = use_item.name.to_string_repr().into_owned();
34                    let alias_match = use_item.alias.map(|a| a == word).unwrap_or(false);
35                    let last_seg = fqn.rsplit('\\').next().unwrap_or(&fqn);
36                    if alias_match || last_seg == word {
37                        let name_span = use_item.name.span();
38                        let offset = (fqn.len() - last_seg.len()) as u32;
39                        let syn_span = Span {
40                            start: name_span.start + offset,
41                            end: name_span.start + fqn.len() as u32,
42                        };
43                        out.push(syn_span);
44                    }
45                }
46            }
47            StmtKind::Namespace(ns) => {
48                if let NamespaceBody::Braced(inner) = &ns.body {
49                    use_refs(inner, word, out);
50                }
51            }
52            _ => {}
53        }
54    }
55}
56
57// ── RefVisitor trait + shared walker ──────────────────────────────────────────
58
59trait RefVisitor {
60    fn visit_expr(&self, expr: &Expr<'_, '_>, out: &mut Vec<Span>);
61    /// Return true if handled (skip shared control-flow dispatch).
62    fn visit_stmt(&self, stmt: &Stmt<'_, '_>, out: &mut Vec<Span>) -> bool;
63}
64
65fn walk_refs(v: &impl RefVisitor, stmts: &[Stmt<'_, '_>], out: &mut Vec<Span>) {
66    for stmt in stmts {
67        walk_ref_stmt(v, stmt, out);
68    }
69}
70
71fn walk_ref_stmt(v: &impl RefVisitor, stmt: &Stmt<'_, '_>, out: &mut Vec<Span>) {
72    if v.visit_stmt(stmt, out) {
73        return;
74    }
75    match &stmt.kind {
76        StmtKind::Expression(e) => v.visit_expr(e, out),
77        StmtKind::Return(Some(e)) => v.visit_expr(e, out),
78        StmtKind::Echo(exprs) => {
79            for expr in exprs.iter() {
80                v.visit_expr(expr, out);
81            }
82        }
83        StmtKind::If(i) => {
84            v.visit_expr(&i.condition, out);
85            walk_ref_stmt(v, i.then_branch, out);
86            for ei in i.elseif_branches.iter() {
87                v.visit_expr(&ei.condition, out);
88                walk_ref_stmt(v, &ei.body, out);
89            }
90            if let Some(e) = &i.else_branch {
91                walk_ref_stmt(v, e, out);
92            }
93        }
94        StmtKind::While(w) => {
95            v.visit_expr(&w.condition, out);
96            walk_ref_stmt(v, w.body, out);
97        }
98        StmtKind::DoWhile(d) => {
99            walk_ref_stmt(v, d.body, out);
100            v.visit_expr(&d.condition, out);
101        }
102        StmtKind::Foreach(f) => {
103            v.visit_expr(&f.expr, out);
104            walk_ref_stmt(v, f.body, out);
105        }
106        StmtKind::For(f) => {
107            for e in f.init.iter() {
108                v.visit_expr(e, out);
109            }
110            for cond in f.condition.iter() {
111                v.visit_expr(cond, out);
112            }
113            for e in f.update.iter() {
114                v.visit_expr(e, out);
115            }
116            walk_ref_stmt(v, f.body, out);
117        }
118        StmtKind::TryCatch(t) => {
119            walk_refs(v, &t.body, out);
120            for catch in t.catches.iter() {
121                walk_refs(v, &catch.body, out);
122            }
123            if let Some(finally) = &t.finally {
124                walk_refs(v, finally, out);
125            }
126        }
127        StmtKind::Block(stmts) => walk_refs(v, stmts, out),
128        _ => {}
129    }
130}
131
132// ── AllRefsVisitor ───────────────────────────────────────────────────────────
133
134struct AllRefsVisitor<'a> {
135    source: &'a str,
136    word: &'a str,
137}
138
139impl RefVisitor for AllRefsVisitor<'_> {
140    fn visit_expr(&self, expr: &Expr<'_, '_>, out: &mut Vec<Span>) {
141        refs_in_expr(self.source, expr, self.word, out);
142    }
143
144    fn visit_stmt(&self, stmt: &Stmt<'_, '_>, out: &mut Vec<Span>) -> bool {
145        match &stmt.kind {
146            StmtKind::Function(f) => {
147                if f.name == self.word {
148                    let start = str_offset(self.source, f.name);
149                    out.push(Span {
150                        start,
151                        end: start + f.name.len() as u32,
152                    });
153                }
154                walk_refs(self, &f.body, out);
155                true
156            }
157            StmtKind::Class(c) => {
158                if let Some(name) = c.name
159                    && name == self.word
160                {
161                    let start = str_offset(self.source, name);
162                    out.push(Span {
163                        start,
164                        end: start + name.len() as u32,
165                    });
166                }
167                for member in c.members.iter() {
168                    match &member.kind {
169                        ClassMemberKind::Method(m) => {
170                            if m.name == self.word {
171                                let start = str_offset(self.source, m.name);
172                                out.push(Span {
173                                    start,
174                                    end: start + m.name.len() as u32,
175                                });
176                            }
177                            if let Some(body) = &m.body {
178                                walk_refs(self, body, out);
179                            }
180                        }
181                        ClassMemberKind::Property(p) => {
182                            if let Some(default) = &p.default {
183                                refs_in_expr(self.source, default, self.word, out);
184                            }
185                        }
186                        _ => {}
187                    }
188                }
189                true
190            }
191            StmtKind::Interface(i) => {
192                if i.name == self.word {
193                    let start = str_offset(self.source, i.name);
194                    out.push(Span {
195                        start,
196                        end: start + i.name.len() as u32,
197                    });
198                }
199                for member in i.members.iter() {
200                    if let ClassMemberKind::Method(m) = &member.kind
201                        && m.name == self.word
202                    {
203                        let start = str_offset(self.source, m.name);
204                        out.push(Span {
205                            start,
206                            end: start + m.name.len() as u32,
207                        });
208                    }
209                }
210                true
211            }
212            StmtKind::Trait(t) => {
213                if t.name == self.word {
214                    let start = str_offset(self.source, t.name);
215                    out.push(Span {
216                        start,
217                        end: start + t.name.len() as u32,
218                    });
219                }
220                for member in t.members.iter() {
221                    match &member.kind {
222                        ClassMemberKind::Method(m) => {
223                            if m.name == self.word {
224                                let start = str_offset(self.source, m.name);
225                                out.push(Span {
226                                    start,
227                                    end: start + m.name.len() as u32,
228                                });
229                            }
230                            if let Some(body) = &m.body {
231                                walk_refs(self, body, out);
232                            }
233                        }
234                        ClassMemberKind::Property(p) => {
235                            if let Some(default) = &p.default {
236                                refs_in_expr(self.source, default, self.word, out);
237                            }
238                        }
239                        _ => {}
240                    }
241                }
242                true
243            }
244            StmtKind::Enum(e) => {
245                if e.name == self.word {
246                    let start = str_offset(self.source, e.name);
247                    out.push(Span {
248                        start,
249                        end: start + e.name.len() as u32,
250                    });
251                }
252                for member in e.members.iter() {
253                    match &member.kind {
254                        EnumMemberKind::Method(m) => {
255                            if m.name == self.word {
256                                let start = str_offset(self.source, m.name);
257                                out.push(Span {
258                                    start,
259                                    end: start + m.name.len() as u32,
260                                });
261                            }
262                            if let Some(body) = &m.body {
263                                walk_refs(self, body, out);
264                            }
265                        }
266                        EnumMemberKind::Case(c) => {
267                            if let Some(value) = &c.value {
268                                refs_in_expr(self.source, value, self.word, out);
269                            }
270                        }
271                        _ => {}
272                    }
273                }
274                true
275            }
276            StmtKind::Namespace(ns) => {
277                if let NamespaceBody::Braced(inner) = &ns.body {
278                    walk_refs(self, inner, out);
279                }
280                true
281            }
282            StmtKind::StaticVar(vars) => {
283                for var in vars.iter() {
284                    if let Some(v) = &var.default {
285                        refs_in_expr(self.source, v, self.word, out);
286                    }
287                }
288                true
289            }
290            _ => false,
291        }
292    }
293}
294
295fn refs_in_stmt(source: &str, stmt: &Stmt<'_, '_>, word: &str, out: &mut Vec<Span>) {
296    let v = AllRefsVisitor { source, word };
297    walk_ref_stmt(&v, stmt, out);
298}
299
300// ── Variable rename helpers ───────────────────────────────────────────────────
301
302/// Collect all spans where `$var_name` (the variable name WITHOUT `$`) appears as an
303/// ExprKind::Variable within `stmts`. Stops at nested function/closure/arrow-function
304/// scope boundaries so that `$x` in an inner function is not conflated with `$x` in
305/// the outer function.
306pub fn var_refs_in_stmts(stmts: &[Stmt<'_, '_>], var_name: &str, out: &mut Vec<Span>) {
307    for stmt in stmts {
308        var_refs_in_stmt(stmt, var_name, out);
309    }
310}
311
312fn var_refs_in_stmt(stmt: &Stmt<'_, '_>, var_name: &str, out: &mut Vec<Span>) {
313    match &stmt.kind {
314        // Scope boundaries — do NOT recurse into these
315        StmtKind::Function(_) | StmtKind::Class(_) | StmtKind::Trait(_) | StmtKind::Enum(_) => {}
316        StmtKind::Expression(e) => var_refs_in_expr(e, var_name, out),
317        StmtKind::Return(Some(e)) => var_refs_in_expr(e, var_name, out),
318        StmtKind::Return(None) | StmtKind::Break(_) | StmtKind::Continue(_) => {}
319        StmtKind::Echo(exprs) => {
320            for e in exprs.iter() {
321                var_refs_in_expr(e, var_name, out);
322            }
323        }
324        StmtKind::If(i) => {
325            var_refs_in_expr(&i.condition, var_name, out);
326            var_refs_in_stmt(i.then_branch, var_name, out);
327            for ei in i.elseif_branches.iter() {
328                var_refs_in_expr(&ei.condition, var_name, out);
329                var_refs_in_stmt(&ei.body, var_name, out);
330            }
331            if let Some(e) = &i.else_branch {
332                var_refs_in_stmt(e, var_name, out);
333            }
334        }
335        StmtKind::While(w) => {
336            var_refs_in_expr(&w.condition, var_name, out);
337            var_refs_in_stmt(w.body, var_name, out);
338        }
339        StmtKind::DoWhile(d) => {
340            var_refs_in_stmt(d.body, var_name, out);
341            var_refs_in_expr(&d.condition, var_name, out);
342        }
343        StmtKind::Foreach(f) => {
344            var_refs_in_expr(&f.expr, var_name, out);
345            if let Some(k) = &f.key {
346                var_refs_in_expr(k, var_name, out);
347            }
348            var_refs_in_expr(&f.value, var_name, out);
349            var_refs_in_stmt(f.body, var_name, out);
350        }
351        StmtKind::For(f) => {
352            for e in f.init.iter() {
353                var_refs_in_expr(e, var_name, out);
354            }
355            for cond in f.condition.iter() {
356                var_refs_in_expr(cond, var_name, out);
357            }
358            for e in f.update.iter() {
359                var_refs_in_expr(e, var_name, out);
360            }
361            var_refs_in_stmt(f.body, var_name, out);
362        }
363        StmtKind::TryCatch(t) => {
364            var_refs_in_stmts(&t.body, var_name, out);
365            for catch in t.catches.iter() {
366                var_refs_in_stmts(&catch.body, var_name, out);
367            }
368            if let Some(finally) = &t.finally {
369                var_refs_in_stmts(finally, var_name, out);
370            }
371        }
372        StmtKind::Block(inner) => var_refs_in_stmts(inner, var_name, out),
373        StmtKind::StaticVar(vars) => {
374            for v in vars.iter() {
375                if let Some(def) = &v.default {
376                    var_refs_in_expr(def, var_name, out);
377                }
378            }
379        }
380        StmtKind::Namespace(ns) => {
381            if let NamespaceBody::Braced(inner) = &ns.body {
382                var_refs_in_stmts(inner, var_name, out);
383            }
384        }
385        _ => {}
386    }
387}
388
389fn var_refs_in_expr(expr: &Expr<'_, '_>, var_name: &str, out: &mut Vec<Span>) {
390    match &expr.kind {
391        ExprKind::Variable(name) => {
392            if name.as_str() == var_name {
393                out.push(expr.span);
394            }
395        }
396        ExprKind::Assign(a) => {
397            var_refs_in_expr(a.target, var_name, out);
398            var_refs_in_expr(a.value, var_name, out);
399        }
400        ExprKind::Binary(b) => {
401            var_refs_in_expr(b.left, var_name, out);
402            var_refs_in_expr(b.right, var_name, out);
403        }
404        ExprKind::UnaryPrefix(u) => var_refs_in_expr(u.operand, var_name, out),
405        ExprKind::UnaryPostfix(u) => var_refs_in_expr(u.operand, var_name, out),
406        ExprKind::Ternary(t) => {
407            var_refs_in_expr(t.condition, var_name, out);
408            if let Some(then_expr) = t.then_expr {
409                var_refs_in_expr(then_expr, var_name, out);
410            }
411            var_refs_in_expr(t.else_expr, var_name, out);
412        }
413        ExprKind::NullCoalesce(n) => {
414            var_refs_in_expr(n.left, var_name, out);
415            var_refs_in_expr(n.right, var_name, out);
416        }
417        ExprKind::Parenthesized(e) => var_refs_in_expr(e, var_name, out),
418        ExprKind::ErrorSuppress(e) => var_refs_in_expr(e, var_name, out),
419        ExprKind::Cast(_, e) => var_refs_in_expr(e, var_name, out),
420        ExprKind::Clone(e) => var_refs_in_expr(e, var_name, out),
421        ExprKind::ThrowExpr(e) => var_refs_in_expr(e, var_name, out),
422        ExprKind::Print(e) => var_refs_in_expr(e, var_name, out),
423        ExprKind::Empty(e) => var_refs_in_expr(e, var_name, out),
424        ExprKind::Eval(e) => var_refs_in_expr(e, var_name, out),
425        ExprKind::FunctionCall(f) => {
426            var_refs_in_expr(f.name, var_name, out);
427            for a in f.args.iter() {
428                var_refs_in_expr(&a.value, var_name, out);
429            }
430        }
431        ExprKind::MethodCall(m) => {
432            var_refs_in_expr(m.object, var_name, out);
433            for a in m.args.iter() {
434                var_refs_in_expr(&a.value, var_name, out);
435            }
436        }
437        ExprKind::NullsafeMethodCall(m) => {
438            var_refs_in_expr(m.object, var_name, out);
439            for a in m.args.iter() {
440                var_refs_in_expr(&a.value, var_name, out);
441            }
442        }
443        ExprKind::StaticMethodCall(s) => {
444            var_refs_in_expr(s.class, var_name, out);
445            for a in s.args.iter() {
446                var_refs_in_expr(&a.value, var_name, out);
447            }
448        }
449        ExprKind::New(n) => {
450            var_refs_in_expr(n.class, var_name, out);
451            for a in n.args.iter() {
452                var_refs_in_expr(&a.value, var_name, out);
453            }
454        }
455        ExprKind::ArrayAccess(a) => {
456            var_refs_in_expr(a.array, var_name, out);
457            if let Some(idx) = a.index {
458                var_refs_in_expr(idx, var_name, out);
459            }
460        }
461        ExprKind::PropertyAccess(p) => var_refs_in_expr(p.object, var_name, out),
462        ExprKind::NullsafePropertyAccess(p) => var_refs_in_expr(p.object, var_name, out),
463        ExprKind::StaticPropertyAccess(s) => var_refs_in_expr(s.class, var_name, out),
464        ExprKind::Yield(y) => {
465            if let Some(k) = y.key {
466                var_refs_in_expr(k, var_name, out);
467            }
468            if let Some(v) = y.value {
469                var_refs_in_expr(v, var_name, out);
470            }
471        }
472        ExprKind::Match(m) => {
473            var_refs_in_expr(m.subject, var_name, out);
474            for arm in m.arms.iter() {
475                if let Some(conds) = &arm.conditions {
476                    for c in conds.iter() {
477                        var_refs_in_expr(c, var_name, out);
478                    }
479                }
480                var_refs_in_expr(&arm.body, var_name, out);
481            }
482        }
483        ExprKind::Array(elems) => {
484            for el in elems.iter() {
485                if let Some(k) = &el.key {
486                    var_refs_in_expr(k, var_name, out);
487                }
488                var_refs_in_expr(&el.value, var_name, out);
489            }
490        }
491        ExprKind::Isset(exprs) => {
492            for e in exprs.iter() {
493                var_refs_in_expr(e, var_name, out);
494            }
495        }
496        ExprKind::Include(_, e) => var_refs_in_expr(e, var_name, out),
497        ExprKind::Exit(Some(e)) => var_refs_in_expr(e, var_name, out),
498        // Scope boundaries within expressions — do NOT recurse into these
499        ExprKind::Closure(_) | ExprKind::ArrowFunction(_) => {}
500        _ => {}
501    }
502}
503
504/// Collect all `$var_name` spans within the innermost function/method scope
505/// that contains `byte_off`. If `byte_off` is not inside any function, collects
506/// from the top-level stmts (respecting scope boundaries). Also collects the
507/// parameter declaration span when the variable is a parameter of the scope.
508pub fn collect_var_refs_in_scope(
509    stmts: &[Stmt<'_, '_>],
510    var_name: &str,
511    byte_off: usize,
512    out: &mut Vec<Span>,
513) {
514    for stmt in stmts {
515        if collect_in_fn_at(stmt, var_name, byte_off, out) {
516            return;
517        }
518    }
519    // Not inside any function — collect top-level
520    var_refs_in_stmts(stmts, var_name, out);
521}
522
523/// Returns `true` if `stmt` is (or contains) the function/method that owns `byte_off`
524/// and has populated `out` with variable + param spans for `var_name`.
525fn collect_in_fn_at(
526    stmt: &Stmt<'_, '_>,
527    var_name: &str,
528    byte_off: usize,
529    out: &mut Vec<Span>,
530) -> bool {
531    match &stmt.kind {
532        StmtKind::Function(f) => {
533            if byte_off < stmt.span.start as usize || byte_off >= stmt.span.end as usize {
534                return false;
535            }
536            // Check nested functions first.
537            for inner in f.body.iter() {
538                if collect_in_fn_at(inner, var_name, byte_off, out) {
539                    return true;
540                }
541            }
542            // This is the enclosing function — collect param + body refs.
543            for p in f.params.iter() {
544                if p.name == var_name {
545                    out.push(p.span);
546                }
547            }
548            var_refs_in_stmts(&f.body, var_name, out);
549            true
550        }
551        StmtKind::Class(c) => {
552            for member in c.members.iter() {
553                if let ClassMemberKind::Method(m) = &member.kind {
554                    if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
555                    {
556                        continue;
557                    }
558                    if let Some(body) = &m.body {
559                        for inner in body.iter() {
560                            if collect_in_fn_at(inner, var_name, byte_off, out) {
561                                return true;
562                            }
563                        }
564                        for p in m.params.iter() {
565                            if p.name == var_name {
566                                out.push(p.span);
567                            }
568                        }
569                        var_refs_in_stmts(body, var_name, out);
570                    }
571                    return true;
572                }
573            }
574            false
575        }
576        StmtKind::Trait(t) => {
577            for member in t.members.iter() {
578                if let ClassMemberKind::Method(m) = &member.kind {
579                    if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
580                    {
581                        continue;
582                    }
583                    if let Some(body) = &m.body {
584                        for inner in body.iter() {
585                            if collect_in_fn_at(inner, var_name, byte_off, out) {
586                                return true;
587                            }
588                        }
589                        for p in m.params.iter() {
590                            if p.name == var_name {
591                                out.push(p.span);
592                            }
593                        }
594                        var_refs_in_stmts(body, var_name, out);
595                    }
596                    return true;
597                }
598            }
599            false
600        }
601        StmtKind::Namespace(ns) => {
602            if let NamespaceBody::Braced(inner) = &ns.body {
603                for s in inner.iter() {
604                    if collect_in_fn_at(s, var_name, byte_off, out) {
605                        return true;
606                    }
607                }
608            }
609            false
610        }
611        _ => false,
612    }
613}
614
615// ── Property rename helpers ───────────────────────────────────────────────────
616
617/// Collect all spans where `prop_name` is accessed (`->prop`, `?->prop`) or
618/// declared as a class/trait property, across all statements.
619/// Because `PropertyAccess.property` is a `&'src str` sub-slice of `source`,
620/// we use `str_offset` (pointer arithmetic) to obtain its byte offset.
621pub fn property_refs_in_stmts(
622    source: &str,
623    stmts: &[Stmt<'_, '_>],
624    prop_name: &str,
625    out: &mut Vec<Span>,
626) {
627    for stmt in stmts {
628        property_refs_in_stmt(source, stmt, prop_name, out);
629    }
630}
631
632fn property_refs_in_stmt(source: &str, stmt: &Stmt<'_, '_>, prop_name: &str, out: &mut Vec<Span>) {
633    match &stmt.kind {
634        StmtKind::Expression(e) => property_refs_in_expr(source, e, prop_name, out),
635        StmtKind::Return(Some(e)) => property_refs_in_expr(source, e, prop_name, out),
636        StmtKind::Echo(exprs) => {
637            for e in exprs.iter() {
638                property_refs_in_expr(source, e, prop_name, out);
639            }
640        }
641        StmtKind::Function(f) => {
642            property_refs_in_stmts(source, &f.body, prop_name, out);
643        }
644        StmtKind::Class(c) => {
645            for member in c.members.iter() {
646                match &member.kind {
647                    ClassMemberKind::Property(p) if p.name == prop_name => {
648                        let offset = str_offset(source, p.name);
649                        out.push(Span {
650                            start: offset,
651                            end: offset + p.name.len() as u32,
652                        });
653                    }
654                    ClassMemberKind::Method(m) => {
655                        if let Some(body) = &m.body {
656                            property_refs_in_stmts(source, body, prop_name, out);
657                        }
658                    }
659                    _ => {}
660                }
661            }
662        }
663        StmtKind::Trait(t) => {
664            for member in t.members.iter() {
665                match &member.kind {
666                    ClassMemberKind::Property(p) if p.name == prop_name => {
667                        let offset = str_offset(source, p.name);
668                        out.push(Span {
669                            start: offset,
670                            end: offset + p.name.len() as u32,
671                        });
672                    }
673                    ClassMemberKind::Method(m) => {
674                        if let Some(body) = &m.body {
675                            property_refs_in_stmts(source, body, prop_name, out);
676                        }
677                    }
678                    _ => {}
679                }
680            }
681        }
682        StmtKind::Enum(e) => {
683            for member in e.members.iter() {
684                if let EnumMemberKind::Method(m) = &member.kind
685                    && let Some(body) = &m.body
686                {
687                    property_refs_in_stmts(source, body, prop_name, out);
688                }
689            }
690        }
691        StmtKind::Namespace(ns) => {
692            if let NamespaceBody::Braced(inner) = &ns.body {
693                property_refs_in_stmts(source, inner, prop_name, out);
694            }
695        }
696        StmtKind::If(i) => {
697            property_refs_in_expr(source, &i.condition, prop_name, out);
698            property_refs_in_stmt(source, i.then_branch, prop_name, out);
699            for ei in i.elseif_branches.iter() {
700                property_refs_in_expr(source, &ei.condition, prop_name, out);
701                property_refs_in_stmt(source, &ei.body, prop_name, out);
702            }
703            if let Some(e) = &i.else_branch {
704                property_refs_in_stmt(source, e, prop_name, out);
705            }
706        }
707        StmtKind::While(w) => {
708            property_refs_in_expr(source, &w.condition, prop_name, out);
709            property_refs_in_stmt(source, w.body, prop_name, out);
710        }
711        StmtKind::DoWhile(d) => {
712            property_refs_in_stmt(source, d.body, prop_name, out);
713            property_refs_in_expr(source, &d.condition, prop_name, out);
714        }
715        StmtKind::Foreach(f) => {
716            property_refs_in_expr(source, &f.expr, prop_name, out);
717            property_refs_in_stmt(source, f.body, prop_name, out);
718        }
719        StmtKind::For(f) => {
720            for e in f.init.iter() {
721                property_refs_in_expr(source, e, prop_name, out);
722            }
723            for cond in f.condition.iter() {
724                property_refs_in_expr(source, cond, prop_name, out);
725            }
726            for e in f.update.iter() {
727                property_refs_in_expr(source, e, prop_name, out);
728            }
729            property_refs_in_stmt(source, f.body, prop_name, out);
730        }
731        StmtKind::TryCatch(t) => {
732            property_refs_in_stmts(source, &t.body, prop_name, out);
733            for catch in t.catches.iter() {
734                property_refs_in_stmts(source, &catch.body, prop_name, out);
735            }
736            if let Some(finally) = &t.finally {
737                property_refs_in_stmts(source, finally, prop_name, out);
738            }
739        }
740        StmtKind::Block(inner) => property_refs_in_stmts(source, inner, prop_name, out),
741        _ => {}
742    }
743}
744
745fn property_refs_in_expr(source: &str, expr: &Expr<'_, '_>, prop_name: &str, out: &mut Vec<Span>) {
746    match &expr.kind {
747        ExprKind::PropertyAccess(p) => {
748            property_refs_in_expr(source, p.object, prop_name, out);
749            let span = p.property.span;
750            let name_in_src = source
751                .get(span.start as usize..span.end as usize)
752                .unwrap_or("");
753            if name_in_src == prop_name {
754                out.push(span);
755            }
756        }
757        ExprKind::NullsafePropertyAccess(p) => {
758            property_refs_in_expr(source, p.object, prop_name, out);
759            let span = p.property.span;
760            let name_in_src = source
761                .get(span.start as usize..span.end as usize)
762                .unwrap_or("");
763            if name_in_src == prop_name {
764                out.push(span);
765            }
766        }
767        ExprKind::Assign(a) => {
768            property_refs_in_expr(source, a.target, prop_name, out);
769            property_refs_in_expr(source, a.value, prop_name, out);
770        }
771        ExprKind::Binary(b) => {
772            property_refs_in_expr(source, b.left, prop_name, out);
773            property_refs_in_expr(source, b.right, prop_name, out);
774        }
775        ExprKind::UnaryPrefix(u) => property_refs_in_expr(source, u.operand, prop_name, out),
776        ExprKind::UnaryPostfix(u) => property_refs_in_expr(source, u.operand, prop_name, out),
777        ExprKind::Ternary(t) => {
778            property_refs_in_expr(source, t.condition, prop_name, out);
779            if let Some(then_expr) = t.then_expr {
780                property_refs_in_expr(source, then_expr, prop_name, out);
781            }
782            property_refs_in_expr(source, t.else_expr, prop_name, out);
783        }
784        ExprKind::NullCoalesce(n) => {
785            property_refs_in_expr(source, n.left, prop_name, out);
786            property_refs_in_expr(source, n.right, prop_name, out);
787        }
788        ExprKind::Parenthesized(e) => property_refs_in_expr(source, e, prop_name, out),
789        ExprKind::ErrorSuppress(e) => property_refs_in_expr(source, e, prop_name, out),
790        ExprKind::Cast(_, e) => property_refs_in_expr(source, e, prop_name, out),
791        ExprKind::Clone(e) => property_refs_in_expr(source, e, prop_name, out),
792        ExprKind::ThrowExpr(e) => property_refs_in_expr(source, e, prop_name, out),
793        ExprKind::Print(e) => property_refs_in_expr(source, e, prop_name, out),
794        ExprKind::Empty(e) => property_refs_in_expr(source, e, prop_name, out),
795        ExprKind::Eval(e) => property_refs_in_expr(source, e, prop_name, out),
796        ExprKind::FunctionCall(f) => {
797            property_refs_in_expr(source, f.name, prop_name, out);
798            for a in f.args.iter() {
799                property_refs_in_expr(source, &a.value, prop_name, out);
800            }
801        }
802        ExprKind::MethodCall(m) => {
803            property_refs_in_expr(source, m.object, prop_name, out);
804            for a in m.args.iter() {
805                property_refs_in_expr(source, &a.value, prop_name, out);
806            }
807        }
808        ExprKind::NullsafeMethodCall(m) => {
809            property_refs_in_expr(source, m.object, prop_name, out);
810            for a in m.args.iter() {
811                property_refs_in_expr(source, &a.value, prop_name, out);
812            }
813        }
814        ExprKind::StaticMethodCall(s) => {
815            property_refs_in_expr(source, s.class, prop_name, out);
816            for a in s.args.iter() {
817                property_refs_in_expr(source, &a.value, prop_name, out);
818            }
819        }
820        ExprKind::New(n) => {
821            property_refs_in_expr(source, n.class, prop_name, out);
822            for a in n.args.iter() {
823                property_refs_in_expr(source, &a.value, prop_name, out);
824            }
825        }
826        ExprKind::ArrayAccess(a) => {
827            property_refs_in_expr(source, a.array, prop_name, out);
828            if let Some(idx) = a.index {
829                property_refs_in_expr(source, idx, prop_name, out);
830            }
831        }
832        ExprKind::Yield(y) => {
833            if let Some(k) = y.key {
834                property_refs_in_expr(source, k, prop_name, out);
835            }
836            if let Some(v) = y.value {
837                property_refs_in_expr(source, v, prop_name, out);
838            }
839        }
840        ExprKind::Match(m) => {
841            property_refs_in_expr(source, m.subject, prop_name, out);
842            for arm in m.arms.iter() {
843                if let Some(conds) = &arm.conditions {
844                    for c in conds.iter() {
845                        property_refs_in_expr(source, c, prop_name, out);
846                    }
847                }
848                property_refs_in_expr(source, &arm.body, prop_name, out);
849            }
850        }
851        ExprKind::Array(elems) => {
852            for el in elems.iter() {
853                if let Some(k) = &el.key {
854                    property_refs_in_expr(source, k, prop_name, out);
855                }
856                property_refs_in_expr(source, &el.value, prop_name, out);
857            }
858        }
859        ExprKind::Isset(exprs) => {
860            for e in exprs.iter() {
861                property_refs_in_expr(source, e, prop_name, out);
862            }
863        }
864        ExprKind::Include(_, e) => property_refs_in_expr(source, e, prop_name, out),
865        ExprKind::Exit(Some(e)) => property_refs_in_expr(source, e, prop_name, out),
866        ExprKind::Closure(c) => property_refs_in_stmts(source, &c.body, prop_name, out),
867        ExprKind::ArrowFunction(a) => property_refs_in_expr(source, a.body, prop_name, out),
868        _ => {}
869    }
870}
871
872fn args(source: &str, arg_list: &[php_ast::Arg<'_, '_>], word: &str, out: &mut Vec<Span>) {
873    for a in arg_list.iter() {
874        refs_in_expr(source, &a.value, word, out);
875    }
876}
877
878pub fn refs_in_expr(source: &str, expr: &Expr<'_, '_>, word: &str, out: &mut Vec<Span>) {
879    match &expr.kind {
880        ExprKind::Identifier(name) => {
881            if name.as_str() == word {
882                out.push(expr.span);
883            }
884        }
885        ExprKind::FunctionCall(f) => {
886            refs_in_expr(source, f.name, word, out);
887            args(source, &f.args, word, out);
888        }
889        ExprKind::MethodCall(m) => {
890            refs_in_expr(source, m.object, word, out);
891            refs_in_expr(source, m.method, word, out);
892            args(source, &m.args, word, out);
893        }
894        ExprKind::NullsafeMethodCall(m) => {
895            refs_in_expr(source, m.object, word, out);
896            refs_in_expr(source, m.method, word, out);
897            args(source, &m.args, word, out);
898        }
899        ExprKind::StaticMethodCall(s) => {
900            refs_in_expr(source, s.class, word, out);
901            if s.method.as_ref() == word {
902                out.push(expr.span);
903            }
904            args(source, &s.args, word, out);
905        }
906        ExprKind::New(n) => {
907            refs_in_expr(source, n.class, word, out);
908            args(source, &n.args, word, out);
909        }
910        ExprKind::Assign(a) => {
911            refs_in_expr(source, a.target, word, out);
912            refs_in_expr(source, a.value, word, out);
913        }
914        ExprKind::Binary(b) => {
915            refs_in_expr(source, b.left, word, out);
916            refs_in_expr(source, b.right, word, out);
917        }
918        ExprKind::UnaryPrefix(u) => refs_in_expr(source, u.operand, word, out),
919        ExprKind::UnaryPostfix(u) => refs_in_expr(source, u.operand, word, out),
920        ExprKind::Ternary(t) => {
921            refs_in_expr(source, t.condition, word, out);
922            if let Some(then_expr) = t.then_expr {
923                refs_in_expr(source, then_expr, word, out);
924            }
925            refs_in_expr(source, t.else_expr, word, out);
926        }
927        ExprKind::NullCoalesce(n) => {
928            refs_in_expr(source, n.left, word, out);
929            refs_in_expr(source, n.right, word, out);
930        }
931        ExprKind::Parenthesized(e) => refs_in_expr(source, e, word, out),
932        ExprKind::ErrorSuppress(e) => refs_in_expr(source, e, word, out),
933        ExprKind::Cast(_, e) => refs_in_expr(source, e, word, out),
934        ExprKind::Clone(e) => refs_in_expr(source, e, word, out),
935        ExprKind::ThrowExpr(e) => refs_in_expr(source, e, word, out),
936        ExprKind::Print(e) => refs_in_expr(source, e, word, out),
937        ExprKind::Empty(e) => refs_in_expr(source, e, word, out),
938        ExprKind::Eval(e) => refs_in_expr(source, e, word, out),
939        ExprKind::Yield(y) => {
940            if let Some(k) = y.key {
941                refs_in_expr(source, k, word, out);
942            }
943            if let Some(v) = y.value {
944                refs_in_expr(source, v, word, out);
945            }
946        }
947        ExprKind::ArrayAccess(a) => {
948            refs_in_expr(source, a.array, word, out);
949            if let Some(idx) = a.index {
950                refs_in_expr(source, idx, word, out);
951            }
952        }
953        ExprKind::PropertyAccess(p) => refs_in_expr(source, p.object, word, out),
954        ExprKind::NullsafePropertyAccess(p) => refs_in_expr(source, p.object, word, out),
955        ExprKind::StaticPropertyAccess(s) => refs_in_expr(source, s.class, word, out),
956        ExprKind::ClassConstAccess(c) => {
957            refs_in_expr(source, c.class, word, out);
958            if c.member.as_ref() == word {
959                out.push(expr.span);
960            }
961        }
962        ExprKind::Closure(c) => refs_in_stmts(source, &c.body, word, out),
963        ExprKind::ArrowFunction(a) => refs_in_expr(source, a.body, word, out),
964        ExprKind::Match(m) => {
965            refs_in_expr(source, m.subject, word, out);
966            for arm in m.arms.iter() {
967                if let Some(conds) = &arm.conditions {
968                    for cond in conds.iter() {
969                        refs_in_expr(source, cond, word, out);
970                    }
971                }
972                refs_in_expr(source, &arm.body, word, out);
973            }
974        }
975        ExprKind::Array(elements) => {
976            for elem in elements.iter() {
977                if let Some(key) = &elem.key {
978                    refs_in_expr(source, key, word, out);
979                }
980                refs_in_expr(source, &elem.value, word, out);
981            }
982        }
983        ExprKind::Isset(exprs) => {
984            for e in exprs.iter() {
985                refs_in_expr(source, e, word, out);
986            }
987        }
988        ExprKind::Include(_, e) => refs_in_expr(source, e, word, out),
989        ExprKind::Exit(Some(e)) => refs_in_expr(source, e, word, out),
990        ExprKind::AnonymousClass(c) => {
991            for member in c.members.iter() {
992                if let ClassMemberKind::Method(m) = &member.kind
993                    && let Some(body) = &m.body
994                {
995                    refs_in_stmts(source, body, word, out);
996                }
997            }
998        }
999        _ => {}
1000    }
1001}
1002
1003// ── Semantic (context-aware) reference walkers ────────────────────────────────
1004
1005/// Collect spans where `name` is called as a free function (not a method).
1006/// Only matches `name(...)` calls where the callee is a bare identifier, not
1007/// `$obj->name()` or `Class::name()`.
1008pub fn function_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
1009    let v = FunctionRefsVisitor { name };
1010    walk_refs(&v, stmts, out);
1011}
1012
1013struct FunctionRefsVisitor<'a> {
1014    name: &'a str,
1015}
1016
1017impl RefVisitor for FunctionRefsVisitor<'_> {
1018    fn visit_expr(&self, expr: &Expr<'_, '_>, out: &mut Vec<Span>) {
1019        function_refs_in_expr(expr, self.name, out);
1020    }
1021
1022    fn visit_stmt(&self, stmt: &Stmt<'_, '_>, out: &mut Vec<Span>) -> bool {
1023        match &stmt.kind {
1024            StmtKind::Function(f) => {
1025                walk_refs(self, &f.body, out);
1026                true
1027            }
1028            StmtKind::Class(c) => {
1029                for member in c.members.iter() {
1030                    match &member.kind {
1031                        ClassMemberKind::Method(m) => {
1032                            if let Some(body) = &m.body {
1033                                walk_refs(self, body, out);
1034                            }
1035                        }
1036                        ClassMemberKind::Property(p) => {
1037                            if let Some(default) = &p.default {
1038                                function_refs_in_expr(default, self.name, out);
1039                            }
1040                        }
1041                        _ => {}
1042                    }
1043                }
1044                true
1045            }
1046            StmtKind::Trait(t) => {
1047                for member in t.members.iter() {
1048                    if let ClassMemberKind::Method(m) = &member.kind
1049                        && let Some(body) = &m.body
1050                    {
1051                        walk_refs(self, body, out);
1052                    }
1053                }
1054                true
1055            }
1056            StmtKind::Enum(e) => {
1057                for member in e.members.iter() {
1058                    if let EnumMemberKind::Method(m) = &member.kind
1059                        && let Some(body) = &m.body
1060                    {
1061                        walk_refs(self, body, out);
1062                    }
1063                }
1064                true
1065            }
1066            StmtKind::Namespace(ns) => {
1067                if let NamespaceBody::Braced(inner) = &ns.body {
1068                    walk_refs(self, inner, out);
1069                }
1070                true
1071            }
1072            _ => false,
1073        }
1074    }
1075}
1076
1077fn function_refs_in_expr(expr: &Expr<'_, '_>, name: &str, out: &mut Vec<Span>) {
1078    match &expr.kind {
1079        // The core match: a free function call whose callee is a bare identifier.
1080        ExprKind::FunctionCall(f) => {
1081            if let ExprKind::Identifier(id) = &f.name.kind
1082                && id.as_str() == name
1083            {
1084                out.push(f.name.span);
1085            }
1086            // Still recurse into args and a dynamic callee.
1087            function_refs_in_expr(f.name, name, out);
1088            for a in f.args.iter() {
1089                function_refs_in_expr(&a.value, name, out);
1090            }
1091        }
1092        // Recurse into all expression sub-nodes (but skip method/static call names).
1093        ExprKind::MethodCall(m) => {
1094            function_refs_in_expr(m.object, name, out);
1095            // do NOT check m.method — that is a method name, not a function name
1096            for a in m.args.iter() {
1097                function_refs_in_expr(&a.value, name, out);
1098            }
1099        }
1100        ExprKind::NullsafeMethodCall(m) => {
1101            function_refs_in_expr(m.object, name, out);
1102            for a in m.args.iter() {
1103                function_refs_in_expr(&a.value, name, out);
1104            }
1105        }
1106        ExprKind::StaticMethodCall(s) => {
1107            function_refs_in_expr(s.class, name, out);
1108            for a in s.args.iter() {
1109                function_refs_in_expr(&a.value, name, out);
1110            }
1111        }
1112        ExprKind::New(n) => {
1113            for a in n.args.iter() {
1114                function_refs_in_expr(&a.value, name, out);
1115            }
1116        }
1117        ExprKind::Assign(a) => {
1118            function_refs_in_expr(a.target, name, out);
1119            function_refs_in_expr(a.value, name, out);
1120        }
1121        ExprKind::Binary(b) => {
1122            function_refs_in_expr(b.left, name, out);
1123            function_refs_in_expr(b.right, name, out);
1124        }
1125        ExprKind::UnaryPrefix(u) => function_refs_in_expr(u.operand, name, out),
1126        ExprKind::UnaryPostfix(u) => function_refs_in_expr(u.operand, name, out),
1127        ExprKind::Ternary(t) => {
1128            function_refs_in_expr(t.condition, name, out);
1129            if let Some(e) = t.then_expr {
1130                function_refs_in_expr(e, name, out);
1131            }
1132            function_refs_in_expr(t.else_expr, name, out);
1133        }
1134        ExprKind::NullCoalesce(n) => {
1135            function_refs_in_expr(n.left, name, out);
1136            function_refs_in_expr(n.right, name, out);
1137        }
1138        ExprKind::Parenthesized(e) => function_refs_in_expr(e, name, out),
1139        ExprKind::ErrorSuppress(e) => function_refs_in_expr(e, name, out),
1140        ExprKind::Cast(_, e) => function_refs_in_expr(e, name, out),
1141        ExprKind::Clone(e) => function_refs_in_expr(e, name, out),
1142        ExprKind::ThrowExpr(e) => function_refs_in_expr(e, name, out),
1143        ExprKind::Print(e) => function_refs_in_expr(e, name, out),
1144        ExprKind::Empty(e) => function_refs_in_expr(e, name, out),
1145        ExprKind::Eval(e) => function_refs_in_expr(e, name, out),
1146        ExprKind::Yield(y) => {
1147            if let Some(k) = y.key {
1148                function_refs_in_expr(k, name, out);
1149            }
1150            if let Some(v) = y.value {
1151                function_refs_in_expr(v, name, out);
1152            }
1153        }
1154        ExprKind::ArrayAccess(a) => {
1155            function_refs_in_expr(a.array, name, out);
1156            if let Some(idx) = a.index {
1157                function_refs_in_expr(idx, name, out);
1158            }
1159        }
1160        ExprKind::PropertyAccess(p) => function_refs_in_expr(p.object, name, out),
1161        ExprKind::NullsafePropertyAccess(p) => function_refs_in_expr(p.object, name, out),
1162        ExprKind::StaticPropertyAccess(s) => function_refs_in_expr(s.class, name, out),
1163        ExprKind::Match(m) => {
1164            function_refs_in_expr(m.subject, name, out);
1165            for arm in m.arms.iter() {
1166                if let Some(conds) = &arm.conditions {
1167                    for c in conds.iter() {
1168                        function_refs_in_expr(c, name, out);
1169                    }
1170                }
1171                function_refs_in_expr(&arm.body, name, out);
1172            }
1173        }
1174        ExprKind::Array(elements) => {
1175            for elem in elements.iter() {
1176                if let Some(key) = &elem.key {
1177                    function_refs_in_expr(key, name, out);
1178                }
1179                function_refs_in_expr(&elem.value, name, out);
1180            }
1181        }
1182        ExprKind::Isset(exprs) => {
1183            for e in exprs.iter() {
1184                function_refs_in_expr(e, name, out);
1185            }
1186        }
1187        ExprKind::Include(_, e) => function_refs_in_expr(e, name, out),
1188        ExprKind::Exit(Some(e)) => function_refs_in_expr(e, name, out),
1189        ExprKind::Closure(c) => function_refs_in_stmts(&c.body, name, out),
1190        ExprKind::ArrowFunction(a) => function_refs_in_expr(a.body, name, out),
1191        ExprKind::AnonymousClass(c) => {
1192            for member in c.members.iter() {
1193                if let ClassMemberKind::Method(m) = &member.kind
1194                    && let Some(body) = &m.body
1195                {
1196                    function_refs_in_stmts(body, name, out);
1197                }
1198            }
1199        }
1200        _ => {}
1201    }
1202}
1203
1204/// Collect spans where `name` is used as a method: `->name()`, `?->name()`, `::name()`.
1205/// Does NOT match free function calls or class-name identifiers.
1206pub fn method_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
1207    let v = MethodRefsVisitor { name };
1208    walk_refs(&v, stmts, out);
1209}
1210
1211struct MethodRefsVisitor<'a> {
1212    name: &'a str,
1213}
1214
1215impl RefVisitor for MethodRefsVisitor<'_> {
1216    fn visit_expr(&self, expr: &Expr<'_, '_>, out: &mut Vec<Span>) {
1217        method_refs_in_expr(expr, self.name, out);
1218    }
1219
1220    fn visit_stmt(&self, stmt: &Stmt<'_, '_>, out: &mut Vec<Span>) -> bool {
1221        match &stmt.kind {
1222            StmtKind::Function(f) => {
1223                walk_refs(self, &f.body, out);
1224                true
1225            }
1226            StmtKind::Class(c) => {
1227                for member in c.members.iter() {
1228                    if let ClassMemberKind::Method(m) = &member.kind
1229                        && let Some(body) = &m.body
1230                    {
1231                        walk_refs(self, body, out);
1232                    }
1233                }
1234                true
1235            }
1236            StmtKind::Trait(t) => {
1237                for member in t.members.iter() {
1238                    if let ClassMemberKind::Method(m) = &member.kind
1239                        && let Some(body) = &m.body
1240                    {
1241                        walk_refs(self, body, out);
1242                    }
1243                }
1244                true
1245            }
1246            StmtKind::Enum(e) => {
1247                for member in e.members.iter() {
1248                    if let EnumMemberKind::Method(m) = &member.kind
1249                        && let Some(body) = &m.body
1250                    {
1251                        walk_refs(self, body, out);
1252                    }
1253                }
1254                true
1255            }
1256            StmtKind::Namespace(ns) => {
1257                if let NamespaceBody::Braced(inner) = &ns.body {
1258                    walk_refs(self, inner, out);
1259                }
1260                true
1261            }
1262            _ => false,
1263        }
1264    }
1265}
1266
1267fn method_refs_in_expr(expr: &Expr<'_, '_>, name: &str, out: &mut Vec<Span>) {
1268    match &expr.kind {
1269        ExprKind::MethodCall(m) => {
1270            method_refs_in_expr(m.object, name, out);
1271            // Collect the method name span if it matches.
1272            if let ExprKind::Identifier(id) = &m.method.kind
1273                && id.as_str() == name
1274            {
1275                out.push(m.method.span);
1276            }
1277            for a in m.args.iter() {
1278                method_refs_in_expr(&a.value, name, out);
1279            }
1280        }
1281        ExprKind::NullsafeMethodCall(m) => {
1282            method_refs_in_expr(m.object, name, out);
1283            if let ExprKind::Identifier(id) = &m.method.kind
1284                && id.as_str() == name
1285            {
1286                out.push(m.method.span);
1287            }
1288            for a in m.args.iter() {
1289                method_refs_in_expr(&a.value, name, out);
1290            }
1291        }
1292        ExprKind::StaticMethodCall(s) => {
1293            method_refs_in_expr(s.class, name, out);
1294            if s.method.as_ref() == name {
1295                // For static calls, the span covers the whole expression; we need the
1296                // method-name portion. Use the existing refs_in_expr behaviour which
1297                // pushed expr.span for static methods — replicate that here.
1298                out.push(expr.span);
1299            }
1300            for a in s.args.iter() {
1301                method_refs_in_expr(&a.value, name, out);
1302            }
1303        }
1304        ExprKind::FunctionCall(f) => {
1305            method_refs_in_expr(f.name, name, out);
1306            for a in f.args.iter() {
1307                method_refs_in_expr(&a.value, name, out);
1308            }
1309        }
1310        ExprKind::New(n) => {
1311            for a in n.args.iter() {
1312                method_refs_in_expr(&a.value, name, out);
1313            }
1314        }
1315        ExprKind::Assign(a) => {
1316            method_refs_in_expr(a.target, name, out);
1317            method_refs_in_expr(a.value, name, out);
1318        }
1319        ExprKind::Binary(b) => {
1320            method_refs_in_expr(b.left, name, out);
1321            method_refs_in_expr(b.right, name, out);
1322        }
1323        ExprKind::UnaryPrefix(u) => method_refs_in_expr(u.operand, name, out),
1324        ExprKind::UnaryPostfix(u) => method_refs_in_expr(u.operand, name, out),
1325        ExprKind::Ternary(t) => {
1326            method_refs_in_expr(t.condition, name, out);
1327            if let Some(e) = t.then_expr {
1328                method_refs_in_expr(e, name, out);
1329            }
1330            method_refs_in_expr(t.else_expr, name, out);
1331        }
1332        ExprKind::NullCoalesce(n) => {
1333            method_refs_in_expr(n.left, name, out);
1334            method_refs_in_expr(n.right, name, out);
1335        }
1336        ExprKind::Parenthesized(e) => method_refs_in_expr(e, name, out),
1337        ExprKind::ErrorSuppress(e) => method_refs_in_expr(e, name, out),
1338        ExprKind::Cast(_, e) => method_refs_in_expr(e, name, out),
1339        ExprKind::Clone(e) => method_refs_in_expr(e, name, out),
1340        ExprKind::ThrowExpr(e) => method_refs_in_expr(e, name, out),
1341        ExprKind::Print(e) => method_refs_in_expr(e, name, out),
1342        ExprKind::Empty(e) => method_refs_in_expr(e, name, out),
1343        ExprKind::Eval(e) => method_refs_in_expr(e, name, out),
1344        ExprKind::Yield(y) => {
1345            if let Some(k) = y.key {
1346                method_refs_in_expr(k, name, out);
1347            }
1348            if let Some(v) = y.value {
1349                method_refs_in_expr(v, name, out);
1350            }
1351        }
1352        ExprKind::ArrayAccess(a) => {
1353            method_refs_in_expr(a.array, name, out);
1354            if let Some(idx) = a.index {
1355                method_refs_in_expr(idx, name, out);
1356            }
1357        }
1358        ExprKind::PropertyAccess(p) => method_refs_in_expr(p.object, name, out),
1359        ExprKind::NullsafePropertyAccess(p) => method_refs_in_expr(p.object, name, out),
1360        ExprKind::StaticPropertyAccess(s) => method_refs_in_expr(s.class, name, out),
1361        ExprKind::Match(m) => {
1362            method_refs_in_expr(m.subject, name, out);
1363            for arm in m.arms.iter() {
1364                if let Some(conds) = &arm.conditions {
1365                    for c in conds.iter() {
1366                        method_refs_in_expr(c, name, out);
1367                    }
1368                }
1369                method_refs_in_expr(&arm.body, name, out);
1370            }
1371        }
1372        ExprKind::Array(elements) => {
1373            for elem in elements.iter() {
1374                if let Some(key) = &elem.key {
1375                    method_refs_in_expr(key, name, out);
1376                }
1377                method_refs_in_expr(&elem.value, name, out);
1378            }
1379        }
1380        ExprKind::Isset(exprs) => {
1381            for e in exprs.iter() {
1382                method_refs_in_expr(e, name, out);
1383            }
1384        }
1385        ExprKind::Include(_, e) => method_refs_in_expr(e, name, out),
1386        ExprKind::Exit(Some(e)) => method_refs_in_expr(e, name, out),
1387        ExprKind::Closure(c) => method_refs_in_stmts(&c.body, name, out),
1388        ExprKind::ArrowFunction(a) => method_refs_in_expr(a.body, name, out),
1389        ExprKind::AnonymousClass(c) => {
1390            for member in c.members.iter() {
1391                if let ClassMemberKind::Method(m) = &member.kind
1392                    && let Some(body) = &m.body
1393                {
1394                    method_refs_in_stmts(body, name, out);
1395                }
1396            }
1397        }
1398        _ => {}
1399    }
1400}
1401
1402/// Collect spans where `class_name` is used as a class-type reference:
1403/// `new ClassName`, `extends ClassName`, `implements ClassName`, type hints,
1404/// and `$x instanceof ClassName`.  Does NOT match free function calls or
1405/// method names with the same spelling.
1406pub fn class_refs_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str, out: &mut Vec<Span>) {
1407    let v = ClassRefsVisitor { class_name };
1408    walk_refs(&v, stmts, out);
1409}
1410
1411struct ClassRefsVisitor<'a> {
1412    class_name: &'a str,
1413}
1414
1415impl RefVisitor for ClassRefsVisitor<'_> {
1416    fn visit_expr(&self, expr: &Expr<'_, '_>, out: &mut Vec<Span>) {
1417        class_refs_in_expr(expr, self.class_name, out);
1418    }
1419
1420    fn visit_stmt(&self, stmt: &Stmt<'_, '_>, out: &mut Vec<Span>) -> bool {
1421        match &stmt.kind {
1422            StmtKind::Function(f) => {
1423                for p in f.params.iter() {
1424                    if let Some(th) = &p.type_hint {
1425                        collect_class_in_type_hint(th, self.class_name, out);
1426                    }
1427                }
1428                if let Some(rt) = &f.return_type {
1429                    collect_class_in_type_hint(rt, self.class_name, out);
1430                }
1431                walk_refs(self, &f.body, out);
1432                true
1433            }
1434            StmtKind::Class(c) => {
1435                // `extends ClassName`
1436                if let Some(ext) = &c.extends {
1437                    let last = ext
1438                        .to_string_repr()
1439                        .rsplit('\\')
1440                        .next()
1441                        .unwrap_or("")
1442                        .to_string();
1443                    if last == self.class_name {
1444                        let span = ext.span();
1445                        let offset = (ext.to_string_repr().len() - last.len()) as u32;
1446                        out.push(Span {
1447                            start: span.start + offset,
1448                            end: span.end,
1449                        });
1450                    }
1451                }
1452                // `implements ClassName, ...`
1453                for iface in c.implements.iter() {
1454                    let last = iface
1455                        .to_string_repr()
1456                        .rsplit('\\')
1457                        .next()
1458                        .unwrap_or("")
1459                        .to_string();
1460                    if last == self.class_name {
1461                        let span = iface.span();
1462                        let offset = (iface.to_string_repr().len() - last.len()) as u32;
1463                        out.push(Span {
1464                            start: span.start + offset,
1465                            end: span.end,
1466                        });
1467                    }
1468                }
1469                for member in c.members.iter() {
1470                    match &member.kind {
1471                        ClassMemberKind::Method(m) => {
1472                            for p in m.params.iter() {
1473                                if let Some(th) = &p.type_hint {
1474                                    collect_class_in_type_hint(th, self.class_name, out);
1475                                }
1476                            }
1477                            if let Some(rt) = &m.return_type {
1478                                collect_class_in_type_hint(rt, self.class_name, out);
1479                            }
1480                            if let Some(body) = &m.body {
1481                                walk_refs(self, body, out);
1482                            }
1483                        }
1484                        ClassMemberKind::Property(p) => {
1485                            if let Some(th) = &p.type_hint {
1486                                collect_class_in_type_hint(th, self.class_name, out);
1487                            }
1488                            if let Some(default) = &p.default {
1489                                class_refs_in_expr(default, self.class_name, out);
1490                            }
1491                        }
1492                        _ => {}
1493                    }
1494                }
1495                true
1496            }
1497            StmtKind::Interface(i) => {
1498                for parent in i.extends.iter() {
1499                    let last = parent
1500                        .to_string_repr()
1501                        .rsplit('\\')
1502                        .next()
1503                        .unwrap_or("")
1504                        .to_string();
1505                    if last == self.class_name {
1506                        let span = parent.span();
1507                        let offset = (parent.to_string_repr().len() - last.len()) as u32;
1508                        out.push(Span {
1509                            start: span.start + offset,
1510                            end: span.end,
1511                        });
1512                    }
1513                }
1514                true
1515            }
1516            StmtKind::Trait(t) => {
1517                for member in t.members.iter() {
1518                    if let ClassMemberKind::Method(m) = &member.kind {
1519                        for p in m.params.iter() {
1520                            if let Some(th) = &p.type_hint {
1521                                collect_class_in_type_hint(th, self.class_name, out);
1522                            }
1523                        }
1524                        if let Some(rt) = &m.return_type {
1525                            collect_class_in_type_hint(rt, self.class_name, out);
1526                        }
1527                        if let Some(body) = &m.body {
1528                            walk_refs(self, body, out);
1529                        }
1530                    }
1531                }
1532                true
1533            }
1534            StmtKind::Enum(e) => {
1535                for member in e.members.iter() {
1536                    if let EnumMemberKind::Method(m) = &member.kind
1537                        && let Some(body) = &m.body
1538                    {
1539                        walk_refs(self, body, out);
1540                    }
1541                }
1542                true
1543            }
1544            StmtKind::Namespace(ns) => {
1545                if let NamespaceBody::Braced(inner) = &ns.body {
1546                    walk_refs(self, inner, out);
1547                }
1548                true
1549            }
1550            StmtKind::TryCatch(t) => {
1551                walk_refs(self, &t.body, out);
1552                for catch in t.catches.iter() {
1553                    for ty in catch.types.iter() {
1554                        let last = ty
1555                            .to_string_repr()
1556                            .rsplit('\\')
1557                            .next()
1558                            .unwrap_or("")
1559                            .to_string();
1560                        if last == self.class_name {
1561                            let span = ty.span();
1562                            let offset = (ty.to_string_repr().len() - last.len()) as u32;
1563                            out.push(Span {
1564                                start: span.start + offset,
1565                                end: span.end,
1566                            });
1567                        }
1568                    }
1569                    walk_refs(self, &catch.body, out);
1570                }
1571                if let Some(finally) = &t.finally {
1572                    walk_refs(self, finally, out);
1573                }
1574                true
1575            }
1576            _ => false,
1577        }
1578    }
1579}
1580
1581fn class_refs_in_expr(expr: &Expr<'_, '_>, class_name: &str, out: &mut Vec<Span>) {
1582    match &expr.kind {
1583        // `new ClassName(...)` — the class name is an Identifier child of New.
1584        ExprKind::New(n) => {
1585            if let ExprKind::Identifier(id) = &n.class.kind
1586                && id.rsplit('\\').next().unwrap_or(id) == class_name
1587            {
1588                out.push(n.class.span);
1589            } else {
1590                // Dynamic `new $var()` etc. — no match but still recurse into args.
1591            }
1592            for a in n.args.iter() {
1593                class_refs_in_expr(&a.value, class_name, out);
1594            }
1595        }
1596        // `$x instanceof ClassName` — right operand is an Identifier.
1597        ExprKind::Binary(b) => {
1598            class_refs_in_expr(b.left, class_name, out);
1599            // For instanceof, the RHS is a class name; for other operators it is not.
1600            // We check the RHS regardless — if it is an Identifier and matches, we
1601            // include it; class names don't normally appear as operands otherwise.
1602            if let ExprKind::Identifier(id) = &b.right.kind
1603                && id.rsplit('\\').next().unwrap_or(id) == class_name
1604            {
1605                out.push(b.right.span);
1606            } else {
1607                class_refs_in_expr(b.right, class_name, out);
1608            }
1609        }
1610        // `ClassName::method()` or `ClassName::$prop` — class side.
1611        ExprKind::StaticMethodCall(s) => {
1612            if let ExprKind::Identifier(id) = &s.class.kind
1613                && id.rsplit('\\').next().unwrap_or(id) == class_name
1614            {
1615                out.push(s.class.span);
1616            }
1617            for a in s.args.iter() {
1618                class_refs_in_expr(&a.value, class_name, out);
1619            }
1620        }
1621        ExprKind::StaticPropertyAccess(s) => {
1622            if let ExprKind::Identifier(id) = &s.class.kind
1623                && id.rsplit('\\').next().unwrap_or(id) == class_name
1624            {
1625                out.push(s.class.span);
1626            }
1627        }
1628        ExprKind::ClassConstAccess(c) => {
1629            if let ExprKind::Identifier(id) = &c.class.kind
1630                && id.rsplit('\\').next().unwrap_or(id) == class_name
1631            {
1632                out.push(c.class.span);
1633            }
1634        }
1635        // Recurse into other expression kinds.
1636        ExprKind::FunctionCall(f) => {
1637            for a in f.args.iter() {
1638                class_refs_in_expr(&a.value, class_name, out);
1639            }
1640        }
1641        ExprKind::MethodCall(m) => {
1642            class_refs_in_expr(m.object, class_name, out);
1643            for a in m.args.iter() {
1644                class_refs_in_expr(&a.value, class_name, out);
1645            }
1646        }
1647        ExprKind::NullsafeMethodCall(m) => {
1648            class_refs_in_expr(m.object, class_name, out);
1649            for a in m.args.iter() {
1650                class_refs_in_expr(&a.value, class_name, out);
1651            }
1652        }
1653        ExprKind::Assign(a) => {
1654            class_refs_in_expr(a.target, class_name, out);
1655            class_refs_in_expr(a.value, class_name, out);
1656        }
1657        ExprKind::UnaryPrefix(u) => class_refs_in_expr(u.operand, class_name, out),
1658        ExprKind::UnaryPostfix(u) => class_refs_in_expr(u.operand, class_name, out),
1659        ExprKind::Ternary(t) => {
1660            class_refs_in_expr(t.condition, class_name, out);
1661            if let Some(e) = t.then_expr {
1662                class_refs_in_expr(e, class_name, out);
1663            }
1664            class_refs_in_expr(t.else_expr, class_name, out);
1665        }
1666        ExprKind::NullCoalesce(n) => {
1667            class_refs_in_expr(n.left, class_name, out);
1668            class_refs_in_expr(n.right, class_name, out);
1669        }
1670        ExprKind::Parenthesized(e) => class_refs_in_expr(e, class_name, out),
1671        ExprKind::ErrorSuppress(e) => class_refs_in_expr(e, class_name, out),
1672        ExprKind::Cast(_, e) => class_refs_in_expr(e, class_name, out),
1673        ExprKind::Clone(e) => class_refs_in_expr(e, class_name, out),
1674        ExprKind::ThrowExpr(e) => class_refs_in_expr(e, class_name, out),
1675        ExprKind::Print(e) => class_refs_in_expr(e, class_name, out),
1676        ExprKind::Empty(e) => class_refs_in_expr(e, class_name, out),
1677        ExprKind::Eval(e) => class_refs_in_expr(e, class_name, out),
1678        ExprKind::Yield(y) => {
1679            if let Some(k) = y.key {
1680                class_refs_in_expr(k, class_name, out);
1681            }
1682            if let Some(v) = y.value {
1683                class_refs_in_expr(v, class_name, out);
1684            }
1685        }
1686        ExprKind::ArrayAccess(a) => {
1687            class_refs_in_expr(a.array, class_name, out);
1688            if let Some(idx) = a.index {
1689                class_refs_in_expr(idx, class_name, out);
1690            }
1691        }
1692        ExprKind::PropertyAccess(p) => class_refs_in_expr(p.object, class_name, out),
1693        ExprKind::NullsafePropertyAccess(p) => class_refs_in_expr(p.object, class_name, out),
1694        ExprKind::Match(m) => {
1695            class_refs_in_expr(m.subject, class_name, out);
1696            for arm in m.arms.iter() {
1697                if let Some(conds) = &arm.conditions {
1698                    for c in conds.iter() {
1699                        class_refs_in_expr(c, class_name, out);
1700                    }
1701                }
1702                class_refs_in_expr(&arm.body, class_name, out);
1703            }
1704        }
1705        ExprKind::Array(elements) => {
1706            for elem in elements.iter() {
1707                if let Some(key) = &elem.key {
1708                    class_refs_in_expr(key, class_name, out);
1709                }
1710                class_refs_in_expr(&elem.value, class_name, out);
1711            }
1712        }
1713        ExprKind::Isset(exprs) => {
1714            for e in exprs.iter() {
1715                class_refs_in_expr(e, class_name, out);
1716            }
1717        }
1718        ExprKind::Include(_, e) => class_refs_in_expr(e, class_name, out),
1719        ExprKind::Exit(Some(e)) => class_refs_in_expr(e, class_name, out),
1720        ExprKind::Closure(c) => class_refs_in_stmts(&c.body, class_name, out),
1721        ExprKind::ArrowFunction(a) => class_refs_in_expr(a.body, class_name, out),
1722        ExprKind::AnonymousClass(c) => {
1723            for member in c.members.iter() {
1724                if let ClassMemberKind::Method(m) = &member.kind
1725                    && let Some(body) = &m.body
1726                {
1727                    class_refs_in_stmts(body, class_name, out);
1728                }
1729            }
1730        }
1731        _ => {}
1732    }
1733}
1734
1735/// Walk a type hint and emit a span for any `Named` component that matches `class_name`.
1736fn collect_class_in_type_hint(th: &TypeHint<'_, '_>, class_name: &str, out: &mut Vec<Span>) {
1737    match &th.kind {
1738        TypeHintKind::Named(name) => {
1739            let repr = name.to_string_repr();
1740            let last = repr.rsplit('\\').next().unwrap_or(repr.as_ref());
1741            if last == class_name {
1742                let span = name.span();
1743                let offset = (repr.len() - last.len()) as u32;
1744                out.push(Span {
1745                    start: span.start + offset,
1746                    end: span.end,
1747                });
1748            }
1749        }
1750        TypeHintKind::Nullable(inner) => collect_class_in_type_hint(inner, class_name, out),
1751        TypeHintKind::Union(types) | TypeHintKind::Intersection(types) => {
1752            for t in types.iter() {
1753                collect_class_in_type_hint(t, class_name, out);
1754            }
1755        }
1756        TypeHintKind::Keyword(_, _) => {}
1757    }
1758}
1759
1760#[cfg(test)]
1761mod tests {
1762    use super::*;
1763    use crate::ast::ParsedDoc;
1764
1765    /// Return all substrings of `source` at the given spans.
1766    fn spans_to_strs<'a>(source: &'a str, spans: &[Span]) -> Vec<&'a str> {
1767        spans
1768            .iter()
1769            .map(|s| &source[s.start as usize..s.end as usize])
1770            .collect()
1771    }
1772
1773    fn parse(src: &str) -> ParsedDoc {
1774        ParsedDoc::parse(src.to_string())
1775    }
1776
1777    // ── refs_in_stmts ────────────────────────────────────────────────────────
1778
1779    #[test]
1780    fn refs_finds_function_declaration_and_call() {
1781        let src = "<?php\nfunction greet() {}\ngreet();";
1782        let doc = parse(src);
1783        let mut out = vec![];
1784        refs_in_stmts(src, &doc.program().stmts, "greet", &mut out);
1785        let texts = spans_to_strs(src, &out);
1786        assert!(texts.contains(&"greet"), "expected function decl name");
1787        assert_eq!(texts.iter().filter(|&&t| t == "greet").count(), 2);
1788    }
1789
1790    #[test]
1791    fn refs_finds_class_declaration_and_new() {
1792        let src = "<?php\nclass Foo {}\n$x = new Foo();";
1793        let doc = parse(src);
1794        let mut out = vec![];
1795        refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
1796        let texts = spans_to_strs(src, &out);
1797        assert!(texts.iter().all(|&t| t == "Foo"));
1798        assert_eq!(texts.len(), 2);
1799    }
1800
1801    #[test]
1802    fn refs_finds_method_declaration_inside_class() {
1803        let src = "<?php\nclass Bar { function run() { $this->run(); } }";
1804        let doc = parse(src);
1805        let mut out = vec![];
1806        refs_in_stmts(src, &doc.program().stmts, "run", &mut out);
1807        let texts = spans_to_strs(src, &out);
1808        // method decl name + method call name both appear
1809        assert!(texts.iter().any(|&t| t == "run"));
1810    }
1811
1812    #[test]
1813    fn refs_returns_empty_for_unknown_name() {
1814        let src = "<?php\nfunction greet() {}";
1815        let doc = parse(src);
1816        let mut out = vec![];
1817        refs_in_stmts(src, &doc.program().stmts, "nope", &mut out);
1818        assert!(out.is_empty());
1819    }
1820
1821    // ── refs_in_stmts_with_use ───────────────────────────────────────────────
1822
1823    #[test]
1824    fn refs_with_use_includes_use_import() {
1825        let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
1826        let doc = parse(src);
1827        let mut out = vec![];
1828        refs_in_stmts_with_use(src, &doc.program().stmts, "Foo", &mut out);
1829        let texts = spans_to_strs(src, &out);
1830        // Should see the `Foo` segment in the use statement + the new Foo()
1831        assert!(
1832            texts.iter().filter(|&&t| t == "Foo").count() >= 2,
1833            "got: {texts:?}"
1834        );
1835    }
1836
1837    #[test]
1838    fn refs_without_use_misses_use_import() {
1839        let src = "<?php\nuse Vendor\\Lib\\Foo;\n$x = new Foo();";
1840        let doc = parse(src);
1841        let mut out = vec![];
1842        refs_in_stmts(src, &doc.program().stmts, "Foo", &mut out);
1843        let texts = spans_to_strs(src, &out);
1844        // refs_in_stmts does NOT walk use statements
1845        assert!(
1846            texts.iter().filter(|&&t| t == "Foo").count() < 2,
1847            "refs_in_stmts should not include use import; got: {texts:?}"
1848        );
1849    }
1850
1851    // ── var_refs_in_stmts ────────────────────────────────────────────────────
1852
1853    #[test]
1854    fn var_refs_finds_variable_in_assignment_and_echo() {
1855        let src = "<?php\n$x = 1;\necho $x;";
1856        let doc = parse(src);
1857        let mut out = vec![];
1858        var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1859        assert_eq!(out.len(), 2, "expected $x in assignment and echo");
1860    }
1861
1862    #[test]
1863    fn var_refs_respects_function_scope_boundary() {
1864        // $x inside the nested function is a separate scope — must not be collected.
1865        let src = "<?php\n$x = 1;\nfunction inner() { $x = 2; }";
1866        let doc = parse(src);
1867        let mut out = vec![];
1868        var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1869        // Only the top-level $x = 1; should be found (function is a scope boundary).
1870        assert_eq!(out.len(), 1, "inner $x must not cross scope boundary");
1871    }
1872
1873    #[test]
1874    fn var_refs_traverses_if_while_for_foreach() {
1875        let src = "<?php\n$x = 0;\nif ($x) { $x++; }\nwhile ($x > 0) { $x--; }\nfor ($x = 0; $x < 3; $x++) {}\nforeach ([$x] as $v) {}";
1876        let doc = parse(src);
1877        let mut out = vec![];
1878        var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1879        assert!(
1880            out.len() >= 5,
1881            "expected multiple $x refs, got {}",
1882            out.len()
1883        );
1884    }
1885
1886    #[test]
1887    fn var_refs_does_not_cross_closure_boundary() {
1888        let src = "<?php\n$x = 1;\n$f = function() { $x = 2; };";
1889        let doc = parse(src);
1890        let mut out = vec![];
1891        var_refs_in_stmts(&doc.program().stmts, "x", &mut out);
1892        // Closure is a scope boundary — inner $x not collected.
1893        assert_eq!(
1894            out.len(),
1895            1,
1896            "closure $x must not be collected by outer scope walk"
1897        );
1898    }
1899
1900    // ── collect_var_refs_in_scope ────────────────────────────────────────────
1901
1902    #[test]
1903    fn collect_scope_finds_var_inside_function() {
1904        let src = "<?php\nfunction foo($x) { return $x + 1; }";
1905        let doc = parse(src);
1906        // byte_off somewhere inside the function body
1907        let byte_off = src.find("return").unwrap();
1908        let mut out = vec![];
1909        collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
1910        // Should find the param span and the $x in return
1911        assert!(
1912            out.len() >= 2,
1913            "expected param + body ref, got {}",
1914            out.len()
1915        );
1916    }
1917
1918    #[test]
1919    fn collect_scope_top_level_when_no_function() {
1920        let src = "<?php\n$x = 1;\necho $x;";
1921        let doc = parse(src);
1922        let byte_off = src.find("echo").unwrap();
1923        let mut out = vec![];
1924        collect_var_refs_in_scope(&doc.program().stmts, "x", byte_off, &mut out);
1925        assert_eq!(out.len(), 2);
1926    }
1927
1928    // ── property_refs_in_stmts ───────────────────────────────────────────────
1929
1930    #[test]
1931    fn property_refs_finds_declaration_and_access() {
1932        let src = "<?php\nclass Baz { public int $val = 0; function get() { return $this->val; } }";
1933        let doc = parse(src);
1934        let mut out = vec![];
1935        property_refs_in_stmts(src, &doc.program().stmts, "val", &mut out);
1936        // property declaration + $this->val access
1937        assert_eq!(out.len(), 2, "expected decl + access, got {}", out.len());
1938    }
1939
1940    #[test]
1941    fn property_refs_finds_nullsafe_access() {
1942        let src = "<?php\n$r = $obj?->name;";
1943        let doc = parse(src);
1944        let mut out = vec![];
1945        property_refs_in_stmts(src, &doc.program().stmts, "name", &mut out);
1946        assert_eq!(out.len(), 1);
1947    }
1948
1949    // ── function_refs_in_stmts ───────────────────────────────────────────────
1950
1951    #[test]
1952    fn function_refs_only_matches_free_calls_not_methods() {
1953        let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
1954        let doc = parse(src);
1955        let mut out = vec![];
1956        function_refs_in_stmts(&doc.program().stmts, "run", &mut out);
1957        // Only the free call `run()` should match; `$obj->run()` must not.
1958        assert_eq!(out.len(), 1, "got: {out:?}");
1959    }
1960
1961    // ── method_refs_in_stmts ─────────────────────────────────────────────────
1962
1963    #[test]
1964    fn method_refs_only_matches_method_calls_not_free_functions() {
1965        let src = "<?php\nfunction run() {}\nrun();\n$obj->run();";
1966        let doc = parse(src);
1967        let mut out = vec![];
1968        method_refs_in_stmts(&doc.program().stmts, "run", &mut out);
1969        // Only `$obj->run()` method name span should match.
1970        assert_eq!(out.len(), 1, "got: {out:?}");
1971    }
1972
1973    #[test]
1974    fn method_refs_finds_nullsafe_method_call() {
1975        let src = "<?php\n$obj?->process();";
1976        let doc = parse(src);
1977        let mut out = vec![];
1978        method_refs_in_stmts(&doc.program().stmts, "process", &mut out);
1979        assert_eq!(out.len(), 1);
1980    }
1981
1982    // ── class_refs_in_stmts ──────────────────────────────────────────────────
1983
1984    #[test]
1985    fn class_refs_finds_new_and_extends() {
1986        let src = "<?php\nclass Child extends Base {}\n$x = new Base();";
1987        let doc = parse(src);
1988        let mut out = vec![];
1989        class_refs_in_stmts(&doc.program().stmts, "Base", &mut out);
1990        assert!(out.len() >= 2, "expected extends + new, got {}", out.len());
1991    }
1992
1993    #[test]
1994    fn class_refs_does_not_match_free_function_with_same_name() {
1995        let src = "<?php\nfunction Foo() {}\nFoo();";
1996        let doc = parse(src);
1997        let mut out = vec![];
1998        class_refs_in_stmts(&doc.program().stmts, "Foo", &mut out);
1999        assert!(
2000            out.is_empty(),
2001            "free function call must not be a class ref; got: {out:?}"
2002        );
2003    }
2004
2005    #[test]
2006    fn class_refs_finds_type_hint_in_function_param() {
2007        let src = "<?php\nfunction take(MyClass $obj): MyClass { return $obj; }";
2008        let doc = parse(src);
2009        let mut out = vec![];
2010        class_refs_in_stmts(&doc.program().stmts, "MyClass", &mut out);
2011        // param type hint + return type hint
2012        assert_eq!(out.len(), 2, "got {out:?}");
2013    }
2014}