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