/// Deep AST walker — collects all spans where `word` appears as a name reference
/// (function calls, `new Foo`, method calls, bare identifiers, static calls).
use php_ast::{
    ClassMemberKind, EnumMemberKind, Expr, ExprKind, NamespaceBody, Span, Stmt, StmtKind, TypeHint,
    TypeHintKind,
};

use crate::ast::str_offset;

pub fn refs_in_stmts(source: &str, stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
    for stmt in stmts {
        refs_in_stmt(source, stmt, word, out);
    }
}

/// Like `refs_in_stmts`, but also matches spans inside `use` statements.
/// Needed so that renaming a class also renames its `use` import.
pub fn refs_in_stmts_with_use(
    source: &str,
    stmts: &[Stmt<'_, '_>],
    word: &str,
    out: &mut Vec<Span>,
) {
    refs_in_stmts(source, stmts, word, out);
    use_refs(stmts, word, out);
}

fn use_refs(stmts: &[Stmt<'_, '_>], word: &str, out: &mut Vec<Span>) {
    for stmt in stmts {
        match &stmt.kind {
            StmtKind::Use(u) => {
                for use_item in u.uses.iter() {
                    let fqn = use_item.name.to_string_repr().into_owned();
                    let alias_match = use_item.alias.map(|a| a == word).unwrap_or(false);
                    let last_seg = fqn.rsplit('\\').next().unwrap_or(&fqn);
                    if alias_match || last_seg == word {
                        let name_span = use_item.name.span();
                        let offset = (fqn.len() - last_seg.len()) as u32;
                        let syn_span = Span {
                            start: name_span.start + offset,
                            end: name_span.start + fqn.len() as u32,
                        };
                        out.push(syn_span);
                    }
                }
            }
            StmtKind::Namespace(ns) => {
                if let NamespaceBody::Braced(inner) = &ns.body {
                    use_refs(inner, word, out);
                }
            }
            _ => {}
        }
    }
}

pub fn refs_in_stmt(source: &str, stmt: &Stmt<'_, '_>, word: &str, out: &mut Vec<Span>) {
    match &stmt.kind {
        StmtKind::Expression(e) => refs_in_expr(source, e, word, out),
        StmtKind::Return(Some(v)) => refs_in_expr(source, v, word, out),
        StmtKind::Echo(exprs) => {
            for expr in exprs.iter() {
                refs_in_expr(source, expr, word, out);
            }
        }
        StmtKind::Function(f) => {
            if f.name == word {
                let start = str_offset(source, f.name);
                out.push(Span {
                    start,
                    end: start + f.name.len() as u32,
                });
            }
            refs_in_stmts(source, &f.body, word, out);
        }
        StmtKind::Class(c) => {
            if let Some(name) = c.name
                && name == word
            {
                let start = str_offset(source, name);
                out.push(Span {
                    start,
                    end: start + name.len() as u32,
                });
            }
            for member in c.members.iter() {
                match &member.kind {
                    ClassMemberKind::Method(m) => {
                        if m.name == word {
                            let start = str_offset(source, m.name);
                            out.push(Span {
                                start,
                                end: start + m.name.len() as u32,
                            });
                        }
                        if let Some(body) = &m.body {
                            refs_in_stmts(source, body, word, out);
                        }
                    }
                    ClassMemberKind::Property(p) => {
                        if let Some(default) = &p.default {
                            refs_in_expr(source, default, word, out);
                        }
                    }
                    _ => {}
                }
            }
        }
        StmtKind::Interface(i) => {
            if i.name == word {
                let start = str_offset(source, i.name);
                out.push(Span {
                    start,
                    end: start + i.name.len() as u32,
                });
            }
        }
        StmtKind::Trait(t) => {
            if t.name == word {
                let start = str_offset(source, t.name);
                out.push(Span {
                    start,
                    end: start + t.name.len() as u32,
                });
            }
            for member in t.members.iter() {
                match &member.kind {
                    ClassMemberKind::Method(m) => {
                        if m.name == word {
                            let start = str_offset(source, m.name);
                            out.push(Span {
                                start,
                                end: start + m.name.len() as u32,
                            });
                        }
                        if let Some(body) = &m.body {
                            refs_in_stmts(source, body, word, out);
                        }
                    }
                    ClassMemberKind::Property(p) => {
                        if let Some(default) = &p.default {
                            refs_in_expr(source, default, word, out);
                        }
                    }
                    _ => {}
                }
            }
        }
        StmtKind::Enum(e) => {
            if e.name == word {
                let start = str_offset(source, e.name);
                out.push(Span {
                    start,
                    end: start + e.name.len() as u32,
                });
            }
            for member in e.members.iter() {
                match &member.kind {
                    EnumMemberKind::Method(m) => {
                        if m.name == word {
                            let start = str_offset(source, m.name);
                            out.push(Span {
                                start,
                                end: start + m.name.len() as u32,
                            });
                        }
                        if let Some(body) = &m.body {
                            refs_in_stmts(source, body, word, out);
                        }
                    }
                    EnumMemberKind::Case(c) => {
                        if let Some(value) = &c.value {
                            refs_in_expr(source, value, word, out);
                        }
                    }
                    _ => {}
                }
            }
        }
        StmtKind::Namespace(ns) => {
            if let NamespaceBody::Braced(inner) = &ns.body {
                refs_in_stmts(source, inner, word, out);
            }
        }
        StmtKind::If(i) => {
            refs_in_expr(source, &i.condition, word, out);
            refs_in_stmt(source, i.then_branch, word, out);
            for ei in i.elseif_branches.iter() {
                refs_in_expr(source, &ei.condition, word, out);
                refs_in_stmt(source, &ei.body, word, out);
            }
            if let Some(e) = &i.else_branch {
                refs_in_stmt(source, e, word, out);
            }
        }
        StmtKind::While(w) => {
            refs_in_expr(source, &w.condition, word, out);
            refs_in_stmt(source, w.body, word, out);
        }
        StmtKind::DoWhile(d) => {
            refs_in_stmt(source, d.body, word, out);
            refs_in_expr(source, &d.condition, word, out);
        }
        StmtKind::Foreach(f) => {
            refs_in_expr(source, &f.expr, word, out);
            refs_in_stmt(source, f.body, word, out);
        }
        StmtKind::For(f) => {
            for e in f.init.iter() {
                refs_in_expr(source, e, word, out);
            }
            for cond in f.condition.iter() {
                refs_in_expr(source, cond, word, out);
            }
            for e in f.update.iter() {
                refs_in_expr(source, e, word, out);
            }
            refs_in_stmt(source, f.body, word, out);
        }
        StmtKind::TryCatch(t) => {
            refs_in_stmts(source, &t.body, word, out);
            for catch in t.catches.iter() {
                refs_in_stmts(source, &catch.body, word, out);
            }
            if let Some(finally) = &t.finally {
                refs_in_stmts(source, finally, word, out);
            }
        }
        StmtKind::Block(stmts) => refs_in_stmts(source, stmts, word, out),
        StmtKind::StaticVar(vars) => {
            for var in vars.iter() {
                if let Some(v) = &var.default {
                    refs_in_expr(source, v, word, out);
                }
            }
        }
        _ => {}
    }
}

// ── Variable rename helpers ───────────────────────────────────────────────────

/// Collect all spans where `$var_name` (the variable name WITHOUT `$`) appears as an
/// ExprKind::Variable within `stmts`. Stops at nested function/closure/arrow-function
/// scope boundaries so that `$x` in an inner function is not conflated with `$x` in
/// the outer function.
pub fn var_refs_in_stmts(stmts: &[Stmt<'_, '_>], var_name: &str, out: &mut Vec<Span>) {
    for stmt in stmts {
        var_refs_in_stmt(stmt, var_name, out);
    }
}

fn var_refs_in_stmt(stmt: &Stmt<'_, '_>, var_name: &str, out: &mut Vec<Span>) {
    match &stmt.kind {
        // Scope boundaries — do NOT recurse into these
        StmtKind::Function(_) | StmtKind::Class(_) | StmtKind::Trait(_) | StmtKind::Enum(_) => {}
        StmtKind::Expression(e) => var_refs_in_expr(e, var_name, out),
        StmtKind::Return(Some(e)) => var_refs_in_expr(e, var_name, out),
        StmtKind::Return(None) | StmtKind::Break(_) | StmtKind::Continue(_) => {}
        StmtKind::Echo(exprs) => {
            for e in exprs.iter() {
                var_refs_in_expr(e, var_name, out);
            }
        }
        StmtKind::If(i) => {
            var_refs_in_expr(&i.condition, var_name, out);
            var_refs_in_stmt(i.then_branch, var_name, out);
            for ei in i.elseif_branches.iter() {
                var_refs_in_expr(&ei.condition, var_name, out);
                var_refs_in_stmt(&ei.body, var_name, out);
            }
            if let Some(e) = &i.else_branch {
                var_refs_in_stmt(e, var_name, out);
            }
        }
        StmtKind::While(w) => {
            var_refs_in_expr(&w.condition, var_name, out);
            var_refs_in_stmt(w.body, var_name, out);
        }
        StmtKind::DoWhile(d) => {
            var_refs_in_stmt(d.body, var_name, out);
            var_refs_in_expr(&d.condition, var_name, out);
        }
        StmtKind::Foreach(f) => {
            var_refs_in_expr(&f.expr, var_name, out);
            if let Some(k) = &f.key {
                var_refs_in_expr(k, var_name, out);
            }
            var_refs_in_expr(&f.value, var_name, out);
            var_refs_in_stmt(f.body, var_name, out);
        }
        StmtKind::For(f) => {
            for e in f.init.iter() {
                var_refs_in_expr(e, var_name, out);
            }
            for cond in f.condition.iter() {
                var_refs_in_expr(cond, var_name, out);
            }
            for e in f.update.iter() {
                var_refs_in_expr(e, var_name, out);
            }
            var_refs_in_stmt(f.body, var_name, out);
        }
        StmtKind::TryCatch(t) => {
            var_refs_in_stmts(&t.body, var_name, out);
            for catch in t.catches.iter() {
                var_refs_in_stmts(&catch.body, var_name, out);
            }
            if let Some(finally) = &t.finally {
                var_refs_in_stmts(finally, var_name, out);
            }
        }
        StmtKind::Block(inner) => var_refs_in_stmts(inner, var_name, out),
        StmtKind::StaticVar(vars) => {
            for v in vars.iter() {
                if let Some(def) = &v.default {
                    var_refs_in_expr(def, var_name, out);
                }
            }
        }
        StmtKind::Namespace(ns) => {
            if let NamespaceBody::Braced(inner) = &ns.body {
                var_refs_in_stmts(inner, var_name, out);
            }
        }
        _ => {}
    }
}

fn var_refs_in_expr(expr: &Expr<'_, '_>, var_name: &str, out: &mut Vec<Span>) {
    match &expr.kind {
        ExprKind::Variable(name) => {
            if name.as_ref() == var_name {
                out.push(expr.span);
            }
        }
        ExprKind::Assign(a) => {
            var_refs_in_expr(a.target, var_name, out);
            var_refs_in_expr(a.value, var_name, out);
        }
        ExprKind::Binary(b) => {
            var_refs_in_expr(b.left, var_name, out);
            var_refs_in_expr(b.right, var_name, out);
        }
        ExprKind::UnaryPrefix(u) => var_refs_in_expr(u.operand, var_name, out),
        ExprKind::UnaryPostfix(u) => var_refs_in_expr(u.operand, var_name, out),
        ExprKind::Ternary(t) => {
            var_refs_in_expr(t.condition, var_name, out);
            if let Some(then_expr) = t.then_expr {
                var_refs_in_expr(then_expr, var_name, out);
            }
            var_refs_in_expr(t.else_expr, var_name, out);
        }
        ExprKind::NullCoalesce(n) => {
            var_refs_in_expr(n.left, var_name, out);
            var_refs_in_expr(n.right, var_name, out);
        }
        ExprKind::Parenthesized(e) => var_refs_in_expr(e, var_name, out),
        ExprKind::ErrorSuppress(e) => var_refs_in_expr(e, var_name, out),
        ExprKind::Cast(_, e) => var_refs_in_expr(e, var_name, out),
        ExprKind::Clone(e) => var_refs_in_expr(e, var_name, out),
        ExprKind::ThrowExpr(e) => var_refs_in_expr(e, var_name, out),
        ExprKind::Print(e) => var_refs_in_expr(e, var_name, out),
        ExprKind::Empty(e) => var_refs_in_expr(e, var_name, out),
        ExprKind::Eval(e) => var_refs_in_expr(e, var_name, out),
        ExprKind::FunctionCall(f) => {
            var_refs_in_expr(f.name, var_name, out);
            for a in f.args.iter() {
                var_refs_in_expr(&a.value, var_name, out);
            }
        }
        ExprKind::MethodCall(m) => {
            var_refs_in_expr(m.object, var_name, out);
            for a in m.args.iter() {
                var_refs_in_expr(&a.value, var_name, out);
            }
        }
        ExprKind::NullsafeMethodCall(m) => {
            var_refs_in_expr(m.object, var_name, out);
            for a in m.args.iter() {
                var_refs_in_expr(&a.value, var_name, out);
            }
        }
        ExprKind::StaticMethodCall(s) => {
            var_refs_in_expr(s.class, var_name, out);
            for a in s.args.iter() {
                var_refs_in_expr(&a.value, var_name, out);
            }
        }
        ExprKind::New(n) => {
            var_refs_in_expr(n.class, var_name, out);
            for a in n.args.iter() {
                var_refs_in_expr(&a.value, var_name, out);
            }
        }
        ExprKind::ArrayAccess(a) => {
            var_refs_in_expr(a.array, var_name, out);
            if let Some(idx) = a.index {
                var_refs_in_expr(idx, var_name, out);
            }
        }
        ExprKind::PropertyAccess(p) => var_refs_in_expr(p.object, var_name, out),
        ExprKind::NullsafePropertyAccess(p) => var_refs_in_expr(p.object, var_name, out),
        ExprKind::StaticPropertyAccess(s) => var_refs_in_expr(s.class, var_name, out),
        ExprKind::Yield(y) => {
            if let Some(k) = y.key {
                var_refs_in_expr(k, var_name, out);
            }
            if let Some(v) = y.value {
                var_refs_in_expr(v, var_name, out);
            }
        }
        ExprKind::Match(m) => {
            var_refs_in_expr(m.subject, var_name, out);
            for arm in m.arms.iter() {
                if let Some(conds) = &arm.conditions {
                    for c in conds.iter() {
                        var_refs_in_expr(c, var_name, out);
                    }
                }
                var_refs_in_expr(&arm.body, var_name, out);
            }
        }
        ExprKind::Array(elems) => {
            for el in elems.iter() {
                if let Some(k) = &el.key {
                    var_refs_in_expr(k, var_name, out);
                }
                var_refs_in_expr(&el.value, var_name, out);
            }
        }
        ExprKind::Isset(exprs) => {
            for e in exprs.iter() {
                var_refs_in_expr(e, var_name, out);
            }
        }
        ExprKind::Include(_, e) => var_refs_in_expr(e, var_name, out),
        ExprKind::Exit(Some(e)) => var_refs_in_expr(e, var_name, out),
        // Scope boundaries within expressions — do NOT recurse into these
        ExprKind::Closure(_) | ExprKind::ArrowFunction(_) => {}
        _ => {}
    }
}

/// Collect all `$var_name` spans within the innermost function/method scope
/// that contains `byte_off`. If `byte_off` is not inside any function, collects
/// from the top-level stmts (respecting scope boundaries). Also collects the
/// parameter declaration span when the variable is a parameter of the scope.
pub fn collect_var_refs_in_scope(
    stmts: &[Stmt<'_, '_>],
    var_name: &str,
    byte_off: usize,
    out: &mut Vec<Span>,
) {
    for stmt in stmts {
        if collect_in_fn_at(stmt, var_name, byte_off, out) {
            return;
        }
    }
    // Not inside any function — collect top-level
    var_refs_in_stmts(stmts, var_name, out);
}

/// Returns `true` if `stmt` is (or contains) the function/method that owns `byte_off`
/// and has populated `out` with variable + param spans for `var_name`.
fn collect_in_fn_at(
    stmt: &Stmt<'_, '_>,
    var_name: &str,
    byte_off: usize,
    out: &mut Vec<Span>,
) -> bool {
    match &stmt.kind {
        StmtKind::Function(f) => {
            if byte_off < stmt.span.start as usize || byte_off >= stmt.span.end as usize {
                return false;
            }
            // Check nested functions first.
            for inner in f.body.iter() {
                if collect_in_fn_at(inner, var_name, byte_off, out) {
                    return true;
                }
            }
            // This is the enclosing function — collect param + body refs.
            for p in f.params.iter() {
                if p.name == var_name {
                    out.push(p.span);
                }
            }
            var_refs_in_stmts(&f.body, var_name, out);
            true
        }
        StmtKind::Class(c) => {
            for member in c.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind {
                    if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
                    {
                        continue;
                    }
                    if let Some(body) = &m.body {
                        for inner in body.iter() {
                            if collect_in_fn_at(inner, var_name, byte_off, out) {
                                return true;
                            }
                        }
                        for p in m.params.iter() {
                            if p.name == var_name {
                                out.push(p.span);
                            }
                        }
                        var_refs_in_stmts(body, var_name, out);
                    }
                    return true;
                }
            }
            false
        }
        StmtKind::Trait(t) => {
            for member in t.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind {
                    if byte_off < member.span.start as usize || byte_off >= member.span.end as usize
                    {
                        continue;
                    }
                    if let Some(body) = &m.body {
                        for inner in body.iter() {
                            if collect_in_fn_at(inner, var_name, byte_off, out) {
                                return true;
                            }
                        }
                        for p in m.params.iter() {
                            if p.name == var_name {
                                out.push(p.span);
                            }
                        }
                        var_refs_in_stmts(body, var_name, out);
                    }
                    return true;
                }
            }
            false
        }
        StmtKind::Namespace(ns) => {
            if let NamespaceBody::Braced(inner) = &ns.body {
                for s in inner.iter() {
                    if collect_in_fn_at(s, var_name, byte_off, out) {
                        return true;
                    }
                }
            }
            false
        }
        _ => false,
    }
}

// ── Property rename helpers ───────────────────────────────────────────────────

/// Collect all spans where `prop_name` is accessed (`->prop`, `?->prop`) or
/// declared as a class/trait property, across all statements.
/// Because `PropertyAccess.property` is a `&'src str` sub-slice of `source`,
/// we use `str_offset` (pointer arithmetic) to obtain its byte offset.
pub fn property_refs_in_stmts(
    source: &str,
    stmts: &[Stmt<'_, '_>],
    prop_name: &str,
    out: &mut Vec<Span>,
) {
    for stmt in stmts {
        property_refs_in_stmt(source, stmt, prop_name, out);
    }
}

fn property_refs_in_stmt(source: &str, stmt: &Stmt<'_, '_>, prop_name: &str, out: &mut Vec<Span>) {
    match &stmt.kind {
        StmtKind::Expression(e) => property_refs_in_expr(source, e, prop_name, out),
        StmtKind::Return(Some(e)) => property_refs_in_expr(source, e, prop_name, out),
        StmtKind::Echo(exprs) => {
            for e in exprs.iter() {
                property_refs_in_expr(source, e, prop_name, out);
            }
        }
        StmtKind::Function(f) => {
            property_refs_in_stmts(source, &f.body, prop_name, out);
        }
        StmtKind::Class(c) => {
            for member in c.members.iter() {
                match &member.kind {
                    ClassMemberKind::Property(p) if p.name == prop_name => {
                        let offset = str_offset(source, p.name);
                        out.push(Span {
                            start: offset,
                            end: offset + p.name.len() as u32,
                        });
                    }
                    ClassMemberKind::Method(m) => {
                        if let Some(body) = &m.body {
                            property_refs_in_stmts(source, body, prop_name, out);
                        }
                    }
                    _ => {}
                }
            }
        }
        StmtKind::Trait(t) => {
            for member in t.members.iter() {
                match &member.kind {
                    ClassMemberKind::Property(p) if p.name == prop_name => {
                        let offset = str_offset(source, p.name);
                        out.push(Span {
                            start: offset,
                            end: offset + p.name.len() as u32,
                        });
                    }
                    ClassMemberKind::Method(m) => {
                        if let Some(body) = &m.body {
                            property_refs_in_stmts(source, body, prop_name, out);
                        }
                    }
                    _ => {}
                }
            }
        }
        StmtKind::Enum(e) => {
            for member in e.members.iter() {
                if let EnumMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    property_refs_in_stmts(source, body, prop_name, out);
                }
            }
        }
        StmtKind::Namespace(ns) => {
            if let NamespaceBody::Braced(inner) = &ns.body {
                property_refs_in_stmts(source, inner, prop_name, out);
            }
        }
        StmtKind::If(i) => {
            property_refs_in_expr(source, &i.condition, prop_name, out);
            property_refs_in_stmt(source, i.then_branch, prop_name, out);
            for ei in i.elseif_branches.iter() {
                property_refs_in_expr(source, &ei.condition, prop_name, out);
                property_refs_in_stmt(source, &ei.body, prop_name, out);
            }
            if let Some(e) = &i.else_branch {
                property_refs_in_stmt(source, e, prop_name, out);
            }
        }
        StmtKind::While(w) => {
            property_refs_in_expr(source, &w.condition, prop_name, out);
            property_refs_in_stmt(source, w.body, prop_name, out);
        }
        StmtKind::DoWhile(d) => {
            property_refs_in_stmt(source, d.body, prop_name, out);
            property_refs_in_expr(source, &d.condition, prop_name, out);
        }
        StmtKind::Foreach(f) => {
            property_refs_in_expr(source, &f.expr, prop_name, out);
            property_refs_in_stmt(source, f.body, prop_name, out);
        }
        StmtKind::For(f) => {
            for e in f.init.iter() {
                property_refs_in_expr(source, e, prop_name, out);
            }
            for cond in f.condition.iter() {
                property_refs_in_expr(source, cond, prop_name, out);
            }
            for e in f.update.iter() {
                property_refs_in_expr(source, e, prop_name, out);
            }
            property_refs_in_stmt(source, f.body, prop_name, out);
        }
        StmtKind::TryCatch(t) => {
            property_refs_in_stmts(source, &t.body, prop_name, out);
            for catch in t.catches.iter() {
                property_refs_in_stmts(source, &catch.body, prop_name, out);
            }
            if let Some(finally) = &t.finally {
                property_refs_in_stmts(source, finally, prop_name, out);
            }
        }
        StmtKind::Block(inner) => property_refs_in_stmts(source, inner, prop_name, out),
        _ => {}
    }
}

fn property_refs_in_expr(source: &str, expr: &Expr<'_, '_>, prop_name: &str, out: &mut Vec<Span>) {
    match &expr.kind {
        ExprKind::PropertyAccess(p) => {
            property_refs_in_expr(source, p.object, prop_name, out);
            let span = p.property.span;
            let name_in_src = source
                .get(span.start as usize..span.end as usize)
                .unwrap_or("");
            if name_in_src == prop_name {
                out.push(span);
            }
        }
        ExprKind::NullsafePropertyAccess(p) => {
            property_refs_in_expr(source, p.object, prop_name, out);
            let span = p.property.span;
            let name_in_src = source
                .get(span.start as usize..span.end as usize)
                .unwrap_or("");
            if name_in_src == prop_name {
                out.push(span);
            }
        }
        ExprKind::Assign(a) => {
            property_refs_in_expr(source, a.target, prop_name, out);
            property_refs_in_expr(source, a.value, prop_name, out);
        }
        ExprKind::Binary(b) => {
            property_refs_in_expr(source, b.left, prop_name, out);
            property_refs_in_expr(source, b.right, prop_name, out);
        }
        ExprKind::UnaryPrefix(u) => property_refs_in_expr(source, u.operand, prop_name, out),
        ExprKind::UnaryPostfix(u) => property_refs_in_expr(source, u.operand, prop_name, out),
        ExprKind::Ternary(t) => {
            property_refs_in_expr(source, t.condition, prop_name, out);
            if let Some(then_expr) = t.then_expr {
                property_refs_in_expr(source, then_expr, prop_name, out);
            }
            property_refs_in_expr(source, t.else_expr, prop_name, out);
        }
        ExprKind::NullCoalesce(n) => {
            property_refs_in_expr(source, n.left, prop_name, out);
            property_refs_in_expr(source, n.right, prop_name, out);
        }
        ExprKind::Parenthesized(e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::ErrorSuppress(e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::Cast(_, e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::Clone(e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::ThrowExpr(e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::Print(e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::Empty(e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::Eval(e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::FunctionCall(f) => {
            property_refs_in_expr(source, f.name, prop_name, out);
            for a in f.args.iter() {
                property_refs_in_expr(source, &a.value, prop_name, out);
            }
        }
        ExprKind::MethodCall(m) => {
            property_refs_in_expr(source, m.object, prop_name, out);
            for a in m.args.iter() {
                property_refs_in_expr(source, &a.value, prop_name, out);
            }
        }
        ExprKind::NullsafeMethodCall(m) => {
            property_refs_in_expr(source, m.object, prop_name, out);
            for a in m.args.iter() {
                property_refs_in_expr(source, &a.value, prop_name, out);
            }
        }
        ExprKind::StaticMethodCall(s) => {
            property_refs_in_expr(source, s.class, prop_name, out);
            for a in s.args.iter() {
                property_refs_in_expr(source, &a.value, prop_name, out);
            }
        }
        ExprKind::New(n) => {
            property_refs_in_expr(source, n.class, prop_name, out);
            for a in n.args.iter() {
                property_refs_in_expr(source, &a.value, prop_name, out);
            }
        }
        ExprKind::ArrayAccess(a) => {
            property_refs_in_expr(source, a.array, prop_name, out);
            if let Some(idx) = a.index {
                property_refs_in_expr(source, idx, prop_name, out);
            }
        }
        ExprKind::Yield(y) => {
            if let Some(k) = y.key {
                property_refs_in_expr(source, k, prop_name, out);
            }
            if let Some(v) = y.value {
                property_refs_in_expr(source, v, prop_name, out);
            }
        }
        ExprKind::Match(m) => {
            property_refs_in_expr(source, m.subject, prop_name, out);
            for arm in m.arms.iter() {
                if let Some(conds) = &arm.conditions {
                    for c in conds.iter() {
                        property_refs_in_expr(source, c, prop_name, out);
                    }
                }
                property_refs_in_expr(source, &arm.body, prop_name, out);
            }
        }
        ExprKind::Array(elems) => {
            for el in elems.iter() {
                if let Some(k) = &el.key {
                    property_refs_in_expr(source, k, prop_name, out);
                }
                property_refs_in_expr(source, &el.value, prop_name, out);
            }
        }
        ExprKind::Isset(exprs) => {
            for e in exprs.iter() {
                property_refs_in_expr(source, e, prop_name, out);
            }
        }
        ExprKind::Include(_, e) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::Exit(Some(e)) => property_refs_in_expr(source, e, prop_name, out),
        ExprKind::Closure(c) => property_refs_in_stmts(source, &c.body, prop_name, out),
        ExprKind::ArrowFunction(a) => property_refs_in_expr(source, a.body, prop_name, out),
        _ => {}
    }
}

fn args(source: &str, arg_list: &[php_ast::Arg<'_, '_>], word: &str, out: &mut Vec<Span>) {
    for a in arg_list.iter() {
        refs_in_expr(source, &a.value, word, out);
    }
}

pub fn refs_in_expr(source: &str, expr: &Expr<'_, '_>, word: &str, out: &mut Vec<Span>) {
    match &expr.kind {
        ExprKind::Identifier(name) => {
            if name.as_ref() == word {
                out.push(expr.span);
            }
        }
        ExprKind::FunctionCall(f) => {
            refs_in_expr(source, f.name, word, out);
            args(source, &f.args, word, out);
        }
        ExprKind::MethodCall(m) => {
            refs_in_expr(source, m.object, word, out);
            refs_in_expr(source, m.method, word, out);
            args(source, &m.args, word, out);
        }
        ExprKind::NullsafeMethodCall(m) => {
            refs_in_expr(source, m.object, word, out);
            refs_in_expr(source, m.method, word, out);
            args(source, &m.args, word, out);
        }
        ExprKind::StaticMethodCall(s) => {
            refs_in_expr(source, s.class, word, out);
            if s.method.as_ref() == word {
                out.push(expr.span);
            }
            args(source, &s.args, word, out);
        }
        ExprKind::New(n) => {
            refs_in_expr(source, n.class, word, out);
            args(source, &n.args, word, out);
        }
        ExprKind::Assign(a) => {
            refs_in_expr(source, a.target, word, out);
            refs_in_expr(source, a.value, word, out);
        }
        ExprKind::Binary(b) => {
            refs_in_expr(source, b.left, word, out);
            refs_in_expr(source, b.right, word, out);
        }
        ExprKind::UnaryPrefix(u) => refs_in_expr(source, u.operand, word, out),
        ExprKind::UnaryPostfix(u) => refs_in_expr(source, u.operand, word, out),
        ExprKind::Ternary(t) => {
            refs_in_expr(source, t.condition, word, out);
            if let Some(then_expr) = t.then_expr {
                refs_in_expr(source, then_expr, word, out);
            }
            refs_in_expr(source, t.else_expr, word, out);
        }
        ExprKind::NullCoalesce(n) => {
            refs_in_expr(source, n.left, word, out);
            refs_in_expr(source, n.right, word, out);
        }
        ExprKind::Parenthesized(e) => refs_in_expr(source, e, word, out),
        ExprKind::ErrorSuppress(e) => refs_in_expr(source, e, word, out),
        ExprKind::Cast(_, e) => refs_in_expr(source, e, word, out),
        ExprKind::Clone(e) => refs_in_expr(source, e, word, out),
        ExprKind::ThrowExpr(e) => refs_in_expr(source, e, word, out),
        ExprKind::Print(e) => refs_in_expr(source, e, word, out),
        ExprKind::Empty(e) => refs_in_expr(source, e, word, out),
        ExprKind::Eval(e) => refs_in_expr(source, e, word, out),
        ExprKind::Yield(y) => {
            if let Some(k) = y.key {
                refs_in_expr(source, k, word, out);
            }
            if let Some(v) = y.value {
                refs_in_expr(source, v, word, out);
            }
        }
        ExprKind::ArrayAccess(a) => {
            refs_in_expr(source, a.array, word, out);
            if let Some(idx) = a.index {
                refs_in_expr(source, idx, word, out);
            }
        }
        ExprKind::PropertyAccess(p) => refs_in_expr(source, p.object, word, out),
        ExprKind::NullsafePropertyAccess(p) => refs_in_expr(source, p.object, word, out),
        ExprKind::StaticPropertyAccess(s) => refs_in_expr(source, s.class, word, out),
        ExprKind::ClassConstAccess(c) => {
            refs_in_expr(source, c.class, word, out);
            if c.member.as_ref() == word {
                out.push(expr.span);
            }
        }
        ExprKind::Closure(c) => refs_in_stmts(source, &c.body, word, out),
        ExprKind::ArrowFunction(a) => refs_in_expr(source, a.body, word, out),
        ExprKind::Match(m) => {
            refs_in_expr(source, m.subject, word, out);
            for arm in m.arms.iter() {
                if let Some(conds) = &arm.conditions {
                    for cond in conds.iter() {
                        refs_in_expr(source, cond, word, out);
                    }
                }
                refs_in_expr(source, &arm.body, word, out);
            }
        }
        ExprKind::Array(elements) => {
            for elem in elements.iter() {
                if let Some(key) = &elem.key {
                    refs_in_expr(source, key, word, out);
                }
                refs_in_expr(source, &elem.value, word, out);
            }
        }
        ExprKind::Isset(exprs) => {
            for e in exprs.iter() {
                refs_in_expr(source, e, word, out);
            }
        }
        ExprKind::Include(_, e) => refs_in_expr(source, e, word, out),
        ExprKind::Exit(Some(e)) => refs_in_expr(source, e, word, out),
        ExprKind::AnonymousClass(c) => {
            for member in c.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    refs_in_stmts(source, body, word, out);
                }
            }
        }
        _ => {}
    }
}

// ── Semantic (context-aware) reference walkers ────────────────────────────────

/// Collect spans where `name` is called as a free function (not a method).
/// Only matches `name(...)` calls where the callee is a bare identifier, not
/// `$obj->name()` or `Class::name()`.
pub fn function_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
    for stmt in stmts {
        function_refs_in_stmt(stmt, name, out);
    }
}

fn function_refs_in_stmt(stmt: &Stmt<'_, '_>, name: &str, out: &mut Vec<Span>) {
    match &stmt.kind {
        StmtKind::Expression(e) => function_refs_in_expr(e, name, out),
        StmtKind::Return(Some(e)) => function_refs_in_expr(e, name, out),
        StmtKind::Echo(exprs) => {
            for e in exprs.iter() {
                function_refs_in_expr(e, name, out);
            }
        }
        StmtKind::Function(f) => {
            function_refs_in_stmts(&f.body, name, out);
        }
        StmtKind::Class(c) => {
            for member in c.members.iter() {
                match &member.kind {
                    ClassMemberKind::Method(m) => {
                        if let Some(body) = &m.body {
                            function_refs_in_stmts(body, name, out);
                        }
                    }
                    ClassMemberKind::Property(p) => {
                        if let Some(default) = &p.default {
                            function_refs_in_expr(default, name, out);
                        }
                    }
                    _ => {}
                }
            }
        }
        StmtKind::Trait(t) => {
            for member in t.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    function_refs_in_stmts(body, name, out);
                }
            }
        }
        StmtKind::Enum(e) => {
            for member in e.members.iter() {
                if let EnumMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    function_refs_in_stmts(body, name, out);
                }
            }
        }
        StmtKind::Namespace(ns) => {
            if let NamespaceBody::Braced(inner) = &ns.body {
                function_refs_in_stmts(inner, name, out);
            }
        }
        StmtKind::If(i) => {
            function_refs_in_expr(&i.condition, name, out);
            function_refs_in_stmt(i.then_branch, name, out);
            for ei in i.elseif_branches.iter() {
                function_refs_in_expr(&ei.condition, name, out);
                function_refs_in_stmt(&ei.body, name, out);
            }
            if let Some(e) = &i.else_branch {
                function_refs_in_stmt(e, name, out);
            }
        }
        StmtKind::While(w) => {
            function_refs_in_expr(&w.condition, name, out);
            function_refs_in_stmt(w.body, name, out);
        }
        StmtKind::DoWhile(d) => {
            function_refs_in_stmt(d.body, name, out);
            function_refs_in_expr(&d.condition, name, out);
        }
        StmtKind::Foreach(f) => {
            function_refs_in_expr(&f.expr, name, out);
            function_refs_in_stmt(f.body, name, out);
        }
        StmtKind::For(f) => {
            for e in f.init.iter() {
                function_refs_in_expr(e, name, out);
            }
            for cond in f.condition.iter() {
                function_refs_in_expr(cond, name, out);
            }
            for e in f.update.iter() {
                function_refs_in_expr(e, name, out);
            }
            function_refs_in_stmt(f.body, name, out);
        }
        StmtKind::TryCatch(t) => {
            function_refs_in_stmts(&t.body, name, out);
            for catch in t.catches.iter() {
                function_refs_in_stmts(&catch.body, name, out);
            }
            if let Some(finally) = &t.finally {
                function_refs_in_stmts(finally, name, out);
            }
        }
        StmtKind::Block(stmts) => function_refs_in_stmts(stmts, name, out),
        _ => {}
    }
}

fn function_refs_in_expr(expr: &Expr<'_, '_>, name: &str, out: &mut Vec<Span>) {
    match &expr.kind {
        // The core match: a free function call whose callee is a bare identifier.
        ExprKind::FunctionCall(f) => {
            if let ExprKind::Identifier(id) = &f.name.kind
                && id.as_ref() == name
            {
                out.push(f.name.span);
            }
            // Still recurse into args and a dynamic callee.
            function_refs_in_expr(f.name, name, out);
            for a in f.args.iter() {
                function_refs_in_expr(&a.value, name, out);
            }
        }
        // Recurse into all expression sub-nodes (but skip method/static call names).
        ExprKind::MethodCall(m) => {
            function_refs_in_expr(m.object, name, out);
            // do NOT check m.method — that is a method name, not a function name
            for a in m.args.iter() {
                function_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::NullsafeMethodCall(m) => {
            function_refs_in_expr(m.object, name, out);
            for a in m.args.iter() {
                function_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::StaticMethodCall(s) => {
            function_refs_in_expr(s.class, name, out);
            for a in s.args.iter() {
                function_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::New(n) => {
            for a in n.args.iter() {
                function_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::Assign(a) => {
            function_refs_in_expr(a.target, name, out);
            function_refs_in_expr(a.value, name, out);
        }
        ExprKind::Binary(b) => {
            function_refs_in_expr(b.left, name, out);
            function_refs_in_expr(b.right, name, out);
        }
        ExprKind::UnaryPrefix(u) => function_refs_in_expr(u.operand, name, out),
        ExprKind::UnaryPostfix(u) => function_refs_in_expr(u.operand, name, out),
        ExprKind::Ternary(t) => {
            function_refs_in_expr(t.condition, name, out);
            if let Some(e) = t.then_expr {
                function_refs_in_expr(e, name, out);
            }
            function_refs_in_expr(t.else_expr, name, out);
        }
        ExprKind::NullCoalesce(n) => {
            function_refs_in_expr(n.left, name, out);
            function_refs_in_expr(n.right, name, out);
        }
        ExprKind::Parenthesized(e) => function_refs_in_expr(e, name, out),
        ExprKind::ErrorSuppress(e) => function_refs_in_expr(e, name, out),
        ExprKind::Cast(_, e) => function_refs_in_expr(e, name, out),
        ExprKind::Clone(e) => function_refs_in_expr(e, name, out),
        ExprKind::ThrowExpr(e) => function_refs_in_expr(e, name, out),
        ExprKind::Print(e) => function_refs_in_expr(e, name, out),
        ExprKind::Empty(e) => function_refs_in_expr(e, name, out),
        ExprKind::Eval(e) => function_refs_in_expr(e, name, out),
        ExprKind::Yield(y) => {
            if let Some(k) = y.key {
                function_refs_in_expr(k, name, out);
            }
            if let Some(v) = y.value {
                function_refs_in_expr(v, name, out);
            }
        }
        ExprKind::ArrayAccess(a) => {
            function_refs_in_expr(a.array, name, out);
            if let Some(idx) = a.index {
                function_refs_in_expr(idx, name, out);
            }
        }
        ExprKind::PropertyAccess(p) => function_refs_in_expr(p.object, name, out),
        ExprKind::NullsafePropertyAccess(p) => function_refs_in_expr(p.object, name, out),
        ExprKind::StaticPropertyAccess(s) => function_refs_in_expr(s.class, name, out),
        ExprKind::Match(m) => {
            function_refs_in_expr(m.subject, name, out);
            for arm in m.arms.iter() {
                if let Some(conds) = &arm.conditions {
                    for c in conds.iter() {
                        function_refs_in_expr(c, name, out);
                    }
                }
                function_refs_in_expr(&arm.body, name, out);
            }
        }
        ExprKind::Array(elements) => {
            for elem in elements.iter() {
                if let Some(key) = &elem.key {
                    function_refs_in_expr(key, name, out);
                }
                function_refs_in_expr(&elem.value, name, out);
            }
        }
        ExprKind::Isset(exprs) => {
            for e in exprs.iter() {
                function_refs_in_expr(e, name, out);
            }
        }
        ExprKind::Include(_, e) => function_refs_in_expr(e, name, out),
        ExprKind::Exit(Some(e)) => function_refs_in_expr(e, name, out),
        ExprKind::Closure(c) => function_refs_in_stmts(&c.body, name, out),
        ExprKind::ArrowFunction(a) => function_refs_in_expr(a.body, name, out),
        ExprKind::AnonymousClass(c) => {
            for member in c.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    function_refs_in_stmts(body, name, out);
                }
            }
        }
        _ => {}
    }
}

/// Collect spans where `name` is used as a method: `->name()`, `?->name()`, `::name()`.
/// Does NOT match free function calls or class-name identifiers.
pub fn method_refs_in_stmts(stmts: &[Stmt<'_, '_>], name: &str, out: &mut Vec<Span>) {
    for stmt in stmts {
        method_refs_in_stmt(stmt, name, out);
    }
}

fn method_refs_in_stmt(stmt: &Stmt<'_, '_>, name: &str, out: &mut Vec<Span>) {
    match &stmt.kind {
        StmtKind::Expression(e) => method_refs_in_expr(e, name, out),
        StmtKind::Return(Some(e)) => method_refs_in_expr(e, name, out),
        StmtKind::Echo(exprs) => {
            for e in exprs.iter() {
                method_refs_in_expr(e, name, out);
            }
        }
        StmtKind::Function(f) => method_refs_in_stmts(&f.body, name, out),
        StmtKind::Class(c) => {
            for member in c.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    method_refs_in_stmts(body, name, out);
                }
            }
        }
        StmtKind::Trait(t) => {
            for member in t.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    method_refs_in_stmts(body, name, out);
                }
            }
        }
        StmtKind::Enum(e) => {
            for member in e.members.iter() {
                if let EnumMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    method_refs_in_stmts(body, name, out);
                }
            }
        }
        StmtKind::Namespace(ns) => {
            if let NamespaceBody::Braced(inner) = &ns.body {
                method_refs_in_stmts(inner, name, out);
            }
        }
        StmtKind::If(i) => {
            method_refs_in_expr(&i.condition, name, out);
            method_refs_in_stmt(i.then_branch, name, out);
            for ei in i.elseif_branches.iter() {
                method_refs_in_expr(&ei.condition, name, out);
                method_refs_in_stmt(&ei.body, name, out);
            }
            if let Some(e) = &i.else_branch {
                method_refs_in_stmt(e, name, out);
            }
        }
        StmtKind::While(w) => {
            method_refs_in_expr(&w.condition, name, out);
            method_refs_in_stmt(w.body, name, out);
        }
        StmtKind::DoWhile(d) => {
            method_refs_in_stmt(d.body, name, out);
            method_refs_in_expr(&d.condition, name, out);
        }
        StmtKind::Foreach(f) => {
            method_refs_in_expr(&f.expr, name, out);
            method_refs_in_stmt(f.body, name, out);
        }
        StmtKind::For(f) => {
            for e in f.init.iter() {
                method_refs_in_expr(e, name, out);
            }
            for cond in f.condition.iter() {
                method_refs_in_expr(cond, name, out);
            }
            for e in f.update.iter() {
                method_refs_in_expr(e, name, out);
            }
            method_refs_in_stmt(f.body, name, out);
        }
        StmtKind::TryCatch(t) => {
            method_refs_in_stmts(&t.body, name, out);
            for catch in t.catches.iter() {
                method_refs_in_stmts(&catch.body, name, out);
            }
            if let Some(finally) = &t.finally {
                method_refs_in_stmts(finally, name, out);
            }
        }
        StmtKind::Block(stmts) => method_refs_in_stmts(stmts, name, out),
        _ => {}
    }
}

fn method_refs_in_expr(expr: &Expr<'_, '_>, name: &str, out: &mut Vec<Span>) {
    match &expr.kind {
        ExprKind::MethodCall(m) => {
            method_refs_in_expr(m.object, name, out);
            // Collect the method name span if it matches.
            if let ExprKind::Identifier(id) = &m.method.kind
                && id.as_ref() == name
            {
                out.push(m.method.span);
            }
            for a in m.args.iter() {
                method_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::NullsafeMethodCall(m) => {
            method_refs_in_expr(m.object, name, out);
            if let ExprKind::Identifier(id) = &m.method.kind
                && id.as_ref() == name
            {
                out.push(m.method.span);
            }
            for a in m.args.iter() {
                method_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::StaticMethodCall(s) => {
            method_refs_in_expr(s.class, name, out);
            if s.method.as_ref() == name {
                // For static calls, the span covers the whole expression; we need the
                // method-name portion. Use the existing refs_in_expr behaviour which
                // pushed expr.span for static methods — replicate that here.
                out.push(expr.span);
            }
            for a in s.args.iter() {
                method_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::FunctionCall(f) => {
            method_refs_in_expr(f.name, name, out);
            for a in f.args.iter() {
                method_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::New(n) => {
            for a in n.args.iter() {
                method_refs_in_expr(&a.value, name, out);
            }
        }
        ExprKind::Assign(a) => {
            method_refs_in_expr(a.target, name, out);
            method_refs_in_expr(a.value, name, out);
        }
        ExprKind::Binary(b) => {
            method_refs_in_expr(b.left, name, out);
            method_refs_in_expr(b.right, name, out);
        }
        ExprKind::UnaryPrefix(u) => method_refs_in_expr(u.operand, name, out),
        ExprKind::UnaryPostfix(u) => method_refs_in_expr(u.operand, name, out),
        ExprKind::Ternary(t) => {
            method_refs_in_expr(t.condition, name, out);
            if let Some(e) = t.then_expr {
                method_refs_in_expr(e, name, out);
            }
            method_refs_in_expr(t.else_expr, name, out);
        }
        ExprKind::NullCoalesce(n) => {
            method_refs_in_expr(n.left, name, out);
            method_refs_in_expr(n.right, name, out);
        }
        ExprKind::Parenthesized(e) => method_refs_in_expr(e, name, out),
        ExprKind::ErrorSuppress(e) => method_refs_in_expr(e, name, out),
        ExprKind::Cast(_, e) => method_refs_in_expr(e, name, out),
        ExprKind::Clone(e) => method_refs_in_expr(e, name, out),
        ExprKind::ThrowExpr(e) => method_refs_in_expr(e, name, out),
        ExprKind::Print(e) => method_refs_in_expr(e, name, out),
        ExprKind::Empty(e) => method_refs_in_expr(e, name, out),
        ExprKind::Eval(e) => method_refs_in_expr(e, name, out),
        ExprKind::Yield(y) => {
            if let Some(k) = y.key {
                method_refs_in_expr(k, name, out);
            }
            if let Some(v) = y.value {
                method_refs_in_expr(v, name, out);
            }
        }
        ExprKind::ArrayAccess(a) => {
            method_refs_in_expr(a.array, name, out);
            if let Some(idx) = a.index {
                method_refs_in_expr(idx, name, out);
            }
        }
        ExprKind::PropertyAccess(p) => method_refs_in_expr(p.object, name, out),
        ExprKind::NullsafePropertyAccess(p) => method_refs_in_expr(p.object, name, out),
        ExprKind::StaticPropertyAccess(s) => method_refs_in_expr(s.class, name, out),
        ExprKind::Match(m) => {
            method_refs_in_expr(m.subject, name, out);
            for arm in m.arms.iter() {
                if let Some(conds) = &arm.conditions {
                    for c in conds.iter() {
                        method_refs_in_expr(c, name, out);
                    }
                }
                method_refs_in_expr(&arm.body, name, out);
            }
        }
        ExprKind::Array(elements) => {
            for elem in elements.iter() {
                if let Some(key) = &elem.key {
                    method_refs_in_expr(key, name, out);
                }
                method_refs_in_expr(&elem.value, name, out);
            }
        }
        ExprKind::Isset(exprs) => {
            for e in exprs.iter() {
                method_refs_in_expr(e, name, out);
            }
        }
        ExprKind::Include(_, e) => method_refs_in_expr(e, name, out),
        ExprKind::Exit(Some(e)) => method_refs_in_expr(e, name, out),
        ExprKind::Closure(c) => method_refs_in_stmts(&c.body, name, out),
        ExprKind::ArrowFunction(a) => method_refs_in_expr(a.body, name, out),
        ExprKind::AnonymousClass(c) => {
            for member in c.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    method_refs_in_stmts(body, name, out);
                }
            }
        }
        _ => {}
    }
}

/// Collect spans where `class_name` is used as a class-type reference:
/// `new ClassName`, `extends ClassName`, `implements ClassName`, type hints,
/// and `$x instanceof ClassName`.  Does NOT match free function calls or
/// method names with the same spelling.
pub fn class_refs_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str, out: &mut Vec<Span>) {
    for stmt in stmts {
        class_refs_in_stmt(stmt, class_name, out);
    }
}

fn class_refs_in_stmt(stmt: &Stmt<'_, '_>, class_name: &str, out: &mut Vec<Span>) {
    match &stmt.kind {
        StmtKind::Expression(e) => class_refs_in_expr(e, class_name, out),
        StmtKind::Return(Some(e)) => class_refs_in_expr(e, class_name, out),
        StmtKind::Echo(exprs) => {
            for e in exprs.iter() {
                class_refs_in_expr(e, class_name, out);
            }
        }
        StmtKind::Function(f) => {
            // Type hints on params and return type
            for p in f.params.iter() {
                if let Some(th) = &p.type_hint {
                    collect_class_in_type_hint(th, class_name, out);
                }
            }
            if let Some(rt) = &f.return_type {
                collect_class_in_type_hint(rt, class_name, out);
            }
            class_refs_in_stmts(&f.body, class_name, out);
        }
        StmtKind::Class(c) => {
            // `extends ClassName`
            if let Some(ext) = &c.extends {
                let last = ext
                    .to_string_repr()
                    .rsplit('\\')
                    .next()
                    .unwrap_or("")
                    .to_string();
                if last == class_name {
                    let span = ext.span();
                    let offset = (ext.to_string_repr().len() - last.len()) as u32;
                    out.push(Span {
                        start: span.start + offset,
                        end: span.end,
                    });
                }
            }
            // `implements ClassName, ...`
            for iface in c.implements.iter() {
                let last = iface
                    .to_string_repr()
                    .rsplit('\\')
                    .next()
                    .unwrap_or("")
                    .to_string();
                if last == class_name {
                    let span = iface.span();
                    let offset = (iface.to_string_repr().len() - last.len()) as u32;
                    out.push(Span {
                        start: span.start + offset,
                        end: span.end,
                    });
                }
            }
            for member in c.members.iter() {
                match &member.kind {
                    ClassMemberKind::Method(m) => {
                        for p in m.params.iter() {
                            if let Some(th) = &p.type_hint {
                                collect_class_in_type_hint(th, class_name, out);
                            }
                        }
                        if let Some(rt) = &m.return_type {
                            collect_class_in_type_hint(rt, class_name, out);
                        }
                        if let Some(body) = &m.body {
                            class_refs_in_stmts(body, class_name, out);
                        }
                    }
                    ClassMemberKind::Property(p) => {
                        if let Some(th) = &p.type_hint {
                            collect_class_in_type_hint(th, class_name, out);
                        }
                        if let Some(default) = &p.default {
                            class_refs_in_expr(default, class_name, out);
                        }
                    }
                    _ => {}
                }
            }
        }
        StmtKind::Interface(i) => {
            for parent in i.extends.iter() {
                let last = parent
                    .to_string_repr()
                    .rsplit('\\')
                    .next()
                    .unwrap_or("")
                    .to_string();
                if last == class_name {
                    let span = parent.span();
                    let offset = (parent.to_string_repr().len() - last.len()) as u32;
                    out.push(Span {
                        start: span.start + offset,
                        end: span.end,
                    });
                }
            }
        }
        StmtKind::Trait(t) => {
            for member in t.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind {
                    for p in m.params.iter() {
                        if let Some(th) = &p.type_hint {
                            collect_class_in_type_hint(th, class_name, out);
                        }
                    }
                    if let Some(rt) = &m.return_type {
                        collect_class_in_type_hint(rt, class_name, out);
                    }
                    if let Some(body) = &m.body {
                        class_refs_in_stmts(body, class_name, out);
                    }
                }
            }
        }
        StmtKind::Enum(e) => {
            for member in e.members.iter() {
                if let EnumMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    class_refs_in_stmts(body, class_name, out);
                }
            }
        }
        StmtKind::Namespace(ns) => {
            if let NamespaceBody::Braced(inner) = &ns.body {
                class_refs_in_stmts(inner, class_name, out);
            }
        }
        StmtKind::If(i) => {
            class_refs_in_expr(&i.condition, class_name, out);
            class_refs_in_stmt(i.then_branch, class_name, out);
            for ei in i.elseif_branches.iter() {
                class_refs_in_expr(&ei.condition, class_name, out);
                class_refs_in_stmt(&ei.body, class_name, out);
            }
            if let Some(e) = &i.else_branch {
                class_refs_in_stmt(e, class_name, out);
            }
        }
        StmtKind::While(w) => {
            class_refs_in_expr(&w.condition, class_name, out);
            class_refs_in_stmt(w.body, class_name, out);
        }
        StmtKind::DoWhile(d) => {
            class_refs_in_stmt(d.body, class_name, out);
            class_refs_in_expr(&d.condition, class_name, out);
        }
        StmtKind::Foreach(f) => {
            class_refs_in_expr(&f.expr, class_name, out);
            class_refs_in_stmt(f.body, class_name, out);
        }
        StmtKind::For(f) => {
            for e in f.init.iter() {
                class_refs_in_expr(e, class_name, out);
            }
            for cond in f.condition.iter() {
                class_refs_in_expr(cond, class_name, out);
            }
            for e in f.update.iter() {
                class_refs_in_expr(e, class_name, out);
            }
            class_refs_in_stmt(f.body, class_name, out);
        }
        StmtKind::TryCatch(t) => {
            class_refs_in_stmts(&t.body, class_name, out);
            for catch in t.catches.iter() {
                for ty in catch.types.iter() {
                    let last = ty
                        .to_string_repr()
                        .rsplit('\\')
                        .next()
                        .unwrap_or("")
                        .to_string();
                    if last == class_name {
                        let span = ty.span();
                        let offset = (ty.to_string_repr().len() - last.len()) as u32;
                        out.push(Span {
                            start: span.start + offset,
                            end: span.end,
                        });
                    }
                }
                class_refs_in_stmts(&catch.body, class_name, out);
            }
            if let Some(finally) = &t.finally {
                class_refs_in_stmts(finally, class_name, out);
            }
        }
        StmtKind::Block(stmts) => class_refs_in_stmts(stmts, class_name, out),
        _ => {}
    }
}

fn class_refs_in_expr(expr: &Expr<'_, '_>, class_name: &str, out: &mut Vec<Span>) {
    match &expr.kind {
        // `new ClassName(...)` — the class name is an Identifier child of New.
        ExprKind::New(n) => {
            if let ExprKind::Identifier(id) = &n.class.kind
                && id.as_ref().rsplit('\\').next().unwrap_or(id.as_ref()) == class_name
            {
                out.push(n.class.span);
            } else {
                // Dynamic `new $var()` etc. — no match but still recurse into args.
            }
            for a in n.args.iter() {
                class_refs_in_expr(&a.value, class_name, out);
            }
        }
        // `$x instanceof ClassName` — right operand is an Identifier.
        ExprKind::Binary(b) => {
            class_refs_in_expr(b.left, class_name, out);
            // For instanceof, the RHS is a class name; for other operators it is not.
            // We check the RHS regardless — if it is an Identifier and matches, we
            // include it; class names don't normally appear as operands otherwise.
            if let ExprKind::Identifier(id) = &b.right.kind
                && id.as_ref().rsplit('\\').next().unwrap_or(id.as_ref()) == class_name
            {
                out.push(b.right.span);
            } else {
                class_refs_in_expr(b.right, class_name, out);
            }
        }
        // `ClassName::method()` or `ClassName::$prop` — class side.
        ExprKind::StaticMethodCall(s) => {
            if let ExprKind::Identifier(id) = &s.class.kind
                && id.as_ref().rsplit('\\').next().unwrap_or(id.as_ref()) == class_name
            {
                out.push(s.class.span);
            }
            for a in s.args.iter() {
                class_refs_in_expr(&a.value, class_name, out);
            }
        }
        ExprKind::StaticPropertyAccess(s) => {
            if let ExprKind::Identifier(id) = &s.class.kind
                && id.as_ref().rsplit('\\').next().unwrap_or(id.as_ref()) == class_name
            {
                out.push(s.class.span);
            }
        }
        ExprKind::ClassConstAccess(c) => {
            if let ExprKind::Identifier(id) = &c.class.kind
                && id.as_ref().rsplit('\\').next().unwrap_or(id.as_ref()) == class_name
            {
                out.push(c.class.span);
            }
        }
        // Recurse into other expression kinds.
        ExprKind::FunctionCall(f) => {
            for a in f.args.iter() {
                class_refs_in_expr(&a.value, class_name, out);
            }
        }
        ExprKind::MethodCall(m) => {
            class_refs_in_expr(m.object, class_name, out);
            for a in m.args.iter() {
                class_refs_in_expr(&a.value, class_name, out);
            }
        }
        ExprKind::NullsafeMethodCall(m) => {
            class_refs_in_expr(m.object, class_name, out);
            for a in m.args.iter() {
                class_refs_in_expr(&a.value, class_name, out);
            }
        }
        ExprKind::Assign(a) => {
            class_refs_in_expr(a.target, class_name, out);
            class_refs_in_expr(a.value, class_name, out);
        }
        ExprKind::UnaryPrefix(u) => class_refs_in_expr(u.operand, class_name, out),
        ExprKind::UnaryPostfix(u) => class_refs_in_expr(u.operand, class_name, out),
        ExprKind::Ternary(t) => {
            class_refs_in_expr(t.condition, class_name, out);
            if let Some(e) = t.then_expr {
                class_refs_in_expr(e, class_name, out);
            }
            class_refs_in_expr(t.else_expr, class_name, out);
        }
        ExprKind::NullCoalesce(n) => {
            class_refs_in_expr(n.left, class_name, out);
            class_refs_in_expr(n.right, class_name, out);
        }
        ExprKind::Parenthesized(e) => class_refs_in_expr(e, class_name, out),
        ExprKind::ErrorSuppress(e) => class_refs_in_expr(e, class_name, out),
        ExprKind::Cast(_, e) => class_refs_in_expr(e, class_name, out),
        ExprKind::Clone(e) => class_refs_in_expr(e, class_name, out),
        ExprKind::ThrowExpr(e) => class_refs_in_expr(e, class_name, out),
        ExprKind::Print(e) => class_refs_in_expr(e, class_name, out),
        ExprKind::Empty(e) => class_refs_in_expr(e, class_name, out),
        ExprKind::Eval(e) => class_refs_in_expr(e, class_name, out),
        ExprKind::Yield(y) => {
            if let Some(k) = y.key {
                class_refs_in_expr(k, class_name, out);
            }
            if let Some(v) = y.value {
                class_refs_in_expr(v, class_name, out);
            }
        }
        ExprKind::ArrayAccess(a) => {
            class_refs_in_expr(a.array, class_name, out);
            if let Some(idx) = a.index {
                class_refs_in_expr(idx, class_name, out);
            }
        }
        ExprKind::PropertyAccess(p) => class_refs_in_expr(p.object, class_name, out),
        ExprKind::NullsafePropertyAccess(p) => class_refs_in_expr(p.object, class_name, out),
        ExprKind::Match(m) => {
            class_refs_in_expr(m.subject, class_name, out);
            for arm in m.arms.iter() {
                if let Some(conds) = &arm.conditions {
                    for c in conds.iter() {
                        class_refs_in_expr(c, class_name, out);
                    }
                }
                class_refs_in_expr(&arm.body, class_name, out);
            }
        }
        ExprKind::Array(elements) => {
            for elem in elements.iter() {
                if let Some(key) = &elem.key {
                    class_refs_in_expr(key, class_name, out);
                }
                class_refs_in_expr(&elem.value, class_name, out);
            }
        }
        ExprKind::Isset(exprs) => {
            for e in exprs.iter() {
                class_refs_in_expr(e, class_name, out);
            }
        }
        ExprKind::Include(_, e) => class_refs_in_expr(e, class_name, out),
        ExprKind::Exit(Some(e)) => class_refs_in_expr(e, class_name, out),
        ExprKind::Closure(c) => class_refs_in_stmts(&c.body, class_name, out),
        ExprKind::ArrowFunction(a) => class_refs_in_expr(a.body, class_name, out),
        ExprKind::AnonymousClass(c) => {
            for member in c.members.iter() {
                if let ClassMemberKind::Method(m) = &member.kind
                    && let Some(body) = &m.body
                {
                    class_refs_in_stmts(body, class_name, out);
                }
            }
        }
        _ => {}
    }
}

/// Walk a type hint and emit a span for any `Named` component that matches `class_name`.
fn collect_class_in_type_hint(th: &TypeHint<'_, '_>, class_name: &str, out: &mut Vec<Span>) {
    match &th.kind {
        TypeHintKind::Named(name) => {
            let repr = name.to_string_repr();
            let last = repr.rsplit('\\').next().unwrap_or(repr.as_ref());
            if last == class_name {
                let span = name.span();
                let offset = (repr.len() - last.len()) as u32;
                out.push(Span {
                    start: span.start + offset,
                    end: span.end,
                });
            }
        }
        TypeHintKind::Nullable(inner) => collect_class_in_type_hint(inner, class_name, out),
        TypeHintKind::Union(types) | TypeHintKind::Intersection(types) => {
            for t in types.iter() {
                collect_class_in_type_hint(t, class_name, out);
            }
        }
        TypeHintKind::Keyword(_, _) => {}
    }
}
