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