Skip to main content

php_ast/owned/
fold.rs

1//! Owned AST transformation via the [`FoldOwned`] trait.
2//!
3//! [`FoldOwned`] is the transformation counterpart of [`super::visitor::OwnedVisitor`].
4//! Where `OwnedVisitor` reads nodes in place, `FoldOwned` rebuilds them — reading
5//! from an input node (borrowed) and returning a new owned value. Override only the
6//! node types you want to change; all others are rebuilt identically by the default
7//! implementations (equivalent to `Clone`).
8//!
9//! # Example
10//!
11//! ```
12//! use php_ast::owned::fold::{FoldOwned, fold_owned_expr};
13//! use php_ast::owned::{Expr, ExprKind};
14//!
15//! struct NegateInts;
16//!
17//! impl FoldOwned for NegateInts {
18//!     fn fold_expr(&mut self, expr: &Expr) -> Expr {
19//!         if let ExprKind::Int(n) = &expr.kind {
20//!             return Expr { kind: ExprKind::Int(-n), span: expr.span };
21//!         }
22//!         fold_owned_expr(self, expr)
23//!     }
24//! }
25//! ```
26
27use super::*;
28
29// =============================================================================
30// FoldOwned trait
31// =============================================================================
32
33/// Trait for transforming owned PHP AST nodes.
34///
35/// All methods have identity-fold default implementations that call the
36/// corresponding free `fold_owned_*` function. Override only the node types
37/// you want to change; the rest recurse automatically.
38pub trait FoldOwned {
39    /// Transforms a program node; recurses by default.
40    fn fold_program(&mut self, program: &Program) -> Program {
41        fold_owned_program(self, program)
42    }
43
44    /// Transforms a statement node; recurses by default.
45    fn fold_stmt(&mut self, stmt: &Stmt) -> Stmt {
46        fold_owned_stmt(self, stmt)
47    }
48
49    /// Transforms a block node; recurses by default.
50    fn fold_block(&mut self, block: &Block) -> Block {
51        fold_owned_block(self, block)
52    }
53
54    /// Transforms an expression node; recurses by default.
55    fn fold_expr(&mut self, expr: &Expr) -> Expr {
56        fold_owned_expr(self, expr)
57    }
58
59    /// Transforms a param node; recurses by default.
60    fn fold_param(&mut self, param: &Param) -> Param {
61        fold_owned_param(self, param)
62    }
63
64    /// Transforms an arg node; recurses by default.
65    fn fold_arg(&mut self, arg: &Arg) -> Arg {
66        fold_owned_arg(self, arg)
67    }
68
69    /// Transforms a class member node; recurses by default.
70    fn fold_class_member(&mut self, member: &ClassMember) -> ClassMember {
71        fold_owned_class_member(self, member)
72    }
73
74    /// Transforms an enum member node; recurses by default.
75    fn fold_enum_member(&mut self, member: &EnumMember) -> EnumMember {
76        fold_owned_enum_member(self, member)
77    }
78
79    /// Transforms a property hook node; recurses by default.
80    fn fold_property_hook(&mut self, hook: &PropertyHook) -> PropertyHook {
81        fold_owned_property_hook(self, hook)
82    }
83
84    /// Transforms a type hint node; recurses by default.
85    fn fold_type_hint(&mut self, type_hint: &TypeHint) -> TypeHint {
86        fold_owned_type_hint(self, type_hint)
87    }
88
89    /// Transforms an attribute node; recurses by default.
90    fn fold_attribute(&mut self, attribute: &Attribute) -> Attribute {
91        fold_owned_attribute(self, attribute)
92    }
93
94    /// Transforms a catch clause node; recurses by default.
95    fn fold_catch_clause(&mut self, catch: &CatchClause) -> CatchClause {
96        fold_owned_catch_clause(self, catch)
97    }
98
99    /// Transforms a match arm node; recurses by default.
100    fn fold_match_arm(&mut self, arm: &MatchArm) -> MatchArm {
101        fold_owned_match_arm(self, arm)
102    }
103
104    /// Transforms a closure use var node; recurses by default.
105    fn fold_closure_use_var(&mut self, var: &ClosureUseVar) -> ClosureUseVar {
106        fold_owned_closure_use_var(self, var)
107    }
108
109    /// Transforms a name node; recurses by default.
110    fn fold_name(&mut self, name: &Name) -> Name {
111        fold_owned_name(self, name)
112    }
113}
114
115// =============================================================================
116// Free fold functions
117// =============================================================================
118
119/// Folds a program, recursing into its children.
120pub fn fold_owned_program<F: FoldOwned + ?Sized>(folder: &mut F, program: &Program) -> Program {
121    Program {
122        stmts: program.stmts.iter().map(|s| folder.fold_stmt(s)).collect(),
123        span: program.span,
124    }
125}
126
127/// Folds a statement, recursing into its children.
128pub fn fold_owned_stmt<F: FoldOwned + ?Sized>(folder: &mut F, stmt: &Stmt) -> Stmt {
129    Stmt {
130        kind: fold_owned_stmt_kind(folder, &stmt.kind),
131        span: stmt.span,
132        doc_comment: stmt.doc_comment.clone(),
133    }
134}
135
136fn fold_owned_stmts<F: FoldOwned + ?Sized>(folder: &mut F, stmts: &[Stmt]) -> Box<[Stmt]> {
137    stmts.iter().map(|s| folder.fold_stmt(s)).collect()
138}
139
140/// Folds a block, recursing into its children.
141pub fn fold_owned_block<F: FoldOwned + ?Sized>(folder: &mut F, block: &Block) -> Block {
142    Block {
143        stmts: fold_owned_stmts(folder, &block.stmts),
144        span: block.span,
145    }
146}
147
148fn fold_owned_exprs<F: FoldOwned + ?Sized>(folder: &mut F, exprs: &[Expr]) -> Box<[Expr]> {
149    exprs.iter().map(|e| folder.fold_expr(e)).collect()
150}
151
152fn fold_owned_args<F: FoldOwned + ?Sized>(folder: &mut F, args: &[Arg]) -> Box<[Arg]> {
153    args.iter().map(|a| folder.fold_arg(a)).collect()
154}
155
156fn fold_owned_attrs<F: FoldOwned + ?Sized>(
157    folder: &mut F,
158    attrs: &[Attribute],
159) -> Box<[Attribute]> {
160    attrs.iter().map(|a| folder.fold_attribute(a)).collect()
161}
162
163fn fold_owned_params<F: FoldOwned + ?Sized>(folder: &mut F, params: &[Param]) -> Box<[Param]> {
164    params.iter().map(|p| folder.fold_param(p)).collect()
165}
166
167fn fold_owned_hooks<F: FoldOwned + ?Sized>(
168    folder: &mut F,
169    hooks: &[PropertyHook],
170) -> Box<[PropertyHook]> {
171    hooks.iter().map(|h| folder.fold_property_hook(h)).collect()
172}
173
174fn fold_owned_members<F: FoldOwned + ?Sized>(
175    folder: &mut F,
176    members: &[ClassMember],
177) -> Box<[ClassMember]> {
178    members
179        .iter()
180        .map(|m| folder.fold_class_member(m))
181        .collect()
182}
183
184fn fold_owned_string_parts<F: FoldOwned + ?Sized>(
185    folder: &mut F,
186    parts: &[StringPart],
187) -> Box<[StringPart]> {
188    parts
189        .iter()
190        .map(|p| match p {
191            StringPart::Literal(s) => StringPart::Literal(s.clone()),
192            StringPart::Expr(e) => StringPart::Expr(folder.fold_expr(e)),
193        })
194        .collect()
195}
196
197fn fold_owned_stmt_kind<F: FoldOwned + ?Sized>(folder: &mut F, k: &StmtKind) -> StmtKind {
198    match k {
199        StmtKind::Expression(e) => StmtKind::Expression(Box::new(folder.fold_expr(e))),
200        StmtKind::Echo(exprs) => StmtKind::Echo(fold_owned_exprs(folder, exprs)),
201        StmtKind::Return(e) => StmtKind::Return(e.as_ref().map(|e| Box::new(folder.fold_expr(e)))),
202        StmtKind::Block(block) => StmtKind::Block(Box::new(folder.fold_block(block))),
203        StmtKind::If(s) => StmtKind::If(Box::new(IfStmt {
204            condition: folder.fold_expr(&s.condition),
205            then_branch: Box::new(folder.fold_stmt(&s.then_branch)),
206            elseif_branches: s
207                .elseif_branches
208                .iter()
209                .map(|b| ElseIfBranch {
210                    condition: folder.fold_expr(&b.condition),
211                    body: folder.fold_stmt(&b.body),
212                    span: b.span,
213                })
214                .collect(),
215            else_branch: s
216                .else_branch
217                .as_ref()
218                .map(|b| Box::new(folder.fold_stmt(b))),
219            else_kw_start: s.else_kw_start,
220            uses_alternative: s.uses_alternative,
221        })),
222        StmtKind::While(s) => StmtKind::While(Box::new(WhileStmt {
223            condition: folder.fold_expr(&s.condition),
224            body: Box::new(folder.fold_stmt(&s.body)),
225            uses_alternative: s.uses_alternative,
226        })),
227        StmtKind::For(s) => StmtKind::For(Box::new(ForStmt {
228            init: fold_owned_exprs(folder, &s.init),
229            condition: fold_owned_exprs(folder, &s.condition),
230            update: fold_owned_exprs(folder, &s.update),
231            body: Box::new(folder.fold_stmt(&s.body)),
232            uses_alternative: s.uses_alternative,
233        })),
234        StmtKind::Foreach(s) => StmtKind::Foreach(Box::new(ForeachStmt {
235            expr: folder.fold_expr(&s.expr),
236            key: s.key.as_ref().map(|e| folder.fold_expr(e)),
237            value: folder.fold_expr(&s.value),
238            body: Box::new(folder.fold_stmt(&s.body)),
239            uses_alternative: s.uses_alternative,
240        })),
241        StmtKind::DoWhile(s) => StmtKind::DoWhile(Box::new(DoWhileStmt {
242            body: Box::new(folder.fold_stmt(&s.body)),
243            condition: folder.fold_expr(&s.condition),
244        })),
245        StmtKind::Function(f) => StmtKind::Function(Box::new(FunctionDecl {
246            name: f.name.clone(),
247            params: fold_owned_params(folder, &f.params),
248            body: Box::new(folder.fold_block(&f.body)),
249            return_type: f.return_type.as_ref().map(|t| folder.fold_type_hint(t)),
250            by_ref: f.by_ref,
251            attributes: fold_owned_attrs(folder, &f.attributes),
252            doc_comment: f.doc_comment.clone(),
253        })),
254        StmtKind::Break(e) => StmtKind::Break(e.as_ref().map(|e| Box::new(folder.fold_expr(e)))),
255        StmtKind::Continue(e) => {
256            StmtKind::Continue(e.as_ref().map(|e| Box::new(folder.fold_expr(e))))
257        }
258        StmtKind::Switch(s) => StmtKind::Switch(Box::new(SwitchStmt {
259            expr: folder.fold_expr(&s.expr),
260            body: SwitchBody {
261                cases: s
262                    .body
263                    .cases
264                    .iter()
265                    .map(|c| SwitchCase {
266                        value: c.value.as_ref().map(|v| folder.fold_expr(v)),
267                        body: fold_owned_stmts(folder, &c.body),
268                        span: c.span,
269                    })
270                    .collect(),
271                span: s.body.span,
272            },
273            uses_alternative: s.uses_alternative,
274        })),
275        StmtKind::Goto(ident) => StmtKind::Goto(ident.clone()),
276        StmtKind::Label(s) => StmtKind::Label(s.clone()),
277        StmtKind::Declare(d) => StmtKind::Declare(Box::new(DeclareStmt {
278            directives: d
279                .directives
280                .iter()
281                .map(|(k, v)| (k.clone(), folder.fold_expr(v)))
282                .collect(),
283            body: d.body.as_ref().map(|b| Box::new(folder.fold_stmt(b))),
284            uses_alternative: d.uses_alternative,
285        })),
286        StmtKind::Unset(exprs) => StmtKind::Unset(fold_owned_exprs(folder, exprs)),
287        StmtKind::Throw(e) => StmtKind::Throw(Box::new(folder.fold_expr(e))),
288        StmtKind::TryCatch(t) => StmtKind::TryCatch(Box::new(TryCatchStmt {
289            body: Box::new(folder.fold_block(&t.body)),
290            catches: t
291                .catches
292                .iter()
293                .map(|c| folder.fold_catch_clause(c))
294                .collect(),
295            finally: t.finally.as_deref().map(|f| Box::new(folder.fold_block(f))),
296            finally_kw_start: t.finally_kw_start,
297        })),
298        StmtKind::Global(exprs) => StmtKind::Global(fold_owned_exprs(folder, exprs)),
299        StmtKind::Class(cls) => StmtKind::Class(Box::new(fold_owned_class_decl(folder, cls))),
300        StmtKind::Interface(iface) => {
301            StmtKind::Interface(Box::new(fold_owned_interface_decl(folder, iface)))
302        }
303        StmtKind::Trait(tr) => StmtKind::Trait(Box::new(fold_owned_trait_decl(folder, tr))),
304        StmtKind::Enum(en) => StmtKind::Enum(Box::new(fold_owned_enum_decl(folder, en))),
305        StmtKind::Namespace(ns) => StmtKind::Namespace(Box::new(NamespaceDecl {
306            name: ns.name.as_ref().map(|n| folder.fold_name(n)),
307            body: match &ns.body {
308                NamespaceBody::Braced(block) => {
309                    NamespaceBody::Braced(Box::new(folder.fold_block(block)))
310                }
311                NamespaceBody::Simple => NamespaceBody::Simple,
312            },
313        })),
314        StmtKind::Use(u) => StmtKind::Use(Box::new(UseDecl {
315            kind: u.kind,
316            uses: u
317                .uses
318                .iter()
319                .map(|item| UseItem {
320                    name: folder.fold_name(&item.name),
321                    alias: item.alias.clone(),
322                    kind: item.kind,
323                    span: item.span,
324                })
325                .collect(),
326        })),
327        StmtKind::Const(items) => StmtKind::Const(
328            items
329                .iter()
330                .map(|item| ConstItem {
331                    name: item.name.clone(),
332                    value: folder.fold_expr(&item.value),
333                    attributes: fold_owned_attrs(folder, &item.attributes),
334                    span: item.span,
335                    doc_comment: item.doc_comment.clone(),
336                })
337                .collect(),
338        ),
339        StmtKind::StaticVar(vars) => StmtKind::StaticVar(
340            vars.iter()
341                .map(|v| StaticVar {
342                    name: v.name.clone(),
343                    default: v.default.as_ref().map(|e| folder.fold_expr(e)),
344                    span: v.span,
345                })
346                .collect(),
347        ),
348        StmtKind::HaltCompiler(s) => StmtKind::HaltCompiler(s.clone()),
349        StmtKind::Nop => StmtKind::Nop,
350        StmtKind::InlineHtml(s) => StmtKind::InlineHtml(s.clone()),
351        StmtKind::Error => StmtKind::Error,
352    }
353}
354
355/// Folds an expression, recursing into its children.
356pub fn fold_owned_expr<F: FoldOwned + ?Sized>(folder: &mut F, expr: &Expr) -> Expr {
357    Expr {
358        kind: fold_owned_expr_kind(folder, &expr.kind),
359        span: expr.span,
360    }
361}
362
363fn fold_owned_expr_kind<F: FoldOwned + ?Sized>(folder: &mut F, k: &ExprKind) -> ExprKind {
364    match k {
365        ExprKind::Int(v) => ExprKind::Int(*v),
366        ExprKind::Float(v) => ExprKind::Float(*v),
367        ExprKind::String(s) => ExprKind::String(s.clone()),
368        ExprKind::InterpolatedString(parts) => {
369            ExprKind::InterpolatedString(fold_owned_string_parts(folder, parts))
370        }
371        ExprKind::Heredoc { label, parts } => ExprKind::Heredoc {
372            label: label.clone(),
373            parts: fold_owned_string_parts(folder, parts),
374        },
375        ExprKind::Nowdoc { label, value } => ExprKind::Nowdoc {
376            label: label.clone(),
377            value: value.clone(),
378        },
379        ExprKind::ShellExec(parts) => ExprKind::ShellExec(fold_owned_string_parts(folder, parts)),
380        ExprKind::Bool(v) => ExprKind::Bool(*v),
381        ExprKind::Null => ExprKind::Null,
382        ExprKind::Variable(s) => ExprKind::Variable(s.clone()),
383        ExprKind::VariableVariable(inner) => {
384            ExprKind::VariableVariable(Box::new(folder.fold_expr(inner)))
385        }
386        ExprKind::Identifier(s) => ExprKind::Identifier(s.clone()),
387        ExprKind::Assign(a) => ExprKind::Assign(AssignExpr {
388            target: Box::new(folder.fold_expr(&a.target)),
389            op: a.op,
390            value: Box::new(folder.fold_expr(&a.value)),
391            by_ref: a.by_ref,
392        }),
393        ExprKind::Binary(b) => ExprKind::Binary(BinaryExpr {
394            left: Box::new(folder.fold_expr(&b.left)),
395            op: b.op,
396            right: Box::new(folder.fold_expr(&b.right)),
397        }),
398        ExprKind::UnaryPrefix(u) => ExprKind::UnaryPrefix(UnaryPrefixExpr {
399            op: u.op,
400            operand: Box::new(folder.fold_expr(&u.operand)),
401        }),
402        ExprKind::UnaryPostfix(u) => ExprKind::UnaryPostfix(UnaryPostfixExpr {
403            operand: Box::new(folder.fold_expr(&u.operand)),
404            op: u.op,
405        }),
406        ExprKind::Ternary(t) => ExprKind::Ternary(TernaryExpr {
407            condition: Box::new(folder.fold_expr(&t.condition)),
408            then_expr: t.then_expr.as_ref().map(|e| Box::new(folder.fold_expr(e))),
409            else_expr: Box::new(folder.fold_expr(&t.else_expr)),
410        }),
411        ExprKind::NullCoalesce(n) => ExprKind::NullCoalesce(NullCoalesceExpr {
412            left: Box::new(folder.fold_expr(&n.left)),
413            right: Box::new(folder.fold_expr(&n.right)),
414        }),
415        ExprKind::FunctionCall(f) => ExprKind::FunctionCall(FunctionCallExpr {
416            name: Box::new(folder.fold_expr(&f.name)),
417            args: fold_owned_args(folder, &f.args),
418        }),
419        ExprKind::Array(elems) => ExprKind::Array(
420            elems
421                .iter()
422                .map(|e| ArrayElement {
423                    key: e.key.as_ref().map(|k| folder.fold_expr(k)),
424                    value: folder.fold_expr(&e.value),
425                    unpack: e.unpack,
426                    by_ref: e.by_ref,
427                    span: e.span,
428                })
429                .collect(),
430        ),
431        ExprKind::ArrayAccess(a) => ExprKind::ArrayAccess(ArrayAccessExpr {
432            array: Box::new(folder.fold_expr(&a.array)),
433            index: a.index.as_ref().map(|e| Box::new(folder.fold_expr(e))),
434        }),
435        ExprKind::Print(e) => ExprKind::Print(Box::new(folder.fold_expr(e))),
436        ExprKind::Parenthesized(e) => ExprKind::Parenthesized(Box::new(folder.fold_expr(e))),
437        ExprKind::Cast(kind, e) => ExprKind::Cast(*kind, Box::new(folder.fold_expr(e))),
438        ExprKind::ErrorSuppress(e) => ExprKind::ErrorSuppress(Box::new(folder.fold_expr(e))),
439        ExprKind::Isset(exprs) => ExprKind::Isset(fold_owned_exprs(folder, exprs)),
440        ExprKind::Empty(e) => ExprKind::Empty(Box::new(folder.fold_expr(e))),
441        ExprKind::Include(kind, e) => ExprKind::Include(*kind, Box::new(folder.fold_expr(e))),
442        ExprKind::Eval(e) => ExprKind::Eval(Box::new(folder.fold_expr(e))),
443        ExprKind::Exit(e) => ExprKind::Exit(e.as_ref().map(|e| Box::new(folder.fold_expr(e)))),
444        ExprKind::MagicConst(m) => ExprKind::MagicConst(*m),
445        ExprKind::Clone(e) => ExprKind::Clone(Box::new(folder.fold_expr(e))),
446        ExprKind::CloneWith(obj, props) => ExprKind::CloneWith(
447            Box::new(folder.fold_expr(obj)),
448            Box::new(folder.fold_expr(props)),
449        ),
450        ExprKind::New(n) => ExprKind::New(NewExpr {
451            class: Box::new(folder.fold_expr(&n.class)),
452            args: fold_owned_args(folder, &n.args),
453        }),
454        ExprKind::PropertyAccess(p) => ExprKind::PropertyAccess(PropertyAccessExpr {
455            object: Box::new(folder.fold_expr(&p.object)),
456            property: Box::new(folder.fold_expr(&p.property)),
457        }),
458        ExprKind::NullsafePropertyAccess(p) => {
459            ExprKind::NullsafePropertyAccess(PropertyAccessExpr {
460                object: Box::new(folder.fold_expr(&p.object)),
461                property: Box::new(folder.fold_expr(&p.property)),
462            })
463        }
464        ExprKind::MethodCall(m) => ExprKind::MethodCall(Box::new(MethodCallExpr {
465            object: Box::new(folder.fold_expr(&m.object)),
466            method: Box::new(folder.fold_expr(&m.method)),
467            args: fold_owned_args(folder, &m.args),
468        })),
469        ExprKind::NullsafeMethodCall(m) => ExprKind::NullsafeMethodCall(Box::new(MethodCallExpr {
470            object: Box::new(folder.fold_expr(&m.object)),
471            method: Box::new(folder.fold_expr(&m.method)),
472            args: fold_owned_args(folder, &m.args),
473        })),
474        ExprKind::StaticPropertyAccess(s) => ExprKind::StaticPropertyAccess(StaticAccessExpr {
475            class: Box::new(folder.fold_expr(&s.class)),
476            member: Box::new(folder.fold_expr(&s.member)),
477        }),
478        ExprKind::StaticMethodCall(s) => {
479            ExprKind::StaticMethodCall(Box::new(StaticMethodCallExpr {
480                class: Box::new(folder.fold_expr(&s.class)),
481                method: Box::new(folder.fold_expr(&s.method)),
482                args: fold_owned_args(folder, &s.args),
483            }))
484        }
485        ExprKind::StaticDynMethodCall(s) => {
486            ExprKind::StaticDynMethodCall(Box::new(StaticDynMethodCallExpr {
487                class: Box::new(folder.fold_expr(&s.class)),
488                method: Box::new(folder.fold_expr(&s.method)),
489                args: fold_owned_args(folder, &s.args),
490            }))
491        }
492        ExprKind::ClassConstAccess(s) => ExprKind::ClassConstAccess(StaticAccessExpr {
493            class: Box::new(folder.fold_expr(&s.class)),
494            member: Box::new(folder.fold_expr(&s.member)),
495        }),
496        ExprKind::ClassConstAccessDynamic { class, member } => ExprKind::ClassConstAccessDynamic {
497            class: Box::new(folder.fold_expr(class)),
498            member: Box::new(folder.fold_expr(member)),
499        },
500        ExprKind::StaticPropertyAccessDynamic { class, member } => {
501            ExprKind::StaticPropertyAccessDynamic {
502                class: Box::new(folder.fold_expr(class)),
503                member: Box::new(folder.fold_expr(member)),
504            }
505        }
506        ExprKind::Closure(c) => ExprKind::Closure(Box::new(ClosureExpr {
507            is_static: c.is_static,
508            by_ref: c.by_ref,
509            params: fold_owned_params(folder, &c.params),
510            use_vars: c
511                .use_vars
512                .iter()
513                .map(|v| folder.fold_closure_use_var(v))
514                .collect(),
515            return_type: c.return_type.as_ref().map(|t| folder.fold_type_hint(t)),
516            body: Box::new(folder.fold_block(&c.body)),
517            attributes: fold_owned_attrs(folder, &c.attributes),
518        })),
519        ExprKind::ArrowFunction(f) => ExprKind::ArrowFunction(Box::new(ArrowFunctionExpr {
520            is_static: f.is_static,
521            by_ref: f.by_ref,
522            params: fold_owned_params(folder, &f.params),
523            return_type: f.return_type.as_ref().map(|t| folder.fold_type_hint(t)),
524            body: Box::new(folder.fold_expr(&f.body)),
525            attributes: fold_owned_attrs(folder, &f.attributes),
526        })),
527        ExprKind::Match(m) => ExprKind::Match(MatchExpr {
528            subject: Box::new(folder.fold_expr(&m.subject)),
529            arms: m
530                .arms
531                .iter()
532                .map(|arm| folder.fold_match_arm(arm))
533                .collect(),
534            brace_start: m.brace_start,
535        }),
536        ExprKind::ThrowExpr(e) => ExprKind::ThrowExpr(Box::new(folder.fold_expr(e))),
537        ExprKind::Yield(y) => ExprKind::Yield(YieldExpr {
538            key: y.key.as_ref().map(|e| Box::new(folder.fold_expr(e))),
539            value: y.value.as_ref().map(|e| Box::new(folder.fold_expr(e))),
540            is_from: y.is_from,
541        }),
542        ExprKind::AnonymousClass(cls) => {
543            ExprKind::AnonymousClass(Box::new(fold_owned_class_decl(folder, cls)))
544        }
545        ExprKind::CallableCreate(c) => ExprKind::CallableCreate(CallableCreateExpr {
546            kind: match &c.kind {
547                CallableCreateKind::Function(e) => {
548                    CallableCreateKind::Function(Box::new(folder.fold_expr(e)))
549                }
550                CallableCreateKind::Method { object, method } => CallableCreateKind::Method {
551                    object: Box::new(folder.fold_expr(object)),
552                    method: Box::new(folder.fold_expr(method)),
553                },
554                CallableCreateKind::NullsafeMethod { object, method } => {
555                    CallableCreateKind::NullsafeMethod {
556                        object: Box::new(folder.fold_expr(object)),
557                        method: Box::new(folder.fold_expr(method)),
558                    }
559                }
560                CallableCreateKind::StaticMethod { class, method } => {
561                    CallableCreateKind::StaticMethod {
562                        class: Box::new(folder.fold_expr(class)),
563                        method: Box::new(folder.fold_expr(method)),
564                    }
565                }
566            },
567        }),
568        ExprKind::Omit => ExprKind::Omit,
569        ExprKind::Error => ExprKind::Error,
570    }
571}
572
573/// Folds a param, recursing into its children.
574pub fn fold_owned_param<F: FoldOwned + ?Sized>(folder: &mut F, p: &Param) -> Param {
575    Param {
576        name: p.name.clone(),
577        type_hint: p.type_hint.as_ref().map(|t| folder.fold_type_hint(t)),
578        default: p.default.as_ref().map(|e| folder.fold_expr(e)),
579        by_ref: p.by_ref,
580        variadic: p.variadic,
581        is_readonly: p.is_readonly,
582        is_final: p.is_final,
583        visibility: p.visibility,
584        set_visibility: p.set_visibility,
585        attributes: fold_owned_attrs(folder, &p.attributes),
586        hooks: fold_owned_hooks(folder, &p.hooks),
587        doc_comment: p.doc_comment.clone(),
588        span: p.span,
589    }
590}
591
592/// Folds an argument, recursing into its children.
593pub fn fold_owned_arg<F: FoldOwned + ?Sized>(folder: &mut F, arg: &Arg) -> Arg {
594    Arg {
595        name: arg.name.as_ref().map(|n| folder.fold_name(n)),
596        value: arg.value.as_ref().map(|v| folder.fold_expr(v)),
597        unpack: arg.unpack,
598        by_ref: arg.by_ref,
599        span: arg.span,
600    }
601}
602
603/// Folds a closure use var, recursing into its children.
604pub fn fold_owned_closure_use_var<F: FoldOwned + ?Sized>(
605    _folder: &mut F,
606    var: &ClosureUseVar,
607) -> ClosureUseVar {
608    var.clone()
609}
610
611/// Folds a name, recursing into its children.
612pub fn fold_owned_name<F: FoldOwned + ?Sized>(_folder: &mut F, name: &Name) -> Name {
613    name.clone()
614}
615
616/// Folds a class member, recursing into its children.
617pub fn fold_owned_class_member<F: FoldOwned + ?Sized>(
618    folder: &mut F,
619    member: &ClassMember,
620) -> ClassMember {
621    ClassMember {
622        kind: match &member.kind {
623            ClassMemberKind::Property(p) => ClassMemberKind::Property(PropertyDecl {
624                name: p.name.clone(),
625                visibility: p.visibility,
626                set_visibility: p.set_visibility,
627                is_static: p.is_static,
628                is_readonly: p.is_readonly,
629                type_hint: p.type_hint.as_ref().map(|t| folder.fold_type_hint(t)),
630                default: p.default.as_ref().map(|e| folder.fold_expr(e)),
631                attributes: fold_owned_attrs(folder, &p.attributes),
632                hooks: fold_owned_hooks(folder, &p.hooks),
633                doc_comment: p.doc_comment.clone(),
634            }),
635            ClassMemberKind::Method(m) => ClassMemberKind::Method(MethodDecl {
636                name: m.name.clone(),
637                visibility: m.visibility,
638                is_static: m.is_static,
639                is_abstract: m.is_abstract,
640                is_final: m.is_final,
641                by_ref: m.by_ref,
642                params: fold_owned_params(folder, &m.params),
643                return_type: m.return_type.as_ref().map(|t| folder.fold_type_hint(t)),
644                body: m.body.as_ref().map(|b| Box::new(folder.fold_block(b))),
645                attributes: fold_owned_attrs(folder, &m.attributes),
646                doc_comment: m.doc_comment.clone(),
647            }),
648            ClassMemberKind::ClassConst(c) => {
649                ClassMemberKind::ClassConst(fold_owned_class_const(folder, c))
650            }
651            ClassMemberKind::TraitUse(t) => {
652                ClassMemberKind::TraitUse(fold_owned_trait_use(folder, t))
653            }
654        },
655        span: member.span,
656    }
657}
658
659/// Folds an enum member, recursing into its children.
660pub fn fold_owned_enum_member<F: FoldOwned + ?Sized>(
661    folder: &mut F,
662    member: &EnumMember,
663) -> EnumMember {
664    EnumMember {
665        kind: match &member.kind {
666            EnumMemberKind::Case(c) => EnumMemberKind::Case(EnumCase {
667                name: c.name.clone(),
668                value: c.value.as_ref().map(|e| folder.fold_expr(e)),
669                attributes: fold_owned_attrs(folder, &c.attributes),
670                doc_comment: c.doc_comment.clone(),
671            }),
672            EnumMemberKind::Method(m) => EnumMemberKind::Method(MethodDecl {
673                name: m.name.clone(),
674                visibility: m.visibility,
675                is_static: m.is_static,
676                is_abstract: m.is_abstract,
677                is_final: m.is_final,
678                by_ref: m.by_ref,
679                params: fold_owned_params(folder, &m.params),
680                return_type: m.return_type.as_ref().map(|t| folder.fold_type_hint(t)),
681                body: m.body.as_ref().map(|b| Box::new(folder.fold_block(b))),
682                attributes: fold_owned_attrs(folder, &m.attributes),
683                doc_comment: m.doc_comment.clone(),
684            }),
685            EnumMemberKind::ClassConst(c) => {
686                EnumMemberKind::ClassConst(fold_owned_class_const(folder, c))
687            }
688            EnumMemberKind::TraitUse(t) => {
689                EnumMemberKind::TraitUse(fold_owned_trait_use(folder, t))
690            }
691        },
692        span: member.span,
693    }
694}
695
696/// Folds a property hook, recursing into its children.
697pub fn fold_owned_property_hook<F: FoldOwned + ?Sized>(
698    folder: &mut F,
699    hook: &PropertyHook,
700) -> PropertyHook {
701    PropertyHook {
702        kind: hook.kind,
703        body: match &hook.body {
704            PropertyHookBody::Block(block) => {
705                PropertyHookBody::Block(Box::new(folder.fold_block(block)))
706            }
707            PropertyHookBody::Expression(e) => PropertyHookBody::Expression(folder.fold_expr(e)),
708            PropertyHookBody::Abstract => PropertyHookBody::Abstract,
709        },
710        is_final: hook.is_final,
711        by_ref: hook.by_ref,
712        params: fold_owned_params(folder, &hook.params),
713        attributes: fold_owned_attrs(folder, &hook.attributes),
714        span: hook.span,
715    }
716}
717
718/// Folds a type hint, recursing into its children.
719pub fn fold_owned_type_hint<F: FoldOwned + ?Sized>(
720    folder: &mut F,
721    type_hint: &TypeHint,
722) -> TypeHint {
723    TypeHint {
724        kind: match &type_hint.kind {
725            TypeHintKind::Named(n) => TypeHintKind::Named(folder.fold_name(n)),
726            TypeHintKind::Keyword(b, span) => TypeHintKind::Keyword(*b, *span),
727            TypeHintKind::Nullable(inner) => {
728                TypeHintKind::Nullable(Box::new(folder.fold_type_hint(inner)))
729            }
730            TypeHintKind::Union(types) => {
731                TypeHintKind::Union(types.iter().map(|t| folder.fold_type_hint(t)).collect())
732            }
733            TypeHintKind::Intersection(types) => {
734                TypeHintKind::Intersection(types.iter().map(|t| folder.fold_type_hint(t)).collect())
735            }
736        },
737        span: type_hint.span,
738    }
739}
740
741/// Folds an attribute, recursing into its children.
742pub fn fold_owned_attribute<F: FoldOwned + ?Sized>(
743    folder: &mut F,
744    attribute: &Attribute,
745) -> Attribute {
746    Attribute {
747        name: folder.fold_name(&attribute.name),
748        args: fold_owned_args(folder, &attribute.args),
749        span: attribute.span,
750    }
751}
752
753/// Folds a catch clause, recursing into its children.
754pub fn fold_owned_catch_clause<F: FoldOwned + ?Sized>(
755    folder: &mut F,
756    catch: &CatchClause,
757) -> CatchClause {
758    CatchClause {
759        types: catch.types.iter().map(|n| folder.fold_name(n)).collect(),
760        var: catch.var.clone(),
761        body: Box::new(folder.fold_block(&catch.body)),
762        span: catch.span,
763    }
764}
765
766/// Folds a match arm, recursing into its children.
767pub fn fold_owned_match_arm<F: FoldOwned + ?Sized>(folder: &mut F, arm: &MatchArm) -> MatchArm {
768    MatchArm {
769        conditions: arm
770            .conditions
771            .as_ref()
772            .map(|conds| fold_owned_exprs(folder, conds)),
773        body: folder.fold_expr(&arm.body),
774        span: arm.span,
775    }
776}
777
778fn fold_owned_class_const<F: FoldOwned + ?Sized>(
779    folder: &mut F,
780    c: &ClassConstDecl,
781) -> ClassConstDecl {
782    ClassConstDecl {
783        name: c.name.clone(),
784        visibility: c.visibility,
785        is_final: c.is_final,
786        type_hint: c
787            .type_hint
788            .as_ref()
789            .map(|th| Box::new(folder.fold_type_hint(th))),
790        value: folder.fold_expr(&c.value),
791        attributes: fold_owned_attrs(folder, &c.attributes),
792        doc_comment: c.doc_comment.clone(),
793    }
794}
795
796fn fold_owned_trait_use<F: FoldOwned + ?Sized>(folder: &mut F, t: &TraitUseDecl) -> TraitUseDecl {
797    TraitUseDecl {
798        traits: t.traits.iter().map(|n| folder.fold_name(n)).collect(),
799        adaptations: t
800            .adaptations
801            .iter()
802            .map(|a| TraitAdaptation {
803                kind: match &a.kind {
804                    TraitAdaptationKind::Precedence {
805                        trait_name,
806                        method,
807                        insteadof,
808                    } => TraitAdaptationKind::Precedence {
809                        trait_name: folder.fold_name(trait_name),
810                        method: folder.fold_name(method),
811                        insteadof: insteadof.iter().map(|n| folder.fold_name(n)).collect(),
812                    },
813                    TraitAdaptationKind::Alias {
814                        trait_name,
815                        method,
816                        new_modifier,
817                        new_name,
818                    } => TraitAdaptationKind::Alias {
819                        trait_name: trait_name.as_ref().map(|n| folder.fold_name(n)),
820                        method: folder.fold_name(method),
821                        new_modifier: *new_modifier,
822                        new_name: new_name.as_ref().map(|n| folder.fold_name(n)),
823                    },
824                },
825                span: a.span,
826            })
827            .collect(),
828        adaptations_brace_start: t.adaptations_brace_start,
829        doc_comment: t.doc_comment.clone(),
830    }
831}
832
833fn fold_owned_class_decl<F: FoldOwned + ?Sized>(folder: &mut F, cls: &ClassDecl) -> ClassDecl {
834    ClassDecl {
835        name: cls.name.clone(),
836        modifiers: cls.modifiers.clone(),
837        extends: cls.extends.as_ref().map(|n| folder.fold_name(n)),
838        implements: cls.implements.iter().map(|n| folder.fold_name(n)).collect(),
839        body: ClassBody {
840            members: fold_owned_members(folder, &cls.body.members),
841            span: cls.body.span,
842        },
843        attributes: fold_owned_attrs(folder, &cls.attributes),
844        doc_comment: cls.doc_comment.clone(),
845    }
846}
847
848fn fold_owned_interface_decl<F: FoldOwned + ?Sized>(
849    folder: &mut F,
850    iface: &InterfaceDecl,
851) -> InterfaceDecl {
852    InterfaceDecl {
853        name: iface.name.clone(),
854        extends: iface.extends.iter().map(|n| folder.fold_name(n)).collect(),
855        body: ClassBody {
856            members: fold_owned_members(folder, &iface.body.members),
857            span: iface.body.span,
858        },
859        attributes: fold_owned_attrs(folder, &iface.attributes),
860        doc_comment: iface.doc_comment.clone(),
861    }
862}
863
864fn fold_owned_trait_decl<F: FoldOwned + ?Sized>(folder: &mut F, tr: &TraitDecl) -> TraitDecl {
865    TraitDecl {
866        name: tr.name.clone(),
867        body: ClassBody {
868            members: fold_owned_members(folder, &tr.body.members),
869            span: tr.body.span,
870        },
871        attributes: fold_owned_attrs(folder, &tr.attributes),
872        doc_comment: tr.doc_comment.clone(),
873    }
874}
875
876fn fold_owned_enum_decl<F: FoldOwned + ?Sized>(folder: &mut F, en: &EnumDecl) -> EnumDecl {
877    EnumDecl {
878        name: en.name.clone(),
879        scalar_type: en.scalar_type.as_ref().map(|n| folder.fold_name(n)),
880        implements: en.implements.iter().map(|n| folder.fold_name(n)).collect(),
881        body: EnumBody {
882            members: en
883                .body
884                .members
885                .iter()
886                .map(|m| folder.fold_enum_member(m))
887                .collect(),
888            span: en.body.span,
889        },
890        attributes: fold_owned_attrs(folder, &en.attributes),
891        doc_comment: en.doc_comment.clone(),
892    }
893}
894
895// =============================================================================
896// Tests
897// =============================================================================
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902    use crate::ast::AssignOp;
903    use crate::Span;
904
905    fn dummy_var(name: &str) -> Expr {
906        Expr {
907            kind: ExprKind::Variable(Box::from(name)),
908            span: Span::DUMMY,
909        }
910    }
911
912    fn dummy_int(n: i64) -> Expr {
913        Expr {
914            kind: ExprKind::Int(n),
915            span: Span::DUMMY,
916        }
917    }
918
919    fn assign(target: Expr, value: Expr) -> Expr {
920        Expr {
921            kind: ExprKind::Assign(AssignExpr {
922                target: Box::new(target),
923                op: AssignOp::Assign,
924                value: Box::new(value),
925                by_ref: false,
926            }),
927            span: Span::DUMMY,
928        }
929    }
930
931    fn expr_stmt(e: Expr) -> Stmt {
932        Stmt {
933            kind: StmtKind::Expression(Box::new(e)),
934            span: Span::DUMMY,
935            doc_comment: None,
936        }
937    }
938
939    fn empty_block() -> Block {
940        Block {
941            stmts: Box::from([]),
942            span: Span::DUMMY,
943        }
944    }
945
946    fn program(stmts: impl IntoIterator<Item = Stmt>) -> Program {
947        Program {
948            stmts: stmts.into_iter().collect(),
949            span: Span::DUMMY,
950        }
951    }
952
953    /// Identity fold on `$x = $y;` must produce structurally equal output.
954    #[test]
955    fn identity_fold_roundtrip() {
956        let p = program([expr_stmt(assign(dummy_var("x"), dummy_var("y")))]);
957        struct Identity;
958        impl FoldOwned for Identity {}
959        let folded = Identity.fold_program(&p);
960        assert_eq!(
961            serde_json::to_string(&folded).unwrap(),
962            serde_json::to_string(&p).unwrap(),
963        );
964    }
965
966    /// NegateInts fold on `$x = 42;` must produce `$x = -42;`.
967    #[test]
968    fn negate_ints() {
969        let p = program([expr_stmt(assign(dummy_var("x"), dummy_int(42)))]);
970        struct NegateInts;
971        impl FoldOwned for NegateInts {
972            fn fold_expr(&mut self, expr: &Expr) -> Expr {
973                if let ExprKind::Int(n) = &expr.kind {
974                    return Expr {
975                        kind: ExprKind::Int(-n),
976                        span: expr.span,
977                    };
978                }
979                fold_owned_expr(self, expr)
980            }
981        }
982        let folded = NegateInts.fold_program(&p);
983        let stmt = &folded.stmts[0];
984        if let StmtKind::Expression(expr) = &stmt.kind {
985            if let ExprKind::Assign(a) = &expr.kind {
986                if let ExprKind::Int(n) = &a.value.kind {
987                    assert_eq!(*n, -42);
988                    return;
989                }
990            }
991        }
992        panic!("expected negated int assignment");
993    }
994
995    /// Fold that rewrites variable names: all occurrences of `$x` become `$renamed`.
996    #[test]
997    fn rename_variable() {
998        // $x = $x;
999        let p = program([expr_stmt(assign(dummy_var("x"), dummy_var("x")))]);
1000        struct Rename;
1001        impl FoldOwned for Rename {
1002            fn fold_expr(&mut self, expr: &Expr) -> Expr {
1003                if let ExprKind::Variable(name) = &expr.kind {
1004                    if name.as_ref() == "x" {
1005                        return Expr {
1006                            kind: ExprKind::Variable(Box::from("renamed")),
1007                            span: expr.span,
1008                        };
1009                    }
1010                }
1011                fold_owned_expr(self, expr)
1012            }
1013        }
1014        let folded = Rename.fold_program(&p);
1015        let stmt = &folded.stmts[0];
1016        if let StmtKind::Expression(expr) = &stmt.kind {
1017            if let ExprKind::Assign(a) = &expr.kind {
1018                assert!(matches!(&a.target.kind, ExprKind::Variable(n) if n.as_ref() == "renamed"));
1019                assert!(matches!(&a.value.kind, ExprKind::Variable(n) if n.as_ref() == "renamed"));
1020                return;
1021            }
1022        }
1023        panic!("expected renamed variable assignment");
1024    }
1025
1026    /// Rename fold must reach into closure `use` capture lists.
1027    #[test]
1028    fn rename_variable_in_closure_use() {
1029        // function() use ($x) {}
1030        let closure = Expr {
1031            kind: ExprKind::Closure(Box::new(ClosureExpr {
1032                is_static: false,
1033                by_ref: false,
1034                params: Box::from([]),
1035                use_vars: Box::from([ClosureUseVar {
1036                    name: Box::from("x"),
1037                    by_ref: false,
1038                    span: Span::DUMMY,
1039                }]),
1040                return_type: None,
1041                body: Box::new(empty_block()),
1042                attributes: Box::from([]),
1043            })),
1044            span: Span::DUMMY,
1045        };
1046        let p = program([expr_stmt(closure)]);
1047
1048        struct Rename;
1049        impl FoldOwned for Rename {
1050            fn fold_closure_use_var(&mut self, var: &ClosureUseVar) -> ClosureUseVar {
1051                ClosureUseVar {
1052                    name: if var.name.as_ref() == "x" {
1053                        Box::from("renamed")
1054                    } else {
1055                        var.name.clone()
1056                    },
1057                    by_ref: var.by_ref,
1058                    span: var.span,
1059                }
1060            }
1061        }
1062
1063        let folded = Rename.fold_program(&p);
1064        if let StmtKind::Expression(expr) = &folded.stmts[0].kind {
1065            if let ExprKind::Closure(c) = &expr.kind {
1066                assert_eq!(c.use_vars[0].name.as_ref(), "renamed");
1067                return;
1068            }
1069        }
1070        panic!("expected closure with renamed use-var");
1071    }
1072}