Chris@0: Walking the AST Chris@0: =============== Chris@0: Chris@0: The most common way to work with the AST is by using a node traverser and one or more node visitors. Chris@0: As a basic example, the following code changes all literal integers in the AST into strings (e.g., Chris@0: `42` becomes `'42'`.) Chris@0: Chris@0: ```php Chris@0: use PhpParser\{Node, NodeTraverser, NodeVisitorAbstract}; Chris@0: Chris@0: $traverser = new NodeTraverser; Chris@0: $traverser->addVisitor(new class extends NodeVisitorAbstract { Chris@0: public function leaveNode(Node $node) { Chris@0: if ($node instanceof Node\Scalar\LNumber) { Chris@0: return new Node\Scalar\String_((string) $node->value); Chris@0: } Chris@0: } Chris@0: }); Chris@0: Chris@0: $stmts = ...; Chris@0: $modifiedStmts = $traverser->traverse($stmts); Chris@0: ``` Chris@0: Chris@0: Node visitors Chris@0: ------------- Chris@0: Chris@0: Each node visitor implements an interface with following four methods: Chris@0: Chris@0: ```php Chris@0: interface NodeVisitor { Chris@0: public function beforeTraverse(array $nodes); Chris@0: public function enterNode(Node $node); Chris@0: public function leaveNode(Node $node); Chris@0: public function afterTraverse(array $nodes); Chris@0: } Chris@0: ``` Chris@0: Chris@0: The `beforeTraverse()` and `afterTraverse()` methods are called before and after the traversal Chris@0: respectively, and are passed the entire AST. They can be used to perform any necessary state Chris@0: setup or cleanup. Chris@0: Chris@0: The `enterNode()` method is called when a node is first encountered, before its children are Chris@0: processed ("preorder"). The `leaveNode()` method is called after all children have been visited Chris@0: ("postorder"). Chris@0: Chris@0: For example, if we have the following excerpt of an AST Chris@0: Chris@0: ``` Chris@0: Expr_FuncCall( Chris@0: name: Name( Chris@0: parts: array( Chris@0: 0: printLine Chris@0: ) Chris@0: ) Chris@0: args: array( Chris@0: 0: Arg( Chris@0: value: Scalar_String( Chris@0: value: Hello World!!! Chris@0: ) Chris@0: byRef: false Chris@0: unpack: false Chris@0: ) Chris@0: ) Chris@0: ) Chris@0: ``` Chris@0: Chris@0: then the enter/leave methods will be called in the following order: Chris@0: Chris@0: ``` Chris@0: enterNode(Expr_FuncCall) Chris@0: enterNode(Name) Chris@0: leaveNode(Name) Chris@0: enterNode(Arg) Chris@0: enterNode(Scalar_String) Chris@0: leaveNode(Scalar_String) Chris@0: leaveNode(Arg) Chris@0: leaveNode(Expr_FuncCall) Chris@0: ``` Chris@0: Chris@0: A common pattern is that `enterNode` is used to collect some information and then `leaveNode` Chris@0: performs modifications based on that. At the time when `leaveNode` is called, all the code inside Chris@0: the node will have already been visited and necessary information collected. Chris@0: Chris@0: As you usually do not want to implement all four methods, it is recommended that you extend Chris@0: `NodeVisitorAbstract` instead of implementing the interface directly. The abstract class provides Chris@0: empty default implementations. Chris@0: Chris@0: Modifying the AST Chris@0: ----------------- Chris@0: Chris@0: There are a number of ways in which the AST can be modified from inside a node visitor. The first Chris@0: and simplest is to simply change AST properties inside the visitor: Chris@0: Chris@0: ```php Chris@0: public function leaveNode(Node $node) { Chris@0: if ($node instanceof Node\Scalar\LNumber) { Chris@0: // increment all integer literals Chris@0: $node->value++; Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: The second is to replace a node entirely by returning a new node: Chris@0: Chris@0: ```php Chris@0: public function leaveNode(Node $node) { Chris@0: if ($node instanceof Node\Expr\BinaryOp\BooleanAnd) { Chris@0: // Convert all $a && $b expressions into !($a && $b) Chris@0: return new Node\Expr\BooleanNot($node); Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: Doing this is supported both inside enterNode and leaveNode. However, you have to be mindful about Chris@0: where you perform the replacement: If a node is replaced in enterNode, then the recursive traversal Chris@0: will also consider the children of the new node. If you aren't careful, this can lead to infinite Chris@0: recursion. For example, let's take the previous code sample and use enterNode instead: Chris@0: Chris@0: ```php Chris@0: public function enterNode(Node $node) { Chris@0: if ($node instanceof Node\Expr\BinaryOp\BooleanAnd) { Chris@0: // Convert all $a && $b expressions into !($a && $b) Chris@0: return new Node\Expr\BooleanNot($node); Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: Now `$a && $b` will be replaced by `!($a && $b)`. Then the traverser will go into the first (and Chris@0: only) child of `!($a && $b)`, which is `$a && $b`. The transformation applies again and we end up Chris@0: with `!!($a && $b)`. This will continue until PHP hits the memory limit. Chris@0: Chris@0: Finally, two special replacement types are supported only by leaveNode. The first is removal of a Chris@0: node: Chris@0: Chris@0: ```php Chris@0: public function leaveNode(Node $node) { Chris@0: if ($node instanceof Node\Stmt\Return_) { Chris@0: // Remove all return statements Chris@0: return NodeTraverser::REMOVE_NODE; Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: Node removal only works if the parent structure is an array. This means that usually it only makes Chris@0: sense to remove nodes of type `Node\Stmt`, as they always occur inside statement lists (and a few Chris@0: more node types like `Arg` or `Expr\ArrayItem`, which are also always part of lists). Chris@0: Chris@0: On the other hand, removing a `Node\Expr` does not make sense: If you have `$a * $b`, there is no Chris@0: meaningful way in which the `$a` part could be removed. If you want to remove an expression, you Chris@0: generally want to remove it together with a surrounding expression statement: Chris@0: Chris@0: ```php Chris@0: public function leaveNode(Node $node) { Chris@0: if ($node instanceof Node\Stmt\Expression Chris@0: && $node->expr instanceof Node\Expr\FuncCall Chris@0: && $node->expr->name instanceof Node\Name Chris@0: && $node->expr->name->toString() === 'var_dump' Chris@0: ) { Chris@0: return NodeTraverser::REMOVE_NODE; Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: This example will remove all calls to `var_dump()` which occur as expression statements. This means Chris@0: that `var_dump($a);` will be removed, but `if (var_dump($a))` will not be removed (and there is no Chris@0: obvious way in which it can be removed). Chris@0: Chris@0: Next to removing nodes, it is also possible to replace one node with multiple nodes. Again, this Chris@0: only works inside leaveNode and only if the parent structure is an array. Chris@0: Chris@0: ```php Chris@0: public function leaveNode(Node $node) { Chris@0: if ($node instanceof Node\Stmt\Return_ && $node->expr !== null) { Chris@0: // Convert "return foo();" into "$retval = foo(); return $retval;" Chris@0: $var = new Node\Expr\Variable('retval'); Chris@0: return [ Chris@0: new Node\Stmt\Expression(new Node\Expr\Assign($var, $node->expr)), Chris@0: new Node\Stmt\Return_($var), Chris@0: ]; Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: Short-circuiting traversal Chris@0: -------------------------- Chris@0: Chris@0: An AST can easily contain thousands of nodes, and traversing over all of them may be slow, Chris@0: especially if you have more than one visitor. In some cases, it is possible to avoid a full Chris@0: traversal. Chris@0: Chris@0: If you are looking for all class declarations in a file (and assuming you're not interested in Chris@0: anonymous classes), you know that once you've seen a class declaration, there is no point in also Chris@0: checking all it's child nodes, because PHP does not allow nesting classes. In this case, you can Chris@0: instruct the traverser to not recurse into the class node: Chris@0: Chris@0: ``` Chris@0: private $classes = []; Chris@0: public function enterNode(Node $node) { Chris@0: if ($node instanceof Node\Stmt\Class_) { Chris@0: $this->classes[] = $node; Chris@0: return NodeTraverser::DONT_TRAVERSE_CHILDREN; Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: Of course, this option is only available in enterNode, because it's already too late by the time Chris@0: leaveNode is reached. Chris@0: Chris@0: If you are only looking for one specific node, it is also possible to abort the traversal entirely Chris@0: after finding it. For example, if you are looking for the node of a class with a certain name (and Chris@0: discounting exotic cases like conditionally defining a class two times), you can stop traversal Chris@0: once you found it: Chris@0: Chris@0: ``` Chris@0: private $class = null; Chris@0: public function enterNode(Node $node) { Chris@0: if ($node instanceof Node\Stmt\Class_ && Chris@4: $node->namespacedName->toString() === 'Foo\Bar\Baz' Chris@0: ) { Chris@0: $this->class = $node; Chris@0: return NodeTraverser::STOP_TRAVERSAL; Chris@0: } Chris@0: } Chris@0: ``` Chris@0: Chris@0: This works both in enterNode and leaveNode. Note that this particular case can also be more easily Chris@0: handled using a NodeFinder, which will be introduced below. Chris@0: Chris@0: Multiple visitors Chris@0: ----------------- Chris@0: Chris@0: A single traverser can be used with multiple visitors: Chris@0: Chris@0: ```php Chris@0: $traverser = new NodeTraverser; Chris@0: $traverser->addVisitor($visitorA); Chris@0: $traverser->addVisitor($visitorB); Chris@4: $stmts = $traverser->traverse($stmts); Chris@0: ``` Chris@0: Chris@0: It is important to understand that if a traverser is run with multiple visitors, the visitors will Chris@0: be interleaved. Given the following AST excerpt Chris@0: Chris@0: ``` Chris@0: Stmt_Return( Chris@0: expr: Expr_Variable( Chris@0: name: foobar Chris@0: ) Chris@0: ) Chris@0: ``` Chris@0: Chris@0: the following method calls will be performed: Chris@0: Chris@0: ``` Chris@0: $visitorA->enterNode(Stmt_Return) Chris@0: $visitorB->enterNode(Stmt_Return) Chris@0: $visitorA->enterNode(Expr_Variable) Chris@0: $visitorB->enterNode(Expr_Variable) Chris@0: $visitorA->leaveNode(Expr_Variable) Chris@0: $visitorB->leaveNode(Expr_Variable) Chris@0: $visitorA->leaveNode(Stmt_Return) Chris@0: $visitorB->leaveNode(Stmt_Return) Chris@0: ``` Chris@0: Chris@0: That is, when visiting a node, enterNode and leaveNode will always be called for all visitors. Chris@0: Running multiple visitors in parallel improves performance, as the AST only has to be traversed Chris@0: once. However, it is not always possible to write visitors in a way that allows interleaved Chris@0: execution. In this case, you can always fall back to performing multiple traversals: Chris@0: Chris@0: ```php Chris@0: $traverserA = new NodeTraverser; Chris@0: $traverserA->addVisitor($visitorA); Chris@0: $traverserB = new NodeTraverser; Chris@0: $traverserB->addVisitor($visitorB); Chris@0: $stmts = $traverserA->traverser($stmts); Chris@0: $stmts = $traverserB->traverser($stmts); Chris@0: ``` Chris@0: Chris@0: When using multiple visitors, it is important to understand how they interact with the various Chris@0: special enterNode/leaveNode return values: Chris@0: Chris@0: * If *any* visitor returns `DONT_TRAVERSE_CHILDREN`, the children will be skipped for *all* Chris@0: visitors. Chris@4: * If *any* visitor returns `DONT_TRAVERSE_CURRENT_AND_CHILDREN`, the children will be skipped for *all* Chris@4: visitors, and all *subsequent* visitors will not visit the current node. Chris@0: * If *any* visitor returns `STOP_TRAVERSAL`, traversal is stopped for *all* visitors. Chris@0: * If a visitor returns a replacement node, subsequent visitors will be passed the replacement node, Chris@0: not the original one. Chris@0: * If a visitor returns `REMOVE_NODE`, subsequent visitors will not see this node. Chris@0: * If a visitor returns an array of replacement nodes, subsequent visitors will see neither the node Chris@0: that was replaced, nor the replacement nodes. Chris@0: Chris@0: Simple node finding Chris@0: ------------------- Chris@0: Chris@0: While the node visitor mechanism is very flexible, creating a node visitor can be overly cumbersome Chris@0: for minor tasks. For this reason a `NodeFinder` is provided, which can find AST nodes that either Chris@0: satisfy a certain callback, or which are instanced of a certain node type. A couple of examples are Chris@0: shown in the following: Chris@0: Chris@0: ```php Chris@0: use PhpParser\{Node, NodeFinder}; Chris@0: Chris@0: $nodeFinder = new NodeFinder; Chris@0: Chris@0: // Find all class nodes. Chris@0: $classes = $nodeFinder->findInstanceOf($stmts, Node\Stmt\Class_::class); Chris@0: Chris@0: // Find all classes that extend another class Chris@4: $extendingClasses = $nodeFinder->find($stmts, function(Node $node) { Chris@0: return $node instanceof Node\Stmt\Class_ Chris@0: && $node->extends !== null; Chris@0: }); Chris@0: Chris@0: // Find first class occuring in the AST. Returns null if no class exists. Chris@0: $class = $nodeFinder->findFirstInstanceOf($stmts, Node\Stmt\Class_::class); Chris@0: Chris@0: // Find first class that has name $name Chris@0: $class = $nodeFinder->findFirst($stmts, function(Node $node) use ($name) { Chris@0: return $node instanceof Node\Stmt\Class_ Chris@0: && $node->resolvedName->toString() === $name; Chris@0: }); Chris@0: ``` Chris@0: Chris@0: Internally, the `NodeFinder` also uses a node traverser. It only simplifies the interface for a Chris@0: common use case. Chris@0: Chris@0: Parent and sibling references Chris@0: ----------------------------- Chris@0: Chris@0: The node visitor mechanism is somewhat rigid, in that it prescribes an order in which nodes should Chris@0: be accessed: From parents to children. However, it can often be convenient to operate in the Chris@0: reverse direction: When working on a node, you might want to check if the parent node satisfies a Chris@0: certain property. Chris@0: Chris@0: PHP-Parser does not add parent (or sibling) references to nodes by itself, but you can easily Chris@4: emulate this with a visitor. See the [FAQ](FAQ.markdown) for more information.