1 <?php declare(strict_types=1);
5 class NodeTraverser implements NodeTraverserInterface
8 * If NodeVisitor::enterNode() returns DONT_TRAVERSE_CHILDREN, child nodes
9 * of the current node will not be traversed for any visitors.
11 * For subsequent visitors enterNode() will still be called on the current
12 * node and leaveNode() will also be invoked for the current node.
14 const DONT_TRAVERSE_CHILDREN = 1;
17 * If NodeVisitor::enterNode() or NodeVisitor::leaveNode() returns
18 * STOP_TRAVERSAL, traversal is aborted.
20 * The afterTraverse() method will still be invoked.
22 const STOP_TRAVERSAL = 2;
25 * If NodeVisitor::leaveNode() returns REMOVE_NODE for a node that occurs
26 * in an array, it will be removed from the array.
28 * For subsequent visitors leaveNode() will still be invoked for the
31 const REMOVE_NODE = 3;
34 * If NodeVisitor::enterNode() returns DONT_TRAVERSE_CURRENT_AND_CHILDREN, child nodes
35 * of the current node will not be traversed for any visitors.
37 * For subsequent visitors enterNode() will not be called as well.
38 * leaveNode() will be invoked for visitors that has enterNode() method invoked.
40 const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4;
42 /** @var NodeVisitor[] Visitors */
43 protected $visitors = [];
45 /** @var bool Whether traversal should be stopped */
46 protected $stopTraversal;
48 public function __construct() {
55 * @param NodeVisitor $visitor Visitor to add
57 public function addVisitor(NodeVisitor $visitor) {
58 $this->visitors[] = $visitor;
62 * Removes an added visitor.
64 * @param NodeVisitor $visitor
66 public function removeVisitor(NodeVisitor $visitor) {
67 foreach ($this->visitors as $index => $storedVisitor) {
68 if ($storedVisitor === $visitor) {
69 unset($this->visitors[$index]);
76 * Traverses an array of nodes using the registered visitors.
78 * @param Node[] $nodes Array of nodes
80 * @return Node[] Traversed array of nodes
82 public function traverse(array $nodes) : array {
83 $this->stopTraversal = false;
85 foreach ($this->visitors as $visitor) {
86 if (null !== $return = $visitor->beforeTraverse($nodes)) {
91 $nodes = $this->traverseArray($nodes);
93 foreach ($this->visitors as $visitor) {
94 if (null !== $return = $visitor->afterTraverse($nodes)) {
103 * Recursively traverse a node.
105 * @param Node $node Node to traverse.
107 * @return Node Result of traversal (may be original node or new one)
109 protected function traverseNode(Node $node) : Node {
110 foreach ($node->getSubNodeNames() as $name) {
111 $subNode =& $node->$name;
113 if (\is_array($subNode)) {
114 $subNode = $this->traverseArray($subNode);
115 if ($this->stopTraversal) {
118 } elseif ($subNode instanceof Node) {
119 $traverseChildren = true;
120 $breakVisitorIndex = null;
122 foreach ($this->visitors as $visitorIndex => $visitor) {
123 $return = $visitor->enterNode($subNode);
124 if (null !== $return) {
125 if ($return instanceof Node) {
126 $this->ensureReplacementReasonable($subNode, $return);
128 } elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
129 $traverseChildren = false;
130 } elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
131 $traverseChildren = false;
132 $breakVisitorIndex = $visitorIndex;
134 } elseif (self::STOP_TRAVERSAL === $return) {
135 $this->stopTraversal = true;
138 throw new \LogicException(
139 'enterNode() returned invalid value of type ' . gettype($return)
145 if ($traverseChildren) {
146 $subNode = $this->traverseNode($subNode);
147 if ($this->stopTraversal) {
152 foreach ($this->visitors as $visitorIndex => $visitor) {
153 $return = $visitor->leaveNode($subNode);
155 if (null !== $return) {
156 if ($return instanceof Node) {
157 $this->ensureReplacementReasonable($subNode, $return);
159 } elseif (self::STOP_TRAVERSAL === $return) {
160 $this->stopTraversal = true;
162 } elseif (\is_array($return)) {
163 throw new \LogicException(
164 'leaveNode() may only return an array ' .
165 'if the parent structure is an array'
168 throw new \LogicException(
169 'leaveNode() returned invalid value of type ' . gettype($return)
174 if ($breakVisitorIndex === $visitorIndex) {
185 * Recursively traverse array (usually of nodes).
187 * @param array $nodes Array to traverse
189 * @return array Result of traversal (may be original array or changed one)
191 protected function traverseArray(array $nodes) : array {
194 foreach ($nodes as $i => &$node) {
195 if ($node instanceof Node) {
196 $traverseChildren = true;
197 $breakVisitorIndex = null;
199 foreach ($this->visitors as $visitorIndex => $visitor) {
200 $return = $visitor->enterNode($node);
201 if (null !== $return) {
202 if ($return instanceof Node) {
203 $this->ensureReplacementReasonable($node, $return);
205 } elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
206 $traverseChildren = false;
207 } elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
208 $traverseChildren = false;
209 $breakVisitorIndex = $visitorIndex;
211 } elseif (self::STOP_TRAVERSAL === $return) {
212 $this->stopTraversal = true;
215 throw new \LogicException(
216 'enterNode() returned invalid value of type ' . gettype($return)
222 if ($traverseChildren) {
223 $node = $this->traverseNode($node);
224 if ($this->stopTraversal) {
229 foreach ($this->visitors as $visitorIndex => $visitor) {
230 $return = $visitor->leaveNode($node);
232 if (null !== $return) {
233 if ($return instanceof Node) {
234 $this->ensureReplacementReasonable($node, $return);
236 } elseif (\is_array($return)) {
237 $doNodes[] = [$i, $return];
239 } elseif (self::REMOVE_NODE === $return) {
240 $doNodes[] = [$i, []];
242 } elseif (self::STOP_TRAVERSAL === $return) {
243 $this->stopTraversal = true;
245 } elseif (false === $return) {
246 throw new \LogicException(
247 'bool(false) return from leaveNode() no longer supported. ' .
248 'Return NodeTraverser::REMOVE_NODE instead'
251 throw new \LogicException(
252 'leaveNode() returned invalid value of type ' . gettype($return)
257 if ($breakVisitorIndex === $visitorIndex) {
261 } elseif (\is_array($node)) {
262 throw new \LogicException('Invalid node structure: Contains nested arrays');
266 if (!empty($doNodes)) {
267 while (list($i, $replace) = array_pop($doNodes)) {
268 array_splice($nodes, $i, 1, $replace);
275 private function ensureReplacementReasonable($old, $new) {
276 if ($old instanceof Node\Stmt && $new instanceof Node\Expr) {
277 throw new \LogicException(
278 "Trying to replace statement ({$old->getType()}) " .
279 "with expression ({$new->getType()}). Are you missing a " .
280 "Stmt_Expression wrapper?"
284 if ($old instanceof Node\Expr && $new instanceof Node\Stmt) {
285 throw new \LogicException(
286 "Trying to replace expression ({$old->getType()}) " .
287 "with statement ({$new->getType()})"