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