Skip to main content

php_lsp/
walk.rs

1/// AST walkers — collect all spans where a name, variable, property, function,
2/// method, or class reference appears in the given statements.
3use 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
17// ── Public entry points ───────────────────────────────────────────────────────
18
19pub 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
23/// Like `refs_in_stmts`, but also matches spans inside `use` statements.
24/// Needed so that renaming a class also renames its `use` import.
25pub 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
53// ── AllRefsVisitor ────────────────────────────────────────────────────────────
54
55struct 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 there's an alias and it matches, emit the alias span (not the FQN)
92                        if alias == self.word {
93                            // Find the position of the alias in the source
94                            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                        // No alias: check if the last segment of FQN matches
103                        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                // Scope the name search to this member's own span — a
125                // global `str_offset` returns the first occurrence in
126                // the file, so when two classes share a method name
127                // both methods would resolve to the same range.
128                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            // For enum members, we don't have a statement span, so we'll search the entire source
151            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
172// ── Variable rename helpers ───────────────────────────────────────────────────
173
174/// Collect all spans where `$var_name` (the variable name WITHOUT `$`) appears as an
175/// `ExprKind::Variable` within `stmts`. Stops at nested function/closure/arrow-function
176/// scope boundaries so that `$x` in an inner function is not conflated with `$x` in
177/// the outer function.
178pub 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        // Stop at scope-defining statement boundaries.
201        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                // foreach key/value are write positions (being assigned)
209                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                // Walk the rest of the foreach
221                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            // Collect matching variable references.
232            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            // Assignment: target is WRITE, value is READ
239            ExprKind::Assign(a) => {
240                // Visit target with WRITE kind
241                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                // Visit value with READ kind (default)
249                let _ = self.visit_expr(a.value);
250                ControlFlow::Continue(())
251            }
252            // Pre/post increment/decrement are both read and write, but mark as WRITE
253            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            // Closures are scope boundaries, but arrow functions auto-capture outer variables.
280            ExprKind::Closure(c) => {
281                // Before stopping, collect variables from the closure's use($x) clause.
282                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            // Arrow functions auto-capture and should be traversed.
290            ExprKind::ArrowFunction(_) => walk_expr(self, expr),
291            _ => walk_expr(self, expr),
292        }
293    }
294}
295
296/// Collect all `$var_name` spans within the innermost function/method scope
297/// that contains `byte_off`. If `byte_off` is not inside any function, collects
298/// from the top-level stmts (respecting scope boundaries). Also collects the
299/// parameter declaration span when the variable is a parameter of the scope.
300pub 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    // Not inside any function — collect top-level
312    var_refs_in_stmts(stmts, var_name, out);
313}
314
315/// Returns `true` if `stmt` is (or contains) the function/method that owns `byte_off`
316/// and has populated `out` with variable + param spans for `var_name`.
317fn 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            // Check nested functions first.
329            for inner in f.body.iter() {
330                if collect_in_fn_at(inner, var_name, byte_off, out) {
331                    return true;
332                }
333            }
334            // This is the enclosing function — collect param + body refs.
335            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
457// ── Property rename helpers ───────────────────────────────────────────────────
458
459/// Collect all spans where `prop_name` is accessed (`->prop`, `?->prop`) or
460/// declared as a class/trait property, across all statements.
461pub 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            // Constructor-promoted parameters act as property declarations.
512            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
529// ── Function-reference walker ─────────────────────────────────────────────────
530
531/// Collect spans where `name` is called as a free function (not a method).
532/// Only matches `name(...)` calls where the callee is a bare identifier, not
533/// `$obj->name()` or `Class::name()`.
534pub 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
562// ── Method-reference walker ───────────────────────────────────────────────────
563
564/// Collect spans where `name` is used as a method: `->name()`, `?->name()`, `::name()`.
565/// Does NOT match free function calls or class-name identifiers.
566pub 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
601// ── Class-reference walker ────────────────────────────────────────────────────
602
603/// Collect spans for `new ClassName(...)` expressions only — excludes type hints,
604/// `instanceof`, `extends`, `implements`, and static calls.
605///
606/// `class_fqn` — when `Some`, FQN-qualified identifiers in the source (those
607/// containing `\`) are compared against the FQN rather than just the short name,
608/// preventing false positives when two classes share a short name across namespaces.
609pub 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                // Fully-qualified identifier: compare by FQN for exact namespace match.
641                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
653/// Collect every fully-qualified class name (i.e. starting with `\`) that
654/// appears as the class argument of a `new` expression in `stmts`.
655/// Returns de-duplicated FQCN strings with the leading `\` stripped, ready to
656/// pass to `session.lazy_load_class`.
657pub 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
683/// `new ClassName`, `extends ClassName`, `implements ClassName`, type hints,
684/// and `$x instanceof ClassName`. Does NOT match free function calls or
685/// method names with the same spelling.
686pub 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    /// Push the span of the last segment of `name` if it matches `class_name`.
704    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    /// Return all substrings of `source` at the given spans.
802    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    // ── refs_in_stmts ────────────────────────────────────────────────────────
814
815    #[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        // method decl name + method call name both appear
845        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    // ── refs_in_stmts_with_use ───────────────────────────────────────────────
858
859    #[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        // Should see the `Foo` segment in the use statement + the new Foo()
867        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        // refs_in_stmts does NOT walk use statements
881        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    // ── var_refs_in_stmts ────────────────────────────────────────────────────
888
889    #[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        // $x inside the nested function is a separate scope — must not be collected.
901        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        // Only the top-level $x = 1; should be found (function is a scope boundary).
906        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        // Closure is a scope boundary — inner $x not collected.
929        assert_eq!(
930            out.len(),
931            1,
932            "closure $x must not be collected by outer scope walk"
933        );
934    }
935
936    // ── collect_var_refs_in_scope ────────────────────────────────────────────
937
938    #[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        // byte_off somewhere inside the function body
943        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        // Should find the param span and the $x in return
947        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        // cursor is at the top-level $arg = 1, outside the enum
984        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        // only the top-level $arg should be found, not the enum method param
988        assert_eq!(
989            out.len(),
990            1,
991            "enum method $arg must not bleed into outer scope"
992        );
993    }
994
995    // ── property_refs_in_stmts ───────────────────────────────────────────────
996
997    #[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        // property declaration + $this->val access
1004        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    // ── function_refs_in_stmts ───────────────────────────────────────────────
1017
1018    #[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        // Only the free call `run()` should match; `$obj->run()` must not.
1025        assert_eq!(out.len(), 1, "got: {out:?}");
1026    }
1027
1028    // ── method_refs_in_stmts ─────────────────────────────────────────────────
1029
1030    #[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        // Only `$obj->run()` method name span should match.
1037        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    // ── class_refs_in_stmts ──────────────────────────────────────────────────
1050
1051    #[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        // param type hint + return type hint
1079        assert_eq!(out.len(), 2, "got {out:?}");
1080    }
1081}