Skip to main content

php_ast/
visitor.rs

1use std::ops::ControlFlow;
2
3use crate::ast::*;
4
5/// Visitor trait for immutable AST traversal.
6///
7/// All methods return `ControlFlow<()>`:
8/// - `ControlFlow::Continue(())` — keep walking.
9/// - `ControlFlow::Break(())` — stop the entire traversal immediately.
10///
11/// Default implementations recursively walk child nodes, so implementors
12/// only need to override the node types they care about.
13///
14/// To **skip** a subtree, override the method and return `Continue(())`
15/// without calling the corresponding `walk_*` function.
16///
17/// # Example
18///
19/// ```
20/// use php_ast::visitor::{Visitor, walk_expr};
21/// use php_ast::ast::*;
22/// use std::ops::ControlFlow;
23///
24/// struct VarCounter { count: usize }
25///
26/// impl<'arena, 'src> Visitor<'arena, 'src> for VarCounter {
27///     fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
28///         if matches!(&expr.kind, ExprKind::Variable(_)) {
29///             self.count += 1;
30///         }
31///         walk_expr(self, expr)
32///     }
33/// }
34/// ```
35pub trait Visitor<'arena, 'src> {
36    fn visit_program(&mut self, program: &Program<'arena, 'src>) -> ControlFlow<()> {
37        walk_program(self, program)
38    }
39
40    fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
41        walk_stmt(self, stmt)
42    }
43
44    fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
45        walk_expr(self, expr)
46    }
47
48    fn visit_param(&mut self, param: &Param<'arena, 'src>) -> ControlFlow<()> {
49        walk_param(self, param)
50    }
51
52    fn visit_arg(&mut self, arg: &Arg<'arena, 'src>) -> ControlFlow<()> {
53        walk_arg(self, arg)
54    }
55
56    fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
57        walk_class_member(self, member)
58    }
59
60    fn visit_enum_member(&mut self, member: &EnumMember<'arena, 'src>) -> ControlFlow<()> {
61        walk_enum_member(self, member)
62    }
63
64    fn visit_property_hook(&mut self, hook: &PropertyHook<'arena, 'src>) -> ControlFlow<()> {
65        walk_property_hook(self, hook)
66    }
67
68    fn visit_type_hint(&mut self, type_hint: &TypeHint<'arena, 'src>) -> ControlFlow<()> {
69        walk_type_hint(self, type_hint)
70    }
71
72    fn visit_attribute(&mut self, attribute: &Attribute<'arena, 'src>) -> ControlFlow<()> {
73        walk_attribute(self, attribute)
74    }
75
76    fn visit_catch_clause(&mut self, catch: &CatchClause<'arena, 'src>) -> ControlFlow<()> {
77        walk_catch_clause(self, catch)
78    }
79
80    fn visit_match_arm(&mut self, arm: &MatchArm<'arena, 'src>) -> ControlFlow<()> {
81        walk_match_arm(self, arm)
82    }
83
84    fn visit_closure_use_var(&mut self, _var: &ClosureUseVar<'src>) -> ControlFlow<()> {
85        ControlFlow::Continue(())
86    }
87
88    fn visit_trait_use(&mut self, trait_use: &TraitUseDecl<'arena, 'src>) -> ControlFlow<()> {
89        walk_trait_use(self, trait_use)
90    }
91
92    fn visit_trait_adaptation(
93        &mut self,
94        _adaptation: &TraitAdaptation<'arena, 'src>,
95    ) -> ControlFlow<()> {
96        ControlFlow::Continue(())
97    }
98
99    fn visit_name(&mut self, _name: &Name<'arena, 'src>) -> ControlFlow<()> {
100        ControlFlow::Continue(())
101    }
102}
103
104// =============================================================================
105// Walk functions
106// =============================================================================
107
108/// Calls [`Visitor::visit_name`] on `name`.
109///
110/// The default [`Visitor::visit_name`] is a leaf — it does nothing and returns
111/// `Continue(())`. Override it if you need to inspect name nodes.
112pub fn walk_name<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
113    visitor: &mut V,
114    name: &Name<'arena, 'src>,
115) -> ControlFlow<()> {
116    visitor.visit_name(name)
117}
118
119/// Visits every top-level statement in `program` by calling [`Visitor::visit_stmt`].
120///
121/// This is the entry point for a full-file traversal. Call it from
122/// [`Visitor::visit_program`] (which is the default) or drive it directly:
123///
124/// ```
125/// # use php_ast::visitor::{Visitor, walk_program};
126/// # use php_ast::ast::*;
127/// # use std::ops::ControlFlow;
128/// # struct V;
129/// # impl<'a, 'b> Visitor<'a, 'b> for V {}
130/// # fn example<'a, 'b>(v: &mut V, program: &Program<'a, 'b>) {
131/// walk_program(v, program);
132/// # }
133/// ```
134pub fn walk_program<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
135    visitor: &mut V,
136    program: &Program<'arena, 'src>,
137) -> ControlFlow<()> {
138    for stmt in program.stmts.iter() {
139        visitor.visit_stmt(stmt)?;
140    }
141    ControlFlow::Continue(())
142}
143
144/// Dispatches `stmt` to the appropriate child visitors based on its [`StmtKind`].
145///
146/// Call this from [`Visitor::visit_stmt`] to recurse into a statement's children.
147/// Omit the call to skip the subtree entirely.
148pub fn walk_stmt<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
149    visitor: &mut V,
150    stmt: &Stmt<'arena, 'src>,
151) -> ControlFlow<()> {
152    match &stmt.kind {
153        StmtKind::Expression(expr) => {
154            visitor.visit_expr(expr)?;
155        }
156        StmtKind::Echo(exprs) => {
157            for expr in exprs.iter() {
158                visitor.visit_expr(expr)?;
159            }
160        }
161        StmtKind::Return(expr) => {
162            if let Some(expr) = expr {
163                visitor.visit_expr(expr)?;
164            }
165        }
166        StmtKind::Block(stmts) => {
167            for stmt in stmts.iter() {
168                visitor.visit_stmt(stmt)?;
169            }
170        }
171        StmtKind::If(if_stmt) => {
172            visitor.visit_expr(&if_stmt.condition)?;
173            visitor.visit_stmt(if_stmt.then_branch)?;
174            for elseif in if_stmt.elseif_branches.iter() {
175                visitor.visit_expr(&elseif.condition)?;
176                visitor.visit_stmt(&elseif.body)?;
177            }
178            if let Some(else_branch) = &if_stmt.else_branch {
179                visitor.visit_stmt(else_branch)?;
180            }
181        }
182        StmtKind::While(while_stmt) => {
183            visitor.visit_expr(&while_stmt.condition)?;
184            visitor.visit_stmt(while_stmt.body)?;
185        }
186        StmtKind::For(for_stmt) => {
187            for expr in for_stmt.init.iter() {
188                visitor.visit_expr(expr)?;
189            }
190            for expr in for_stmt.condition.iter() {
191                visitor.visit_expr(expr)?;
192            }
193            for expr in for_stmt.update.iter() {
194                visitor.visit_expr(expr)?;
195            }
196            visitor.visit_stmt(for_stmt.body)?;
197        }
198        StmtKind::Foreach(foreach_stmt) => {
199            visitor.visit_expr(&foreach_stmt.expr)?;
200            if let Some(key) = &foreach_stmt.key {
201                visitor.visit_expr(key)?;
202            }
203            visitor.visit_expr(&foreach_stmt.value)?;
204            visitor.visit_stmt(foreach_stmt.body)?;
205        }
206        StmtKind::DoWhile(do_while) => {
207            visitor.visit_stmt(do_while.body)?;
208            visitor.visit_expr(&do_while.condition)?;
209        }
210        StmtKind::Function(func) => {
211            walk_function_like(visitor, &func.attributes, &func.params, &func.return_type)?;
212            for stmt in func.body.iter() {
213                visitor.visit_stmt(stmt)?;
214            }
215        }
216        StmtKind::Break(expr) | StmtKind::Continue(expr) => {
217            if let Some(expr) = expr {
218                visitor.visit_expr(expr)?;
219            }
220        }
221        StmtKind::Switch(switch_stmt) => {
222            visitor.visit_expr(&switch_stmt.expr)?;
223            for case in switch_stmt.cases.iter() {
224                if let Some(value) = &case.value {
225                    visitor.visit_expr(value)?;
226                }
227                for stmt in case.body.iter() {
228                    visitor.visit_stmt(stmt)?;
229                }
230            }
231        }
232        StmtKind::Throw(expr) => {
233            visitor.visit_expr(expr)?;
234        }
235        StmtKind::TryCatch(tc) => {
236            for stmt in tc.body.iter() {
237                visitor.visit_stmt(stmt)?;
238            }
239            for catch in tc.catches.iter() {
240                visitor.visit_catch_clause(catch)?;
241            }
242            if let Some(finally) = &tc.finally {
243                for stmt in finally.iter() {
244                    visitor.visit_stmt(stmt)?;
245                }
246            }
247        }
248        StmtKind::Declare(decl) => {
249            for (_, expr) in decl.directives.iter() {
250                visitor.visit_expr(expr)?;
251            }
252            if let Some(body) = decl.body {
253                visitor.visit_stmt(body)?;
254            }
255        }
256        StmtKind::Unset(exprs) | StmtKind::Global(exprs) => {
257            for expr in exprs.iter() {
258                visitor.visit_expr(expr)?;
259            }
260        }
261        StmtKind::Class(class) => {
262            walk_attributes(visitor, &class.attributes)?;
263            if let Some(extends) = &class.extends {
264                visitor.visit_name(extends)?;
265            }
266            for name in class.implements.iter() {
267                visitor.visit_name(name)?;
268            }
269            for member in class.members.iter() {
270                visitor.visit_class_member(member)?;
271            }
272        }
273        StmtKind::Interface(iface) => {
274            walk_attributes(visitor, &iface.attributes)?;
275            for name in iface.extends.iter() {
276                visitor.visit_name(name)?;
277            }
278            for member in iface.members.iter() {
279                visitor.visit_class_member(member)?;
280            }
281        }
282        StmtKind::Trait(trait_decl) => {
283            walk_attributes(visitor, &trait_decl.attributes)?;
284            for member in trait_decl.members.iter() {
285                visitor.visit_class_member(member)?;
286            }
287        }
288        StmtKind::Enum(enum_decl) => {
289            walk_attributes(visitor, &enum_decl.attributes)?;
290            if let Some(scalar_type) = &enum_decl.scalar_type {
291                visitor.visit_name(scalar_type)?;
292            }
293            for name in enum_decl.implements.iter() {
294                visitor.visit_name(name)?;
295            }
296            for member in enum_decl.members.iter() {
297                visitor.visit_enum_member(member)?;
298            }
299        }
300        StmtKind::Namespace(ns) => {
301            if let NamespaceBody::Braced(stmts) = &ns.body {
302                for stmt in stmts.iter() {
303                    visitor.visit_stmt(stmt)?;
304                }
305            }
306        }
307        StmtKind::Const(items) => {
308            for item in items.iter() {
309                walk_attributes(visitor, &item.attributes)?;
310                visitor.visit_expr(&item.value)?;
311            }
312        }
313        StmtKind::StaticVar(vars) => {
314            for var in vars.iter() {
315                if let Some(default) = &var.default {
316                    visitor.visit_expr(default)?;
317                }
318            }
319        }
320        StmtKind::Use(decl) => {
321            for item in decl.uses.iter() {
322                visitor.visit_name(&item.name)?;
323            }
324        }
325        StmtKind::Goto(_)
326        | StmtKind::Label(_)
327        | StmtKind::Nop
328        | StmtKind::InlineHtml(_)
329        | StmtKind::HaltCompiler(_)
330        | StmtKind::Error => {}
331    }
332    ControlFlow::Continue(())
333}
334
335/// Dispatches `expr` to the appropriate child visitors based on its [`ExprKind`].
336///
337/// Call this from [`Visitor::visit_expr`] to recurse into an expression's children.
338/// Omit the call to skip the subtree entirely.
339pub fn walk_expr<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
340    visitor: &mut V,
341    expr: &Expr<'arena, 'src>,
342) -> ControlFlow<()> {
343    match &expr.kind {
344        ExprKind::Assign(assign) => {
345            visitor.visit_expr(assign.target)?;
346            visitor.visit_expr(assign.value)?;
347        }
348        ExprKind::Binary(binary) => {
349            visitor.visit_expr(binary.left)?;
350            visitor.visit_expr(binary.right)?;
351        }
352        ExprKind::UnaryPrefix(unary) => {
353            visitor.visit_expr(unary.operand)?;
354        }
355        ExprKind::UnaryPostfix(unary) => {
356            visitor.visit_expr(unary.operand)?;
357        }
358        ExprKind::Ternary(ternary) => {
359            visitor.visit_expr(ternary.condition)?;
360            if let Some(then_expr) = &ternary.then_expr {
361                visitor.visit_expr(then_expr)?;
362            }
363            visitor.visit_expr(ternary.else_expr)?;
364        }
365        ExprKind::NullCoalesce(nc) => {
366            visitor.visit_expr(nc.left)?;
367            visitor.visit_expr(nc.right)?;
368        }
369        ExprKind::FunctionCall(call) => {
370            visitor.visit_expr(call.name)?;
371            for arg in call.args.iter() {
372                visitor.visit_arg(arg)?;
373            }
374        }
375        ExprKind::Array(elements) => {
376            for elem in elements.iter() {
377                if let Some(key) = &elem.key {
378                    visitor.visit_expr(key)?;
379                }
380                visitor.visit_expr(&elem.value)?;
381            }
382        }
383        ExprKind::ArrayAccess(access) => {
384            visitor.visit_expr(access.array)?;
385            if let Some(index) = &access.index {
386                visitor.visit_expr(index)?;
387            }
388        }
389        ExprKind::Print(expr) => {
390            visitor.visit_expr(expr)?;
391        }
392        ExprKind::Parenthesized(expr) => {
393            visitor.visit_expr(expr)?;
394        }
395        ExprKind::Cast(_, expr) => {
396            visitor.visit_expr(expr)?;
397        }
398        ExprKind::ErrorSuppress(expr) => {
399            visitor.visit_expr(expr)?;
400        }
401        ExprKind::Isset(exprs) => {
402            for expr in exprs.iter() {
403                visitor.visit_expr(expr)?;
404            }
405        }
406        ExprKind::Empty(expr) => {
407            visitor.visit_expr(expr)?;
408        }
409        ExprKind::Include(_, expr) => {
410            visitor.visit_expr(expr)?;
411        }
412        ExprKind::Eval(expr) => {
413            visitor.visit_expr(expr)?;
414        }
415        ExprKind::Exit(expr) => {
416            if let Some(expr) = expr {
417                visitor.visit_expr(expr)?;
418            }
419        }
420        ExprKind::Clone(expr) => {
421            visitor.visit_expr(expr)?;
422        }
423        ExprKind::CloneWith(object, overrides) => {
424            visitor.visit_expr(object)?;
425            visitor.visit_expr(overrides)?;
426        }
427        ExprKind::New(new_expr) => {
428            visitor.visit_expr(new_expr.class)?;
429            for arg in new_expr.args.iter() {
430                visitor.visit_arg(arg)?;
431            }
432        }
433        ExprKind::PropertyAccess(access) | ExprKind::NullsafePropertyAccess(access) => {
434            visitor.visit_expr(access.object)?;
435            visitor.visit_expr(access.property)?;
436        }
437        ExprKind::MethodCall(call) | ExprKind::NullsafeMethodCall(call) => {
438            visitor.visit_expr(call.object)?;
439            visitor.visit_expr(call.method)?;
440            for arg in call.args.iter() {
441                visitor.visit_arg(arg)?;
442            }
443        }
444        ExprKind::StaticPropertyAccess(access) | ExprKind::ClassConstAccess(access) => {
445            visitor.visit_expr(access.class)?;
446            visitor.visit_expr(access.member)?;
447        }
448        ExprKind::ClassConstAccessDynamic { class, member }
449        | ExprKind::StaticPropertyAccessDynamic { class, member } => {
450            visitor.visit_expr(class)?;
451            visitor.visit_expr(member)?;
452        }
453        ExprKind::StaticMethodCall(call) => {
454            visitor.visit_expr(call.class)?;
455            visitor.visit_expr(call.method)?;
456            for arg in call.args.iter() {
457                visitor.visit_arg(arg)?;
458            }
459        }
460        ExprKind::StaticDynMethodCall(call) => {
461            visitor.visit_expr(call.class)?;
462            visitor.visit_expr(call.method)?;
463            for arg in call.args.iter() {
464                visitor.visit_arg(arg)?;
465            }
466        }
467        ExprKind::Closure(closure) => {
468            walk_function_like(
469                visitor,
470                &closure.attributes,
471                &closure.params,
472                &closure.return_type,
473            )?;
474            for use_var in closure.use_vars.iter() {
475                visitor.visit_closure_use_var(use_var)?;
476            }
477            for stmt in closure.body.iter() {
478                visitor.visit_stmt(stmt)?;
479            }
480        }
481        ExprKind::ArrowFunction(arrow) => {
482            walk_function_like(
483                visitor,
484                &arrow.attributes,
485                &arrow.params,
486                &arrow.return_type,
487            )?;
488            visitor.visit_expr(arrow.body)?;
489        }
490        ExprKind::Match(match_expr) => {
491            visitor.visit_expr(match_expr.subject)?;
492            for arm in match_expr.arms.iter() {
493                visitor.visit_match_arm(arm)?;
494            }
495        }
496        ExprKind::ThrowExpr(expr) => {
497            visitor.visit_expr(expr)?;
498        }
499        ExprKind::Yield(yield_expr) => {
500            if let Some(key) = &yield_expr.key {
501                visitor.visit_expr(key)?;
502            }
503            if let Some(value) = &yield_expr.value {
504                visitor.visit_expr(value)?;
505            }
506        }
507        ExprKind::AnonymousClass(class) => {
508            walk_attributes(visitor, &class.attributes)?;
509            for member in class.members.iter() {
510                visitor.visit_class_member(member)?;
511            }
512        }
513        ExprKind::InterpolatedString(parts)
514        | ExprKind::Heredoc { parts, .. }
515        | ExprKind::ShellExec(parts) => {
516            for part in parts.iter() {
517                if let StringPart::Expr(e) = part {
518                    visitor.visit_expr(e)?;
519                }
520            }
521        }
522        ExprKind::VariableVariable(inner) => {
523            visitor.visit_expr(inner)?;
524        }
525        ExprKind::CallableCreate(cc) => match &cc.kind {
526            CallableCreateKind::Function(name) => visitor.visit_expr(name)?,
527            CallableCreateKind::Method { object, method }
528            | CallableCreateKind::NullsafeMethod { object, method } => {
529                visitor.visit_expr(object)?;
530                visitor.visit_expr(method)?;
531            }
532            CallableCreateKind::StaticMethod { class, method } => {
533                visitor.visit_expr(class)?;
534                visitor.visit_expr(method)?;
535            }
536        },
537        ExprKind::Int(_)
538        | ExprKind::Float(_)
539        | ExprKind::String(_)
540        | ExprKind::Bool(_)
541        | ExprKind::Null
542        | ExprKind::Omit
543        | ExprKind::Variable(_)
544        | ExprKind::Identifier(_)
545        | ExprKind::MagicConst(_)
546        | ExprKind::Nowdoc { .. }
547        | ExprKind::Error => {}
548    }
549    ControlFlow::Continue(())
550}
551
552/// Visits a function/method parameter's attributes, type hint, default expression, and property hooks.
553pub fn walk_param<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
554    visitor: &mut V,
555    param: &Param<'arena, 'src>,
556) -> ControlFlow<()> {
557    walk_attributes(visitor, &param.attributes)?;
558    if let Some(type_hint) = &param.type_hint {
559        visitor.visit_type_hint(type_hint)?;
560    }
561    if let Some(default) = &param.default {
562        visitor.visit_expr(default)?;
563    }
564    for hook in param.hooks.iter() {
565        visitor.visit_property_hook(hook)?;
566    }
567    ControlFlow::Continue(())
568}
569
570/// Visits the value expression of a call argument.
571pub fn walk_arg<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
572    visitor: &mut V,
573    arg: &Arg<'arena, 'src>,
574) -> ControlFlow<()> {
575    visitor.visit_expr(&arg.value)
576}
577
578/// Dispatches a class member (property, method, constant, or trait use) to its child visitors.
579pub fn walk_class_member<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
580    visitor: &mut V,
581    member: &ClassMember<'arena, 'src>,
582) -> ControlFlow<()> {
583    match &member.kind {
584        ClassMemberKind::Property(prop) => {
585            walk_property_decl(visitor, prop)?;
586        }
587        ClassMemberKind::Method(method) => {
588            walk_method_decl(visitor, method)?;
589        }
590        ClassMemberKind::ClassConst(cc) => {
591            walk_class_const_decl(visitor, cc)?;
592        }
593        ClassMemberKind::TraitUse(trait_use) => {
594            visitor.visit_trait_use(trait_use)?;
595        }
596    }
597    ControlFlow::Continue(())
598}
599
600/// Visits a property hook's attributes, parameters, and body statements or expression.
601pub fn walk_property_hook<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
602    visitor: &mut V,
603    hook: &PropertyHook<'arena, 'src>,
604) -> ControlFlow<()> {
605    walk_attributes(visitor, &hook.attributes)?;
606    for param in hook.params.iter() {
607        visitor.visit_param(param)?;
608    }
609    match &hook.body {
610        PropertyHookBody::Block(stmts) => {
611            for stmt in stmts.iter() {
612                visitor.visit_stmt(stmt)?;
613            }
614        }
615        PropertyHookBody::Expression(expr) => {
616            visitor.visit_expr(expr)?;
617        }
618        PropertyHookBody::Abstract => {}
619    }
620    ControlFlow::Continue(())
621}
622
623/// Dispatches an enum member (case, method, constant, or trait use) to its child visitors.
624pub fn walk_enum_member<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
625    visitor: &mut V,
626    member: &EnumMember<'arena, 'src>,
627) -> ControlFlow<()> {
628    match &member.kind {
629        EnumMemberKind::Case(case) => {
630            walk_attributes(visitor, &case.attributes)?;
631            if let Some(value) = &case.value {
632                visitor.visit_expr(value)?;
633            }
634        }
635        EnumMemberKind::Method(method) => {
636            walk_method_decl(visitor, method)?;
637        }
638        EnumMemberKind::ClassConst(cc) => {
639            walk_class_const_decl(visitor, cc)?;
640        }
641        EnumMemberKind::TraitUse(trait_use) => {
642            visitor.visit_trait_use(trait_use)?;
643        }
644    }
645    ControlFlow::Continue(())
646}
647
648/// Visits the inner types of a type hint (recursing into nullable, union, and intersection).
649pub fn walk_type_hint<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
650    visitor: &mut V,
651    type_hint: &TypeHint<'arena, 'src>,
652) -> ControlFlow<()> {
653    match &type_hint.kind {
654        TypeHintKind::Nullable(inner) => {
655            visitor.visit_type_hint(inner)?;
656        }
657        TypeHintKind::Union(types) | TypeHintKind::Intersection(types) => {
658            for ty in types.iter() {
659                visitor.visit_type_hint(ty)?;
660            }
661        }
662        TypeHintKind::Named(name) => {
663            visitor.visit_name(name)?;
664        }
665        TypeHintKind::Keyword(_, _) => {}
666    }
667    ControlFlow::Continue(())
668}
669
670/// Visits an attribute's name and argument expressions.
671pub fn walk_attribute<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
672    visitor: &mut V,
673    attribute: &Attribute<'arena, 'src>,
674) -> ControlFlow<()> {
675    visitor.visit_name(&attribute.name)?;
676    for arg in attribute.args.iter() {
677        visitor.visit_arg(arg)?;
678    }
679    ControlFlow::Continue(())
680}
681
682/// Visits a catch clause's caught type names and body statements.
683pub fn walk_catch_clause<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
684    visitor: &mut V,
685    catch: &CatchClause<'arena, 'src>,
686) -> ControlFlow<()> {
687    for ty in catch.types.iter() {
688        visitor.visit_name(ty)?;
689    }
690    for stmt in catch.body.iter() {
691        visitor.visit_stmt(stmt)?;
692    }
693    ControlFlow::Continue(())
694}
695
696/// Visits a match arm's condition expressions (if any) and body expression.
697pub fn walk_match_arm<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
698    visitor: &mut V,
699    arm: &MatchArm<'arena, 'src>,
700) -> ControlFlow<()> {
701    if let Some(conditions) = &arm.conditions {
702        for cond in conditions.iter() {
703            visitor.visit_expr(cond)?;
704        }
705    }
706    visitor.visit_expr(&arm.body)
707}
708
709/// Visits a trait use declaration's trait names and adaptations (`insteadof`, `as`).
710pub fn walk_trait_use<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
711    visitor: &mut V,
712    trait_use: &TraitUseDecl<'arena, 'src>,
713) -> ControlFlow<()> {
714    for name in trait_use.traits.iter() {
715        visitor.visit_name(name)?;
716    }
717    for adaptation in trait_use.adaptations.iter() {
718        visitor.visit_trait_adaptation(adaptation)?;
719    }
720    ControlFlow::Continue(())
721}
722
723// =============================================================================
724// Internal helpers — shared walking logic to avoid duplication
725// =============================================================================
726
727/// Walks the common parts of any function-like construct:
728/// attributes → params → optional return type.
729fn walk_function_like<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
730    visitor: &mut V,
731    attributes: &[Attribute<'arena, 'src>],
732    params: &[Param<'arena, 'src>],
733    return_type: &Option<TypeHint<'arena, 'src>>,
734) -> ControlFlow<()> {
735    walk_attributes(visitor, attributes)?;
736    for param in params.iter() {
737        visitor.visit_param(param)?;
738    }
739    if let Some(ret) = return_type {
740        visitor.visit_type_hint(ret)?;
741    }
742    ControlFlow::Continue(())
743}
744
745/// Walks a method declaration (shared by ClassMember and EnumMember).
746fn walk_method_decl<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
747    visitor: &mut V,
748    method: &MethodDecl<'arena, 'src>,
749) -> ControlFlow<()> {
750    walk_function_like(
751        visitor,
752        &method.attributes,
753        &method.params,
754        &method.return_type,
755    )?;
756    if let Some(body) = &method.body {
757        for stmt in body.iter() {
758            visitor.visit_stmt(stmt)?;
759        }
760    }
761    ControlFlow::Continue(())
762}
763
764/// Walks a class constant declaration (shared by ClassMember and EnumMember).
765fn walk_class_const_decl<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
766    visitor: &mut V,
767    cc: &ClassConstDecl<'arena, 'src>,
768) -> ControlFlow<()> {
769    walk_attributes(visitor, &cc.attributes)?;
770    if let Some(type_hint) = &cc.type_hint {
771        visitor.visit_type_hint(type_hint)?;
772    }
773    visitor.visit_expr(&cc.value)
774}
775
776/// Walks a property declaration.
777fn walk_property_decl<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
778    visitor: &mut V,
779    prop: &PropertyDecl<'arena, 'src>,
780) -> ControlFlow<()> {
781    walk_attributes(visitor, &prop.attributes)?;
782    if let Some(type_hint) = &prop.type_hint {
783        visitor.visit_type_hint(type_hint)?;
784    }
785    if let Some(default) = &prop.default {
786        visitor.visit_expr(default)?;
787    }
788    for hook in prop.hooks.iter() {
789        visitor.visit_property_hook(hook)?;
790    }
791    ControlFlow::Continue(())
792}
793
794fn walk_attributes<'arena, 'src, V: Visitor<'arena, 'src> + ?Sized>(
795    visitor: &mut V,
796    attributes: &[Attribute<'arena, 'src>],
797) -> ControlFlow<()> {
798    for attr in attributes.iter() {
799        visitor.visit_attribute(attr)?;
800    }
801    ControlFlow::Continue(())
802}
803
804// =============================================================================
805// ScopeVisitor — scope-aware traversal
806// =============================================================================
807
808/// Lexical scope context passed to each [`ScopeVisitor`] method.
809///
810/// Represents the immediately enclosing namespace, class-like definition,
811/// and named function or method at the point a node is visited.
812/// All fields are `None` when the node is at the global top level.
813///
814/// **Namespace** is set when inside a braced or simple `namespace` declaration.
815/// **`class_name`** is set inside `class`, `interface`, `trait`, and `enum`
816/// declarations; it is `None` for anonymous classes.
817/// **`function_name`** is set inside named functions and methods; it is `None`
818/// inside closures and arrow functions.
819#[derive(Debug, Clone, Copy, Default)]
820pub struct Scope<'src> {
821    /// Current namespace, or `None` for the global namespace.
822    ///
823    /// This is a borrowed slice of the original source string, so copying or
824    /// cloning the scope is always allocation-free.
825    pub namespace: Option<&'src str>,
826    /// Name of the immediately enclosing class-like declaration, or `None`.
827    pub class_name: Option<&'src str>,
828    /// Name of the immediately enclosing named function or method, or `None`.
829    pub function_name: Option<&'src str>,
830}
831
832/// A scope-aware variant of [`Visitor`].
833///
834/// Every visit method receives a [`Scope`] describing the lexical context at
835/// that node — the current namespace, enclosing class-like declaration, and
836/// enclosing named function or method.  All methods have no-op default
837/// implementations, so implementors override only what they need.
838///
839/// Drive traversal with [`ScopeWalker`], which maintains the scope
840/// automatically and calls these methods with the current context.
841///
842/// # Example
843///
844/// ```
845/// use php_ast::visitor::{ScopeVisitor, ScopeWalker, Scope};
846/// use php_ast::ast::*;
847/// use std::ops::ControlFlow;
848///
849/// struct MethodCollector { methods: Vec<String> }
850///
851/// impl<'arena, 'src> ScopeVisitor<'arena, 'src> for MethodCollector {
852///     fn visit_class_member(
853///         &mut self,
854///         member: &ClassMember<'arena, 'src>,
855///         scope: &Scope<'src>,
856///     ) -> ControlFlow<()> {
857///         if let ClassMemberKind::Method(m) = &member.kind {
858///             self.methods.push(format!(
859///                 "{}::{}",
860///                 scope.class_name.unwrap_or("<anon>"),
861///                 m.name
862///             ));
863///         }
864///         ControlFlow::Continue(())
865///     }
866/// }
867/// ```
868pub trait ScopeVisitor<'arena, 'src> {
869    fn visit_program(
870        &mut self,
871        _program: &Program<'arena, 'src>,
872        _scope: &Scope<'src>,
873    ) -> ControlFlow<()> {
874        ControlFlow::Continue(())
875    }
876    fn visit_stmt(&mut self, _stmt: &Stmt<'arena, 'src>, _scope: &Scope<'src>) -> ControlFlow<()> {
877        ControlFlow::Continue(())
878    }
879    fn visit_expr(&mut self, _expr: &Expr<'arena, 'src>, _scope: &Scope<'src>) -> ControlFlow<()> {
880        ControlFlow::Continue(())
881    }
882    fn visit_param(
883        &mut self,
884        _param: &Param<'arena, 'src>,
885        _scope: &Scope<'src>,
886    ) -> ControlFlow<()> {
887        ControlFlow::Continue(())
888    }
889    fn visit_arg(&mut self, _arg: &Arg<'arena, 'src>, _scope: &Scope<'src>) -> ControlFlow<()> {
890        ControlFlow::Continue(())
891    }
892    fn visit_class_member(
893        &mut self,
894        _member: &ClassMember<'arena, 'src>,
895        _scope: &Scope<'src>,
896    ) -> ControlFlow<()> {
897        ControlFlow::Continue(())
898    }
899    fn visit_enum_member(
900        &mut self,
901        _member: &EnumMember<'arena, 'src>,
902        _scope: &Scope<'src>,
903    ) -> ControlFlow<()> {
904        ControlFlow::Continue(())
905    }
906    fn visit_property_hook(
907        &mut self,
908        _hook: &PropertyHook<'arena, 'src>,
909        _scope: &Scope<'src>,
910    ) -> ControlFlow<()> {
911        ControlFlow::Continue(())
912    }
913    fn visit_type_hint(
914        &mut self,
915        _type_hint: &TypeHint<'arena, 'src>,
916        _scope: &Scope<'src>,
917    ) -> ControlFlow<()> {
918        ControlFlow::Continue(())
919    }
920    fn visit_attribute(
921        &mut self,
922        _attribute: &Attribute<'arena, 'src>,
923        _scope: &Scope<'src>,
924    ) -> ControlFlow<()> {
925        ControlFlow::Continue(())
926    }
927    fn visit_catch_clause(
928        &mut self,
929        _catch: &CatchClause<'arena, 'src>,
930        _scope: &Scope<'src>,
931    ) -> ControlFlow<()> {
932        ControlFlow::Continue(())
933    }
934    fn visit_match_arm(
935        &mut self,
936        _arm: &MatchArm<'arena, 'src>,
937        _scope: &Scope<'src>,
938    ) -> ControlFlow<()> {
939        ControlFlow::Continue(())
940    }
941    fn visit_closure_use_var(
942        &mut self,
943        _var: &ClosureUseVar<'src>,
944        _scope: &Scope<'src>,
945    ) -> ControlFlow<()> {
946        ControlFlow::Continue(())
947    }
948
949    fn visit_trait_use(
950        &mut self,
951        _trait_use: &TraitUseDecl<'arena, 'src>,
952        _scope: &Scope<'src>,
953    ) -> ControlFlow<()> {
954        ControlFlow::Continue(())
955    }
956
957    fn visit_trait_adaptation(
958        &mut self,
959        _adaptation: &TraitAdaptation<'arena, 'src>,
960        _scope: &Scope<'src>,
961    ) -> ControlFlow<()> {
962        ControlFlow::Continue(())
963    }
964}
965
966/// Drives a [`ScopeVisitor`] over an AST, maintaining [`Scope`] automatically.
967///
968/// `ScopeWalker` wraps a [`ScopeVisitor`] and tracks the lexical scope as it
969/// descends the tree, updating scope before visiting children and restoring it
970/// on exit from scope-defining nodes (functions, classes, namespaces).
971///
972/// # Usage
973///
974/// ```no_run
975/// # use php_ast::visitor::{ScopeWalker, ScopeVisitor, Scope};
976/// # use php_ast::ast::*;
977/// # use std::ops::ControlFlow;
978/// # struct MyVisitor;
979/// # impl<'a, 'b> ScopeVisitor<'a, 'b> for MyVisitor {}
980/// # fn parse<'a, 'b>(_: &'a bumpalo::Bump, _: &'b str) -> Program<'a, 'b> { unimplemented!() }
981/// let arena = bumpalo::Bump::new();
982/// let src = "<?php class Foo { public function bar() {} }";
983/// let program = parse(&arena, src);
984/// let mut walker = ScopeWalker::new(src, MyVisitor);
985/// walker.walk(&program);
986/// let _my_visitor = walker.into_inner();
987/// ```
988pub struct ScopeWalker<'src, V> {
989    inner: V,
990    scope: Scope<'src>,
991    src: &'src str,
992}
993
994impl<'src, V> ScopeWalker<'src, V> {
995    /// Creates a new `ScopeWalker` wrapping `inner`.
996    ///
997    /// `src` must be the same source string that was passed to the parser that
998    /// produced the [`Program`] you will walk.  It is used to derive
999    /// zero-allocation [`Scope::namespace`] slices for qualified namespace
1000    /// names (e.g. `Foo\Bar`).
1001    pub fn new(src: &'src str, inner: V) -> Self {
1002        Self {
1003            inner,
1004            scope: Scope::default(),
1005            src,
1006        }
1007    }
1008
1009    /// Consumes the walker and returns the inner visitor.
1010    pub fn into_inner(self) -> V {
1011        self.inner
1012    }
1013
1014    /// Returns a reference to the inner visitor.
1015    pub fn inner(&self) -> &V {
1016        &self.inner
1017    }
1018
1019    /// Returns a mutable reference to the inner visitor.
1020    pub fn inner_mut(&mut self) -> &mut V {
1021        &mut self.inner
1022    }
1023}
1024
1025impl<'arena, 'src, V: ScopeVisitor<'arena, 'src>> ScopeWalker<'src, V> {
1026    /// Walks `program`, calling [`ScopeVisitor`] methods with scope context.
1027    pub fn walk(&mut self, program: &Program<'arena, 'src>) -> ControlFlow<()> {
1028        self.visit_program(program)
1029    }
1030}
1031
1032impl<'arena, 'src, V: ScopeVisitor<'arena, 'src>> Visitor<'arena, 'src> for ScopeWalker<'src, V> {
1033    fn visit_program(&mut self, program: &Program<'arena, 'src>) -> ControlFlow<()> {
1034        self.inner.visit_program(program, &self.scope)?;
1035        walk_program(self, program)
1036    }
1037
1038    fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
1039        self.inner.visit_stmt(stmt, &self.scope)?;
1040        match &stmt.kind {
1041            StmtKind::Function(func) => {
1042                let prev_fn = std::mem::replace(&mut self.scope.function_name, func.name.as_str());
1043                walk_stmt(self, stmt)?;
1044                self.scope.function_name = prev_fn;
1045            }
1046            StmtKind::Class(class) => {
1047                let prev_class = self.scope.class_name;
1048                let prev_fn = self.scope.function_name.take();
1049                self.scope.class_name = class.name.and_then(|n| n.as_str());
1050                walk_stmt(self, stmt)?;
1051                self.scope.class_name = prev_class;
1052                self.scope.function_name = prev_fn;
1053            }
1054            StmtKind::Interface(iface) => {
1055                let prev_class = std::mem::replace(&mut self.scope.class_name, iface.name.as_str());
1056                let prev_fn = self.scope.function_name.take();
1057                walk_stmt(self, stmt)?;
1058                self.scope.class_name = prev_class;
1059                self.scope.function_name = prev_fn;
1060            }
1061            StmtKind::Trait(trait_decl) => {
1062                let prev_class =
1063                    std::mem::replace(&mut self.scope.class_name, trait_decl.name.as_str());
1064                let prev_fn = self.scope.function_name.take();
1065                walk_stmt(self, stmt)?;
1066                self.scope.class_name = prev_class;
1067                self.scope.function_name = prev_fn;
1068            }
1069            StmtKind::Enum(enum_decl) => {
1070                let prev_class =
1071                    std::mem::replace(&mut self.scope.class_name, enum_decl.name.as_str());
1072                let prev_fn = self.scope.function_name.take();
1073                walk_stmt(self, stmt)?;
1074                self.scope.class_name = prev_class;
1075                self.scope.function_name = prev_fn;
1076            }
1077            StmtKind::Namespace(ns) => {
1078                let ns_str = ns.name.as_ref().map(|n| n.src_repr(self.src));
1079                match &ns.body {
1080                    NamespaceBody::Braced(_) => {
1081                        let prev_ns = self.scope.namespace;
1082                        let prev_class = self.scope.class_name.take();
1083                        let prev_fn = self.scope.function_name.take();
1084                        self.scope.namespace = ns_str;
1085                        walk_stmt(self, stmt)?;
1086                        self.scope.namespace = prev_ns;
1087                        self.scope.class_name = prev_class;
1088                        self.scope.function_name = prev_fn;
1089                    }
1090                    NamespaceBody::Simple => {
1091                        // Simple namespace: update scope and leave it set for
1092                        // the remainder of the file (no push/pop).
1093                        self.scope.namespace = ns_str;
1094                        self.scope.class_name = None;
1095                        self.scope.function_name = None;
1096                    }
1097                }
1098            }
1099            _ => {
1100                walk_stmt(self, stmt)?;
1101            }
1102        }
1103        ControlFlow::Continue(())
1104    }
1105
1106    fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
1107        self.inner.visit_expr(expr, &self.scope)?;
1108        match &expr.kind {
1109            ExprKind::Closure(_) | ExprKind::ArrowFunction(_) => {
1110                let prev_fn = self.scope.function_name.take();
1111                walk_expr(self, expr)?;
1112                self.scope.function_name = prev_fn;
1113            }
1114            ExprKind::AnonymousClass(_) => {
1115                let prev_class = self.scope.class_name.take();
1116                let prev_fn = self.scope.function_name.take();
1117                walk_expr(self, expr)?;
1118                self.scope.class_name = prev_class;
1119                self.scope.function_name = prev_fn;
1120            }
1121            _ => {
1122                walk_expr(self, expr)?;
1123            }
1124        }
1125        ControlFlow::Continue(())
1126    }
1127
1128    fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
1129        self.inner.visit_class_member(member, &self.scope)?;
1130        if let ClassMemberKind::Method(method) = &member.kind {
1131            let prev_fn = std::mem::replace(&mut self.scope.function_name, method.name.as_str());
1132            walk_class_member(self, member)?;
1133            self.scope.function_name = prev_fn;
1134        } else {
1135            walk_class_member(self, member)?;
1136        }
1137        ControlFlow::Continue(())
1138    }
1139
1140    fn visit_enum_member(&mut self, member: &EnumMember<'arena, 'src>) -> ControlFlow<()> {
1141        self.inner.visit_enum_member(member, &self.scope)?;
1142        if let EnumMemberKind::Method(method) = &member.kind {
1143            let prev_fn = std::mem::replace(&mut self.scope.function_name, method.name.as_str());
1144            walk_enum_member(self, member)?;
1145            self.scope.function_name = prev_fn;
1146        } else {
1147            walk_enum_member(self, member)?;
1148        }
1149        ControlFlow::Continue(())
1150    }
1151
1152    fn visit_param(&mut self, param: &Param<'arena, 'src>) -> ControlFlow<()> {
1153        self.inner.visit_param(param, &self.scope)?;
1154        walk_param(self, param)
1155    }
1156
1157    fn visit_arg(&mut self, arg: &Arg<'arena, 'src>) -> ControlFlow<()> {
1158        self.inner.visit_arg(arg, &self.scope)?;
1159        walk_arg(self, arg)
1160    }
1161
1162    fn visit_property_hook(&mut self, hook: &PropertyHook<'arena, 'src>) -> ControlFlow<()> {
1163        self.inner.visit_property_hook(hook, &self.scope)?;
1164        walk_property_hook(self, hook)
1165    }
1166
1167    fn visit_type_hint(&mut self, type_hint: &TypeHint<'arena, 'src>) -> ControlFlow<()> {
1168        self.inner.visit_type_hint(type_hint, &self.scope)?;
1169        walk_type_hint(self, type_hint)
1170    }
1171
1172    fn visit_attribute(&mut self, attribute: &Attribute<'arena, 'src>) -> ControlFlow<()> {
1173        self.inner.visit_attribute(attribute, &self.scope)?;
1174        walk_attribute(self, attribute)
1175    }
1176
1177    fn visit_catch_clause(&mut self, catch: &CatchClause<'arena, 'src>) -> ControlFlow<()> {
1178        self.inner.visit_catch_clause(catch, &self.scope)?;
1179        walk_catch_clause(self, catch)
1180    }
1181
1182    fn visit_match_arm(&mut self, arm: &MatchArm<'arena, 'src>) -> ControlFlow<()> {
1183        self.inner.visit_match_arm(arm, &self.scope)?;
1184        walk_match_arm(self, arm)
1185    }
1186
1187    fn visit_closure_use_var(&mut self, var: &ClosureUseVar<'src>) -> ControlFlow<()> {
1188        self.inner.visit_closure_use_var(var, &self.scope)
1189    }
1190
1191    fn visit_trait_use(&mut self, trait_use: &TraitUseDecl<'arena, 'src>) -> ControlFlow<()> {
1192        self.inner.visit_trait_use(trait_use, &self.scope)?;
1193        walk_trait_use(self, trait_use)
1194    }
1195
1196    fn visit_trait_adaptation(
1197        &mut self,
1198        adaptation: &TraitAdaptation<'arena, 'src>,
1199    ) -> ControlFlow<()> {
1200        self.inner.visit_trait_adaptation(adaptation, &self.scope)
1201    }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206    use super::*;
1207    use crate::Span;
1208    // =========================================================================
1209    // Unit tests with hand-built ASTs
1210    // =========================================================================
1211
1212    struct VarCounter {
1213        count: usize,
1214    }
1215
1216    impl<'arena, 'src> Visitor<'arena, 'src> for VarCounter {
1217        fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
1218            if matches!(&expr.kind, ExprKind::Variable(_)) {
1219                self.count += 1;
1220            }
1221            walk_expr(self, expr)
1222        }
1223    }
1224
1225    #[test]
1226    fn counts_variables() {
1227        let arena = bumpalo::Bump::new();
1228        let var_x = arena.alloc(Expr {
1229            kind: ExprKind::Variable(NameStr::Src("x")),
1230            span: Span::DUMMY,
1231        });
1232        let var_y = arena.alloc(Expr {
1233            kind: ExprKind::Variable(NameStr::Src("y")),
1234            span: Span::DUMMY,
1235        });
1236        let var_z = arena.alloc(Expr {
1237            kind: ExprKind::Variable(NameStr::Src("z")),
1238            span: Span::DUMMY,
1239        });
1240        let binary = arena.alloc(Expr {
1241            kind: ExprKind::Binary(BinaryExpr {
1242                left: var_y,
1243                op: BinaryOp::Add,
1244                right: var_z,
1245            }),
1246            span: Span::DUMMY,
1247        });
1248        let assign = arena.alloc(Expr {
1249            kind: ExprKind::Assign(AssignExpr {
1250                target: var_x,
1251                op: AssignOp::Assign,
1252                value: binary,
1253                by_ref: false,
1254            }),
1255            span: Span::DUMMY,
1256        });
1257        let mut stmts = ArenaVec::new_in(&arena);
1258        stmts.push(Stmt {
1259            kind: StmtKind::Expression(assign),
1260            span: Span::DUMMY,
1261        });
1262        let program = Program {
1263            stmts,
1264            span: Span::DUMMY,
1265        };
1266
1267        let mut v = VarCounter { count: 0 };
1268        let _ = v.visit_program(&program);
1269        assert_eq!(v.count, 3);
1270    }
1271
1272    #[test]
1273    fn early_termination() {
1274        let arena = bumpalo::Bump::new();
1275        let var_a = arena.alloc(Expr {
1276            kind: ExprKind::Variable(NameStr::Src("a")),
1277            span: Span::DUMMY,
1278        });
1279        let var_b = arena.alloc(Expr {
1280            kind: ExprKind::Variable(NameStr::Src("b")),
1281            span: Span::DUMMY,
1282        });
1283        let binary = arena.alloc(Expr {
1284            kind: ExprKind::Binary(BinaryExpr {
1285                left: var_a,
1286                op: BinaryOp::Add,
1287                right: var_b,
1288            }),
1289            span: Span::DUMMY,
1290        });
1291        let mut stmts = ArenaVec::new_in(&arena);
1292        stmts.push(Stmt {
1293            kind: StmtKind::Expression(binary),
1294            span: Span::DUMMY,
1295        });
1296        let program = Program {
1297            stmts,
1298            span: Span::DUMMY,
1299        };
1300
1301        struct FindFirst {
1302            found: Option<String>,
1303        }
1304        impl<'arena, 'src> Visitor<'arena, 'src> for FindFirst {
1305            fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
1306                if let ExprKind::Variable(name) = &expr.kind {
1307                    self.found = Some(name.to_string());
1308                    return ControlFlow::Break(());
1309                }
1310                walk_expr(self, expr)
1311            }
1312        }
1313
1314        let mut finder = FindFirst { found: None };
1315        let result = finder.visit_program(&program);
1316        assert!(result.is_break());
1317        assert_eq!(finder.found.as_deref(), Some("a"));
1318    }
1319
1320    #[test]
1321    fn skip_subtree() {
1322        let arena = bumpalo::Bump::new();
1323        // 1 + 2; function foo() { 3 + 4; }
1324        let one = arena.alloc(Expr {
1325            kind: ExprKind::Int(1),
1326            span: Span::DUMMY,
1327        });
1328        let two = arena.alloc(Expr {
1329            kind: ExprKind::Int(2),
1330            span: Span::DUMMY,
1331        });
1332        let top = arena.alloc(Expr {
1333            kind: ExprKind::Binary(BinaryExpr {
1334                left: one,
1335                op: BinaryOp::Add,
1336                right: two,
1337            }),
1338            span: Span::DUMMY,
1339        });
1340        let three = arena.alloc(Expr {
1341            kind: ExprKind::Int(3),
1342            span: Span::DUMMY,
1343        });
1344        let four = arena.alloc(Expr {
1345            kind: ExprKind::Int(4),
1346            span: Span::DUMMY,
1347        });
1348        let inner = arena.alloc(Expr {
1349            kind: ExprKind::Binary(BinaryExpr {
1350                left: three,
1351                op: BinaryOp::Add,
1352                right: four,
1353            }),
1354            span: Span::DUMMY,
1355        });
1356        let mut func_body = ArenaVec::new_in(&arena);
1357        func_body.push(Stmt {
1358            kind: StmtKind::Expression(inner),
1359            span: Span::DUMMY,
1360        });
1361        let func = arena.alloc(FunctionDecl {
1362            name: Ident::name("foo"),
1363            params: ArenaVec::new_in(&arena),
1364            body: func_body,
1365            return_type: None,
1366            by_ref: false,
1367            attributes: ArenaVec::new_in(&arena),
1368            doc_comment: None,
1369        });
1370        let mut stmts = ArenaVec::new_in(&arena);
1371        stmts.push(Stmt {
1372            kind: StmtKind::Expression(top),
1373            span: Span::DUMMY,
1374        });
1375        stmts.push(Stmt {
1376            kind: StmtKind::Function(func),
1377            span: Span::DUMMY,
1378        });
1379        let program = Program {
1380            stmts,
1381            span: Span::DUMMY,
1382        };
1383
1384        struct SkipFunctions {
1385            expr_count: usize,
1386        }
1387        impl<'arena, 'src> Visitor<'arena, 'src> for SkipFunctions {
1388            fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
1389                self.expr_count += 1;
1390                walk_expr(self, expr)
1391            }
1392            fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
1393                if matches!(&stmt.kind, StmtKind::Function(_)) {
1394                    return ControlFlow::Continue(());
1395                }
1396                walk_stmt(self, stmt)
1397            }
1398        }
1399
1400        let mut v = SkipFunctions { expr_count: 0 };
1401        let _ = v.visit_program(&program);
1402        // Only top-level: binary(1, 2) = 3 exprs
1403        assert_eq!(v.expr_count, 3);
1404    }
1405}